فهرست منبع

download video for audio files, preimplementation

Milan Jurkulak 3 سال پیش
والد
کامیت
90c981be89
19فایلهای تغییر یافته به همراه1205 افزوده شده و 14 حذف شده
  1. 1 1
      app/build.gradle.kts
  2. 4 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt
  3. 6 1
      tvlib/build.gradle.kts
  4. 120 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/AudioPreview.kt
  5. 14 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/IPrevieEngine.kt
  6. 13 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/NetworkListener.kt
  7. 53 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/NetworkReceiver.kt
  8. 34 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/Command.kt
  9. 196 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/DailyMotionVideoView.kt
  10. 22 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/DailyMotionVideoViewEventListener.kt
  11. 12 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/EventListener.kt
  12. 569 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/PlayerWebView.kt
  13. 19 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/SearchItem.kt
  14. 36 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/SearchResult.kt
  15. 20 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/TRunnable.kt
  16. 69 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/ThreadOnce.kt
  17. 3 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/ExoPlayerImpl.kt
  18. 14 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerContainer.kt
  19. 0 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/NativePlayerImpl.kt

+ 1 - 1
app/build.gradle.kts

@@ -30,11 +30,11 @@ plugins {
     id("kotlin-parcelize")
     id("com.google.dagger.hilt.android")
     id("dagger.hilt.android.plugin")
-//    id("org.jetbrains.dokka") version "1.8.10"
     id("io.objectbox")
     id("MainAppPlugin")
     kotlin("kapt")
     id("com.google.devtools.ksp")
+//    id("org.jetbrains.dokka") version "1.8.10"
 }
 
 android {

+ 4 - 1
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt

@@ -94,6 +94,8 @@ class IPTVScreen : Screen() {
             else -> null
         }
 
+        val isAudio = currentData is ItemAudio
+
         Box(
             modifier = Modifier
                 .fillMaxSize()
@@ -101,7 +103,8 @@ class IPTVScreen : Screen() {
         ) {
             MediaPlayerContainer(
                 modifier = Modifier.fillMaxSize(),
-                uri = mediaUri?.parseUri(),
+                isAudio = isAudio,
+                uri = mediaUri?.parseUri() ?: Uri.EMPTY,
                 title = mediaTitle,
                 subtitle = mediaUri
             )

+ 6 - 1
tvlib/build.gradle.kts

@@ -1,5 +1,6 @@
 @file:Suppress("UnstableApiUsage")
 
+import org.mjdev.gradle.extensions.implementation
 import org.mjdev.gradle.plugin.MainAppPlugin.Companion.javaVersion
 import org.mjdev.gradle.plugin.MainAppPlugin.Companion.kotlinCompilerExtVersion
 import org.mjdev.gradle.plugin.MainAppPlugin.Companion.projectCompileSdk
@@ -18,9 +19,9 @@ plugins {
     kotlin("android")
     id("com.google.dagger.hilt.android")
     id("dagger.hilt.android.plugin")
-//    id("org.jetbrains.dokka") version "1.8.10"
     kotlin("kapt")
     id("com.google.devtools.ksp")
+//    id("org.jetbrains.dokka") version "1.8.10"
 }
 
 android {
@@ -104,6 +105,10 @@ dependencies {
     // okhttp
     implementation("com.squareup.okhttp3:okhttp:4.11.0")
     implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
+    // moshi json
+    implementation("com.squareup.moshi:moshi:1.15.0")
+    implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
+    ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
     // image loading
     implementation("io.coil-kt:coil-base:2.4.0")
     implementation("io.coil-kt:coil-gif:2.4.0")

+ 120 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/AudioPreview.kt

@@ -0,0 +1,120 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.audiopreview
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.ColorDrawable
+import android.net.Uri
+import android.view.Gravity
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import org.mjdev.tvlib.ui.components.audiopreview.dailymotion.DailyMotionVideoView
+import org.mjdev.tvlib.ui.components.media.MediaPlayerState
+import timber.log.Timber
+
+//todo multiple engines
+@SuppressLint("ViewConstructor")
+class AudioPreview constructor(
+    context: Context,
+    val uri: Uri = Uri.EMPTY
+) : FrameLayout(context) {
+
+    private val previewEngine: IPreviewEngine by lazy {
+        DailyMotionVideoView(context)
+    }
+
+    private var networkListener: NetworkReceiver? = null
+
+    init {
+        background = ColorDrawable(0)
+        addView(
+            previewEngine as View,
+            LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                Gravity.CENTER
+            )
+        )
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        networkListener = NetworkReceiver(context, object : NetworkListener {
+            override fun onNetworkAvailable() {
+                this@AudioPreview.onNetworkAvailable()
+            }
+
+            override fun onNetworkUnavailable() {
+                this@AudioPreview.onNetworkUnavailable()
+            }
+        })
+    }
+
+    fun onNetworkUnavailable() {
+        previewEngine.pause()
+    }
+
+    fun onNetworkAvailable() {
+        if (uri != Uri.EMPTY) {
+            previewEngine.searchAndPlayIfFound(
+                filePath = uri.toString(),
+                muted = true,
+                success = {},
+                error = { error ->
+                    Timber.e(error)
+                }
+            )
+        }
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        previewEngine.pause()
+        if (networkListener != null) {
+            networkListener!!.unregister(context)
+        }
+    }
+
+    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+        return false
+    }
+
+    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+        return false
+    }
+
+    companion object {
+
+        // todo video stop pause resume & etc
+        @Suppress("UNUSED_PARAMETER")
+        @Composable
+        fun AudioPreview(
+            modifier: Modifier = Modifier,
+            uri: Uri = Uri.EMPTY,
+            state: MediaPlayerState,
+            onError: @Composable () -> Unit = {},
+        ) {
+            AndroidView(
+                modifier = modifier,
+                factory = { context ->
+                    AudioPreview(context, uri)
+                }
+            )
+        }
+
+    }
+
+}

+ 14 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/IPrevieEngine.kt

@@ -0,0 +1,14 @@
+package org.mjdev.tvlib.ui.components.audiopreview
+
+interface IPreviewEngine {
+
+    fun searchAndPlayIfFound(
+        filePath: String?,
+        muted: Boolean = true,
+        success: () -> Unit = {},
+        error: (error: Exception) -> Unit = {}
+    )
+
+    fun pause()
+
+}

+ 13 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/NetworkListener.kt

@@ -0,0 +1,13 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+package org.mjdev.tvlib.ui.components.audiopreview
+
+internal interface NetworkListener {
+    fun onNetworkAvailable()
+    fun onNetworkUnavailable()
+}

+ 53 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/NetworkReceiver.kt

@@ -0,0 +1,53 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+package org.mjdev.tvlib.ui.components.audiopreview
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.ConnectivityManager
+
+@Suppress("DEPRECATION")
+internal class NetworkReceiver(
+    val context:Context,
+    private val networkListener: NetworkListener
+) : BroadcastReceiver() {
+
+    init {
+        context.registerReceiver(
+            this,
+            IntentFilter(
+                ConnectivityManager.CONNECTIVITY_ACTION
+            )
+        )
+    }
+
+    override fun onReceive(context: Context, intent: Intent) {
+        if (isOnline(context)) {
+            networkListener.onNetworkAvailable()
+        } else {
+            networkListener.onNetworkUnavailable()
+        }
+    }
+
+    fun unregister(context: Context) {
+        context.unregisterReceiver(this)
+    }
+
+    companion object {
+
+        fun isOnline(context: Context): Boolean {
+            val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+            val netInfo = cm.activeNetworkInfo
+            return netInfo != null && netInfo.isConnected
+        }
+
+    }
+
+}

+ 34 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/Command.kt

@@ -0,0 +1,34 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+package org.mjdev.tvlib.ui.components.audiopreview.dailymotion
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class Command (
+    @Json(name = "methodName")
+    var methodName: String = "",
+    @Json(name = "params")
+    var params: Array<out Any> = emptyArray()
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+        other as Command
+        if (methodName != other.methodName) return false
+        if (!params.contentEquals(other.params)) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = methodName.hashCode()
+        result = 31 * result + params.contentHashCode()
+        return result
+    }
+}

+ 196 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/DailyMotionVideoView.kt

@@ -0,0 +1,196 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.audiopreview.dailymotion
+
+import android.content.Context
+import android.graphics.drawable.ColorDrawable
+import android.media.MediaMetadataRetriever
+import android.util.AttributeSet
+import android.view.KeyEvent
+import android.view.MotionEvent
+import com.squareup.moshi.Moshi
+import okhttp3.Cache
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.logging.HttpLoggingInterceptor
+import org.mjdev.tvlib.ui.components.audiopreview.IPreviewEngine
+import org.mjdev.tvlib.ui.components.audiopreview.dailymotion.ThreadOnce.Companion.runInThreadOnce
+import org.mjdev.tvlib.helpers.coil.UserAgentInterceptor
+import org.mjdev.tvlib.network.CacheInterceptor
+import timber.log.Timber
+import java.io.File
+import java.io.IOException
+import java.net.URLEncoder
+
+@Suppress("unused", "PrivatePropertyName", "DEPRECATION")
+class DailyMotionVideoView(
+    context: Context, attrs: AttributeSet? = null
+) : PlayerWebView(
+    context,
+    attrs
+), EventListener, IPreviewEngine {
+
+    private val DM_URL =
+        "https://api.dailymotion.com/videos?fields=id,thumbnail_url,title&search=%s&page=1&limit=1"
+
+    private var dmListener: DailyMotionVideoViewEventListener? = null
+
+    private val USER_AGENT =
+        "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0"
+
+    private val httpCache by lazy {
+        Cache(
+            directory = File(
+                context.applicationContext.cacheDir,
+                "http_cache"
+            ),
+            maxSize = 1024L * 1024L * 1024L
+        )
+    }
+
+    private val userAgentInterceptor by lazy {
+        UserAgentInterceptor(USER_AGENT)
+    }
+
+    private val cacheInterceptor by lazy {
+        CacheInterceptor()
+    }
+
+    private val httpLoggingInterceptor by lazy {
+        HttpLoggingInterceptor().apply {
+            level = HttpLoggingInterceptor.Level.BODY
+        }
+    }
+
+    private val httpClient: OkHttpClient by lazy {
+        OkHttpClient.Builder().apply {
+            addNetworkInterceptor(userAgentInterceptor)
+            addNetworkInterceptor(cacheInterceptor)
+            addNetworkInterceptor(httpLoggingInterceptor)
+            cache(httpCache)
+        }.build()
+    }
+
+    private fun call(url: String) = httpClient.newCall(
+        Request.Builder().url(url).build()
+    ).execute()
+
+    init {
+        background = ColorDrawable(0)
+        visibility = INVISIBLE
+        setEventListener(this)
+    }
+
+    override fun searchAndPlayIfFound(
+        filePath: String?,
+        muted: Boolean,
+        success: () -> Unit,
+        error: (error: Exception) -> Unit
+    ) {
+        if (muted) mute()
+        var q: String?
+        if (filePath != null) {
+            var mr: MediaMetadataRetriever? = null
+            try {
+                mr = MediaMetadataRetriever().apply {
+                    setDataSource(filePath)
+                }
+                val author = mr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)
+                val artist = mr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
+                val genre = mr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE)
+                val year = mr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR)
+                val title = mr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
+                q = String.format(
+                    "%s %s %s %s %s",
+                    genre ?: "",
+                    author ?: "",
+                    artist ?: "",
+                    year ?: "",
+                    title ?: ""
+                )
+            } catch (e: Exception) {
+                q = File(filePath).name.substringBeforeLast(".")
+                Timber.e(e)
+            } finally {
+                if (mr != null) {
+                    try {
+                        mr.release()
+                    } catch (e: IOException) {
+                        // todo
+//                        Timber.e(e)
+                    }
+                }
+            }
+            if (q == null || q.trim { it <= ' ' }.isEmpty()) {
+                q = File(filePath).name.substringBeforeLast(".")
+            }
+            runInThreadOnce {
+                try {
+                    val url = String.format(
+                        DM_URL,
+                        URLEncoder.encode(q)
+                    )
+                    val json = call(url).body?.string() ?: ""
+                    val result: SearchResult? = fromSimpleJSONString<SearchResult>(json)
+                    val videoId = if (result?.list != null && result.list.isNotEmpty())
+                        result.list[0].id
+                    else null
+                    if (videoId != null) {
+                        post {
+                            visibility = VISIBLE
+                            load(videoId)
+                            success()
+                        }
+                    }
+                } catch (e: Exception) {
+                    Timber.e(e)
+                    post {
+                        visibility = GONE
+                        error(e)
+                    }
+                }
+            }
+        } else {
+            visibility = GONE
+        }
+    }
+
+    private inline fun <reified T> fromSimpleJSONString(json: String): T? {
+        return Moshi.Builder().build().adapter(T::class.java).fromJson(json)
+    }
+
+    fun setListener(listener: DailyMotionVideoViewEventListener?) {
+        dmListener = listener
+    }
+
+    fun resume(position: Long) {
+        super.seek(position.toDouble())
+    }
+
+    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+        return false
+    }
+
+    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+        return false
+    }
+
+    override fun onEvent(event: String?, map: HashMap<String?, String?>?) {
+        post {
+            if (event.equals(EVENT_VIDEO_START, ignoreCase = true)) {
+                visibility = VISIBLE
+                background = ColorDrawable(-0x1000000)
+            } else if (event.equals(EVENT_VIDEO_END, ignoreCase = true)) {
+                visibility = INVISIBLE
+                background = ColorDrawable(0)
+            }
+        }
+    }
+
+}

+ 22 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/DailyMotionVideoViewEventListener.kt

@@ -0,0 +1,22 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+package org.mjdev.tvlib.ui.components.audiopreview.dailymotion
+
+@Suppress("unused")
+interface DailyMotionVideoViewEventListener {
+    fun onError(message: String?)
+    fun onTimeChanged(time: Int)
+    fun onEndReached()
+    fun onPlaying(duration: Int)
+    fun onPaused()
+    fun onSeekPerformed()
+    fun onBuffering(buffering: Int)
+    fun onReady()
+    fun onNetworkAvailable()
+    fun onNetworkUnAvailable()
+}

+ 12 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/EventListener.kt

@@ -0,0 +1,12 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+package org.mjdev.tvlib.ui.components.audiopreview.dailymotion
+
+interface EventListener {
+    fun onEvent(event: String?, map: HashMap<String?, String?>?)
+}

+ 569 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/PlayerWebView.kt

@@ -0,0 +1,569 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.audiopreview.dailymotion
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import android.webkit.JavascriptInterface
+import android.webkit.WebChromeClient
+import android.webkit.WebResourceResponse
+import android.webkit.WebSettings
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.ProgressBar
+import com.squareup.moshi.Moshi
+import timber.log.Timber
+import java.io.IOException
+import java.net.URLDecoder
+import java.util.Locale
+
+/**
+ * @noinspection unused
+ */
+@Suppress(
+    "SameParameterValue", "unused", "DEPRECATION", "OVERRIDE_DEPRECATION",
+    "MemberVisibilityCanBePrivate"
+)
+open class PlayerWebView @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null
+) : WebView(context, attrs) {
+
+    private val commandList by lazy { ArrayList<Command>() }
+
+    private var isPlaying = false
+    private var disallowIntercept = false
+    private var videoId: String? = null
+    private var apiReady = false
+    private var position = 0f
+    private var visible = false
+    private var hasMetadata = false
+    private var eventListener: EventListener? = null
+    private var isWebContentsDebuggingEnabled = true
+    private var controlsCommandRunnable: Runnable? = null
+    private var muteCommandRunnable: Runnable? = null
+    private var loadCommandRunnable: Runnable? = null
+    private var videoPaused = false
+    private var bufferedTime = 0.0
+    private var duration = 0.0
+    private var isSeeking = false
+    private var isEnded = false
+    private var isInitialized = false
+    private var isFullScreen = false
+    private var controlsLastTime: Long = 0
+    private var muteLastTime: Long = 0
+    private var loadLastTime: Long = 0
+
+    var quality: String = ""
+        set(quality) {
+            field = quality
+            queueCommand(COMMAND_QUALITY, quality)
+        }
+
+    var playWhenReady: Boolean = true
+        set(playWhenReady) {
+            field = playWhenReady
+            updatePlayState()
+        }
+
+    @SuppressLint("SetJavaScriptEnabled")
+    private fun initialize(
+        baseUrl: String?,
+        queryParameters: Map<String, String>?,
+        httpHeaders: Map<String?, String?>?
+    ) {
+        isInitialized = true
+        val mWebSettings = settings
+        mWebSettings.domStorageEnabled = true
+        mWebSettings.javaScriptEnabled = true
+        mWebSettings.pluginState = WebSettings.PluginState.ON
+        mWebSettings.mediaPlaybackRequiresUserGesture = false
+        setWebContentsDebuggingEnabled(isWebContentsDebuggingEnabled)
+        addJavascriptInterface(object : Any() {
+            @JavascriptInterface
+            fun triggerEvent(e: String) {
+                handler.post {
+                    handleEvent(e)
+                }
+            }
+        }, "dmpNativeBridge")
+        webViewClient = object : WebViewClient() {
+            override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
+                if (url.startsWith(ASSETS_SCHEME)) {
+                    val asset = url.substring(ASSETS_SCHEME.length)
+                    if (asset.endsWith(".ttf") || asset.endsWith(".otf")) {
+                        try {
+                            val inputStream = context.assets.open(asset)
+                            val response: WebResourceResponse?
+                            val encoding = "UTF-8"
+                            val statusCode = 200
+                            val reasonPhase = "OK"
+                            val responseHeaders: MutableMap<String, String> = HashMap()
+                            responseHeaders["Access-Control-Allow-Origin"] = "*"
+                            response = WebResourceResponse(
+                                "font/ttf",
+                                encoding,
+                                statusCode,
+                                reasonPhase,
+                                responseHeaders,
+                                inputStream
+                            )
+                            return response
+                        } catch (e: IOException) {
+                            Timber.e(e)
+                        }
+                    }
+                }
+                return null
+            }
+
+            override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
+                return true
+            }
+
+        }
+        webChromeClient = object : WebChromeClient() {
+            override fun getVideoLoadingProgressView(): View {
+                val pb = ProgressBar(context)
+                pb.isIndeterminate = true
+                return pb
+            }
+
+            override fun onShowCustomView(view: View, callback: CustomViewCallback) {
+            }
+
+            override fun getDefaultVideoPoster(): Bitmap {
+                val colors = IntArray(1)
+                colors[0] = Color.TRANSPARENT
+                return Bitmap.createBitmap(
+                    colors,
+                    0,
+                    1,
+                    1,
+                    1,
+                    Bitmap.Config.ARGB_8888
+                )
+            }
+
+            override fun onHideCustomView() {
+            }
+        }
+        val parameters: MutableMap<String, String> = HashMap()
+        parameters["app"] = context.packageName
+        parameters["api"] = "nativeBridge"
+        parameters.putAll(queryParameters!!)
+        val builder = StringBuilder()
+        builder.append(baseUrl)
+        var isFirstParameter = true
+        for ((key, value) in parameters) {
+            if (isFirstParameter) {
+                isFirstParameter = false
+                builder.append('?')
+            } else {
+                builder.append('&')
+            }
+            builder.append(key)
+            builder.append('=')
+            builder.append(value)
+        }
+        loadUrl(builder.toString(), httpHeaders!!)
+    }
+
+    fun setVisible(visible: Boolean) {
+        if (this.visible != visible) {
+            this.visible = visible
+            if (!this.visible) {
+                playWhenReady = false
+            }
+            if (!this.visible) {
+                pauseTimers()
+                onPause()
+            } else {
+                resumeTimers()
+                onResume()
+            }
+        }
+    }
+
+    private fun updatePlayState() {
+        if (!visible) {
+            pause()
+        } else {
+            if (playWhenReady) {
+                play()
+            } else {
+                pause()
+            }
+        }
+    }
+
+    fun setMinimizeProgress(p: Float) = showControls(p <= 0)
+
+    fun setIsLiked(isLiked: Boolean) = queueCommand(COMMAND_NOTIFY_LIKECHANGED, isLiked)
+
+    fun setIsInWatchLater(isInWatchLater: Boolean) =
+        queueCommand(COMMAND_NOTIFY_WATCHLATERCHANGED, isInWatchLater)
+
+    private fun sendCommand(command: Command) {
+        when (command.methodName) {
+            COMMAND_MUTE -> callPlayerMethod(if (command.params[0] as Boolean) "mute" else "unmute")
+            COMMAND_CONTROLS -> callPlayerMethod(
+                "api",
+                "controls",
+                if (command.params[0] as Boolean) "true" else "false"
+            )
+
+            COMMAND_QUALITY -> callPlayerMethod("api", "quality", command.params[0])
+            COMMAND_SUBTITLE -> callPlayerMethod("api", "subtitle", command.params[0])
+            COMMAND_TOGGLE_CONTROLS -> callPlayerMethod("api", "toggle-controls", command.params)
+            COMMAND_TOGGLE_PLAY -> callPlayerMethod("api", "toggle-play", command.params)
+            else -> callPlayerMethod(command.methodName, *command.params)
+        }
+    }
+
+    private fun handleEvent(e: String) {
+        var ev = e
+        ev = URLDecoder.decode(ev)
+        val p = ev.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+        val map = HashMap<String?, String?>()
+        for (s in p) {
+            val s2 = s.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+            when (s2.size) {
+                1 -> map[s2[0]] = null
+                2 -> map[s2[0]] = s2[1]
+                else -> {
+                    Timber.e("bad param: $s")
+                }
+            }
+        }
+        val event = map["event"]
+        if (event == null) {
+            Timber.e("bad event 2: $e")
+            return
+        }
+        if (event != "timeupdate") {
+            Timber.d("[%d] event %s", hashCode(), e)
+        }
+        when (event) {
+            EVENT_AD_START -> {}
+            EVENT_AD_PLAY -> {}
+            EVENT_AD_END -> {}
+            EVENT_VIDEO_START -> {
+                hideControls()
+                isPlaying = true
+            }
+
+            EVENT_APIREADY -> apiReady = true
+            EVENT_START -> {
+                isEnded = false
+                loadCommandRunnable?.also { c ->
+                    handler.removeCallbacks(c)
+                }
+                loadCommandRunnable = null
+            }
+
+            EVENT_END -> isEnded = true
+            EVENT_PROGRESS -> bufferedTime = map["time"]?.toDouble() ?: 0.0
+            EVENT_TIMEUPDATE -> position = map["time"]?.toFloat() ?: 0f
+            EVENT_DURATION_CHANGE -> duration = map["duration"]?.toDouble() ?: 0.0
+            EVENT_GESTURE_START, EVENT_MENU_DID_SHOW -> disallowIntercept = true
+            EVENT_GESTURE_END, EVENT_MENU_DID_HIDE -> disallowIntercept = false
+            EVENT_VIDEO_END -> isPlaying = false
+            EVENT_PLAY -> {
+                videoPaused = false
+                playWhenReady = true
+            }
+
+            EVENT_PAUSE -> {
+                videoPaused = true
+                playWhenReady = false
+            }
+
+            EVENT_CONTROLSCHANGE -> {
+                controlsCommandRunnable?.also { c ->
+                    handler.removeCallbacks(c)
+                }
+                controlsCommandRunnable = null
+            }
+
+            EVENT_VOLUMECHANGE -> {
+                muteCommandRunnable?.also { c ->
+                    handler.removeCallbacks(c)
+                }
+                muteCommandRunnable = null
+            }
+
+            EVENT_LOADEDMETADATA -> hasMetadata = true
+            EVENT_QUALITY -> quality = map["quality"] ?: ""
+            EVENT_SEEKED -> {
+                isSeeking = false
+                position = map["time"]?.toFloat() ?: 0f
+            }
+
+            EVENT_SEEKING -> {
+                isSeeking = true
+                position = map["time"]?.toFloat() ?: 0f
+            }
+
+            EVENT_FULLSCREEN_TOGGLE_REQUESTED -> {}
+        }
+        eventListener?.onEvent(event, map)
+        tick()
+    }
+
+    private fun tick() {
+        if (!apiReady) {
+            return
+        }
+        val iterator = commandList.iterator()
+        while (iterator.hasNext()) {
+            val command = iterator.next()
+            when (command.methodName) {
+                COMMAND_NOTIFY_LIKECHANGED -> {
+                    if (!hasMetadata) {
+                        continue
+                    }
+                }
+
+                COMMAND_NOTIFY_WATCHLATERCHANGED -> {
+                    if (!hasMetadata) {
+                        continue
+                    }
+                }
+
+                COMMAND_MUTE -> {
+                    if (System.currentTimeMillis() - muteLastTime < 1000) {
+                        continue
+                    }
+                    muteLastTime = System.currentTimeMillis()
+                }
+
+                COMMAND_LOAD -> {
+                    if (System.currentTimeMillis() - loadLastTime < 1000) {
+                        continue
+                    }
+                    loadLastTime = System.currentTimeMillis()
+                }
+
+                COMMAND_CONTROLS -> {
+                    if (System.currentTimeMillis() - controlsLastTime < 1000) {
+                        continue
+                    }
+                    controlsLastTime = System.currentTimeMillis()
+                }
+            }
+            iterator.remove()
+            sendCommand(command)
+        }
+    }
+
+    private fun queueCommand(method: String, vararg params: Any) {
+        var iterator = commandList.iterator()
+        while (iterator.hasNext()) {
+            if (iterator.next().methodName == method) {
+                iterator.remove()
+            }
+        }
+        if (method == COMMAND_LOAD) {
+            position = 0f
+            disallowIntercept = false
+            videoId = params[0] as String?
+            hasMetadata = false
+            iterator = commandList.iterator()
+            while (iterator.hasNext()) {
+                when (iterator.next().methodName) {
+                    COMMAND_NOTIFY_LIKECHANGED,
+                    COMMAND_NOTIFY_WATCHLATERCHANGED,
+                    COMMAND_SEEK,
+                    COMMAND_PAUSE,
+                    COMMAND_PLAY -> iterator.remove()
+                }
+            }
+        }
+        val command = Command()
+        command.methodName = method
+        command.params = params
+        commandList.add(command)
+        tick()
+    }
+
+    private fun callPlayerMethod(method: String, vararg params: Any) {
+        val builder = StringBuilder()
+        builder.append("javascript:player.")
+        builder.append(method)
+        builder.append('(')
+        var count = 0
+        for (o in params) {
+            count++
+            when (o) {
+                is String -> builder.append("'").append(o).append("'")
+                is Number -> builder.append(o)
+                is Boolean -> builder.append(o)
+                else -> builder.append("JSON.parse('" + toSimpleJSON(o) + "')")
+            }
+            if (count < params.size) {
+                builder.append(",")
+            }
+        }
+        builder.append(')')
+        val js = builder.toString()
+        loadUrl(js)
+    }
+
+    private inline fun <reified T> toSimpleJSON(o: T): String {
+        return Moshi.Builder().build().adapter(T::class.java).toJson(o)
+    }
+
+    fun mute(mute: Boolean) {
+        queueCommand(COMMAND_MUTE, mute)
+    }
+
+    fun mute() {
+        mute(true)
+    }
+
+    fun unmute() {
+        mute(false)
+    }
+
+    fun play() {
+        queueCommand(COMMAND_PLAY)
+    }
+
+    fun pause() {
+        queueCommand(COMMAND_PAUSE)
+    }
+
+    fun seek(time: Double) {
+        queueCommand(COMMAND_SEEK, time)
+    }
+
+    fun showControls(show: Boolean) {
+        queueCommand(COMMAND_CONTROLS, show)
+    }
+
+    fun hideControls() {
+        queueCommand(COMMAND_CONTROLS, false)
+    }
+
+    fun setFullscreenButton(fullScreen: Boolean) {
+        if (fullScreen != isFullScreen) {
+            isFullScreen = fullScreen
+            queueCommand(COMMAND_NOTIFYFULLSCREENCHANGED)
+        }
+    }
+
+    fun setEventListener(listener: EventListener?) {
+        eventListener = listener
+    }
+
+    fun release() {
+        loadUrl("about:blank")
+        onPause()
+    }
+
+    @JvmOverloads
+    fun load(videoId: String?, params: Map<String, String> = emptyMap()) {
+        if (!isInitialized) {
+            val defaultQueryParameters: MutableMap<String, String> = HashMap()
+            defaultQueryParameters["sharing-enable"] = "false"
+            defaultQueryParameters["watchlater-enable"] = "false"
+            defaultQueryParameters["like-enable"] = "false"
+            defaultQueryParameters["collections-enable"] = "false"
+            defaultQueryParameters["fullscreen-action"] = "trigger_event"
+            defaultQueryParameters["locale"] = Locale.getDefault().language
+            initialize(
+                "https://www.dailymotion.com/embed/",
+                defaultQueryParameters,
+                HashMap()
+            )
+        }
+        Timber.d("Playing video: $videoId")
+        queueCommand(COMMAND_LOAD, videoId ?: "", params)
+    }
+
+    fun setSubtitle(languageCode: String) = queueCommand(COMMAND_SUBTITLE, languageCode)
+
+    fun toggleControls() = queueCommand(COMMAND_TOGGLE_CONTROLS)
+
+    fun togglePlay() = queueCommand(COMMAND_TOGGLE_PLAY)
+
+    fun getPosition(): Long = (position * 1000).toLong()
+
+    fun setIsWebContentsDebuggingEnabled(isWebContentsDebuggingEnabled: Boolean) {
+        this.isWebContentsDebuggingEnabled = isWebContentsDebuggingEnabled
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    override fun onTouchEvent(event: MotionEvent): Boolean {
+        if (disallowIntercept) {
+            requestDisallowInterceptTouchEvent(true)
+        }
+        return super.onTouchEvent(event)
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        super.onConfigurationChanged(newConfig)
+    }
+
+    companion object {
+
+        const val EVENT_APIREADY = "apiready"
+        const val EVENT_TIMEUPDATE = "timeupdate"
+        const val EVENT_DURATION_CHANGE = "durationchange"
+        const val EVENT_PROGRESS = "progress"
+        const val EVENT_SEEKED = "seeked"
+        const val EVENT_SEEKING = "seeking"
+        const val EVENT_GESTURE_START = "gesture_start"
+        const val EVENT_GESTURE_END = "gesture_end"
+        const val EVENT_MENU_DID_SHOW = "menu_did_show"
+        const val EVENT_MENU_DID_HIDE = "menu_did_hide"
+        const val EVENT_VIDEO_START = "video_start"
+        const val EVENT_VIDEO_END = "video_end"
+        const val EVENT_AD_START = "ad_start"
+        const val EVENT_AD_PLAY = "ad_play"
+        const val EVENT_AD_END = "ad_end"
+        const val EVENT_ADD_TO_COLLECTION_REQUESTED = "add_to_collection_requested"
+        const val EVENT_LIKE_REQUESTED = "like_requested"
+        const val EVENT_WATCH_LATER_REQUESTED = "watch_later_requested"
+        const val EVENT_SHARE_REQUESTED = "share_requested"
+        const val EVENT_FULLSCREEN_TOGGLE_REQUESTED = "fullscreen_toggle_requested"
+        const val EVENT_PLAY = "play"
+        const val EVENT_PAUSE = "pause"
+        const val EVENT_LOADEDMETADATA = "loadedmetadata"
+        const val EVENT_PLAYING = "playing"
+        const val EVENT_START = "start"
+        const val EVENT_END = "end"
+        const val EVENT_CONTROLSCHANGE = "controlschange"
+        const val EVENT_VOLUMECHANGE = "volumechange"
+        const val EVENT_QUALITY = "qualitychange"
+        private const val ASSETS_SCHEME = "asset://"
+        const val COMMAND_NOTIFY_LIKECHANGED = "notifyLikeChanged"
+        const val COMMAND_NOTIFY_WATCHLATERCHANGED = "notifyWatchLaterChanged"
+        const val COMMAND_NOTIFYFULLSCREENCHANGED = "notifyFullscreenChanged"
+        const val COMMAND_LOAD = "load"
+        const val COMMAND_MUTE = "mute"
+        const val COMMAND_CONTROLS = "controls"
+        const val COMMAND_PLAY = "play"
+        const val COMMAND_PAUSE = "pause"
+        const val COMMAND_SEEK = "seek"
+        const val COMMAND_SETPROP = "setProp"
+        const val COMMAND_QUALITY = "quality"
+        const val COMMAND_SUBTITLE = "subtitle"
+        const val COMMAND_TOGGLE_CONTROLS = "toggle-controls"
+        const val COMMAND_TOGGLE_PLAY = "toggle-play"
+
+    }
+
+}

+ 19 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/SearchItem.kt

@@ -0,0 +1,19 @@
+package org.mjdev.tvlib.ui.components.audiopreview.dailymotion
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@Suppress("unused")
+@JsonClass(generateAdapter = true)
+data class SearchItem(
+
+    @Json(name = "id")
+    var id: String? = null,
+
+    @Json(name = "thumbnail_url")
+    var thumbnailUrl: String? = null,
+
+    @Json(name = "title")
+    var title: String? = null,
+
+)

+ 36 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/SearchResult.kt

@@ -0,0 +1,36 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.audiopreview.dailymotion
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@Suppress("unused")
+@JsonClass(generateAdapter = true)
+data class SearchResult (
+
+    @Json(name = "page")
+    var page: Long = 0,
+
+    @Json(name = "limit")
+    var limit: Long = 0,
+
+    @Json(name = "explicit")
+    var explicit: Boolean = false,
+
+    @Json(name = "total")
+    var total: Long = 0,
+
+    @Json(name = "hasMore")
+    var hasMore: Boolean = false,
+
+    @Json(name = "list")
+    var list: List<SearchItem> = emptyList(),
+
+    )

+ 20 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/TRunnable.kt

@@ -0,0 +1,20 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+package org.mjdev.tvlib.ui.components.audiopreview.dailymotion
+
+abstract class TRunnable<T> : Thread(), Runnable {
+
+    var value: T? = null
+
+    protected abstract fun runWithResult(): T
+
+    override fun run() {
+        value = runWithResult()
+    }
+
+}

+ 69 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/ThreadOnce.kt

@@ -0,0 +1,69 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+package org.mjdev.tvlib.ui.components.audiopreview.dailymotion
+
+import androidx.annotation.AnyThread
+import timber.log.Timber
+
+@Suppress("unused")
+open class ThreadOnce(
+    private val runnable: Runnable
+) : Thread() {
+
+    @Suppress("DEPRECATION")
+    override fun run() {
+        process(runnable)
+        interrupt()
+        try {
+            stop()
+        } catch (t: Throwable) {
+            Timber.e(t)
+        }
+    }
+
+    private fun process(r: Runnable?) {
+        try {
+            r?.run()
+        } catch (t: Throwable) {
+            onError(t)
+        }
+    }
+
+    private fun onError(t: Throwable) {
+        Timber.e(t)
+    }
+
+    fun cancel() {
+        try {
+            interrupt()
+        } catch (t: Throwable) {
+            Timber.e(t)
+        }
+    }
+
+    companion object {
+
+        @AnyThread
+        fun runInThreadOnce(r: Runnable): ThreadOnce {
+            val thread = ThreadOnce(r)
+            thread.start()
+            return thread
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        @AnyThread
+        @Throws(InterruptedException::class)
+        fun <T> runInThreadOnce(r: TRunnable<T>): T? {
+            val thread = ThreadOnce(r)
+            thread.start()
+            return (thread.runnable as TRunnable<T>).value
+        }
+
+    }
+
+}

+ 3 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/ExoPlayerImpl.kt

@@ -8,21 +8,19 @@
 
 package org.mjdev.tvlib.ui.components.media
 
+import android.graphics.drawable.ColorDrawable
 import android.net.Uri
 import android.os.Looper
 import android.view.Surface
 import android.view.SurfaceHolder
 import android.view.SurfaceView
 import android.view.TextureView
-import androidx.compose.foundation.background
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.input.key.onPreviewKeyEvent
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
@@ -63,12 +61,14 @@ class ExoPlayerImpl(
         val isEdit = isEditMode()
         val playerView = remember {
             PlayerView(context).apply {
+                background = ColorDrawable(0)
                 controllerAutoShow = true
                 controllerHideOnTouch = true
                 useController = true
                 useArtwork = true
                 artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FIT
                 player = exoPlayer
+                setShutterBackgroundColor(0)
                 setShowBuffering(PlayerView.SHOW_BUFFERING_ALWAYS)
                 setShowMultiWindowTimeBar(true)
                 setShowNextButton(false)
@@ -89,7 +89,6 @@ class ExoPlayerImpl(
             modifier = Modifier
                 .recomposeHighlighter()
                 .size(width.dp, height.dp)
-                .background(Color.Black, RectangleShape)
                 .onPreviewKeyEvent { ev ->
                     val controller = playerView.controller
                     if (controller?.isShown == true) {

+ 14 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerContainer.kt

@@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.media3.ui.PlayerControlView
 import org.mjdev.tvlib.annotations.TvPreview
+import org.mjdev.tvlib.ui.components.audiopreview.AudioPreview.Companion.AudioPreview
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.ui.components.media.MediaPlayerState.Companion.rememberMediaPlayerState
@@ -29,7 +30,8 @@ import org.mjdev.tvlib.ui.components.media.MediaPlayerState.Companion.rememberMe
 @Composable
 fun MediaPlayerContainer(
     modifier: Modifier = Modifier,
-    uri: Uri? = Uri.EMPTY,
+    uri: Uri = Uri.EMPTY,
+    isAudio: Boolean = false,
     title: String? = null,
     subtitle: String? = null,
     autoPlay: Boolean = true,
@@ -39,7 +41,7 @@ fun MediaPlayerContainer(
     val isEdit = isEditMode()
     val state = rememberMediaPlayerState(
         player = mediaPlayer,
-        uri = uri ?: Uri.EMPTY,
+        uri = uri,
         autoPlay = autoPlay,
         startSeek = startSeek,
         title = title,
@@ -50,12 +52,21 @@ fun MediaPlayerContainer(
             .recomposeHighlighter()
             .fillMaxSize()
     ) {
+        if (isAudio) {
+            AudioPreview(
+                modifier = Modifier.fillMaxSize(),
+                state = state,
+                uri = uri
+            )
+        }
         mediaPlayer.GetPlayerView()
         if (isEdit) {
             AndroidView(
                 modifier = Modifier.fillMaxSize(),
                 factory = { context ->
-                    PlayerControlView(context)
+                    PlayerControlView(context).apply {
+                        player = mediaPlayer
+                    }
                 }
             )
         }

+ 0 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/NativePlayerImpl.kt

@@ -16,13 +16,10 @@ import android.view.Surface
 import android.view.SurfaceHolder
 import android.view.SurfaceView
 import android.view.TextureView
-import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
@@ -58,7 +55,6 @@ class NativePlayerImpl(
             modifier = Modifier
                 .recomposeHighlighter()
                 .size(width.dp, height.dp)
-                .background(Color.Black, RectangleShape)
         ) {
             if (!isEdit) {
                 AndroidView(