Explorar o código

feat: 视频消息完善&视频预览切换成exoplayer

wutiaorong hai 1 ano
pai
achega
9e170e8260
Modificáronse 16 ficheiros con 464 adicións e 130 borrados
  1. 111 9
      app/src/main/java/com/adealink/weparty/exoplayer/ExoPlayerHelper.kt
  2. 1 1
      app/src/main/java/com/adealink/weparty/module/image/data/ImageData.kt
  3. 40 87
      module/image/src/main/java/com/adealink/weparty/image/preview/VideoPreviewFragment.kt
  4. 125 0
      module/image/src/main/java/com/adealink/weparty/image/view/VideoTimeBar.kt
  5. 6 0
      module/image/src/main/res/drawable/image_video_bottom_bar_bg.xml
  6. 30 0
      module/image/src/main/res/drawable/image_video_time_bar.xml
  7. 11 0
      module/image/src/main/res/drawable/image_video_time_bar_thumb.xml
  8. 19 11
      module/image/src/main/res/layout/fragment_video_preview.xml
  9. 80 0
      module/image/src/main/res/layout/layout_video_preview_controller.xml
  10. 25 0
      module/image/src/main/res/layout/layout_video_time_bar.xml
  11. 2 2
      module/message/src/main/java/com/adealink/weparty/message/conversation/extension/gallery/GalleryBoardComp.kt
  12. 0 1
      module/message/src/main/java/com/adealink/weparty/message/conversation/provider/ImageMessageItemProvider.kt
  13. 1 1
      module/message/src/main/java/com/adealink/weparty/message/conversation/provider/SightMessageItemProvider.kt
  14. 2 3
      module/message/src/main/java/com/adealink/weparty/message/conversation/viewmodel/WeConversationViewModel.kt
  15. 6 5
      module/message/src/main/res/layout/im_item_sight_message.xml
  16. 5 10
      module/userprotect/src/main/java/com/adealink/weparty/userprotect/report/UploadReportEvidenceActivity.kt

+ 111 - 9
app/src/main/java/com/adealink/weparty/exoplayer/ExoPlayerHelper.kt

@@ -9,12 +9,27 @@ import androidx.lifecycle.LifecycleOwner
 import androidx.media3.common.MediaItem
 import androidx.media3.common.PlaybackException
 import androidx.media3.common.Player
+import androidx.media3.common.Player.STATE_BUFFERING
+import androidx.media3.common.Player.STATE_READY
 import androidx.media3.common.util.UnstableApi
+import androidx.media3.database.StandaloneDatabaseProvider
+import androidx.media3.datasource.DefaultDataSource
+import androidx.media3.datasource.cache.Cache
+import androidx.media3.datasource.cache.CacheDataSource
+import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
+import androidx.media3.datasource.cache.SimpleCache
+import androidx.media3.exoplayer.DefaultRenderersFactory
 import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.exoplayer.source.MediaSource
+import androidx.media3.exoplayer.source.ProgressiveMediaSource
 import androidx.media3.ui.PlayerView
 import com.adealink.frame.log.Log
+import com.adealink.frame.util.md5
+import com.adealink.weparty.storage.file.FilePath
+import java.io.File
 import java.lang.ref.WeakReference
 
+@UnstableApi
 class ExoPlayerHelper : DefaultLifecycleObserver {
 
     private var playerViewRef: WeakReference<View?> = WeakReference(null)
@@ -27,10 +42,17 @@ class ExoPlayerHelper : DefaultLifecycleObserver {
     private var repeatMode = Player.REPEAT_MODE_OFF
     private var playWhenReady = true
     private var player: ExoPlayer? = null
+    private var cache: Cache? = null
 
     var doOnReadyCallback: (() -> Unit)? = null
     var onErrorCallback: ((error: PlaybackException) -> Unit)? = null
 
+    //Player初始化回调
+    private var onPlayerInit: ((player: Player) -> Unit)? = null
+
+    //缓冲导致的播放暂停
+    private var onPauseWhenBuffering: ((isPlaying: Boolean) -> Unit)? = null
+
     fun observe(lifecycle: Lifecycle) {
         lifecycle.addObserver(this)
     }
@@ -43,7 +65,7 @@ class ExoPlayerHelper : DefaultLifecycleObserver {
         return player
     }
 
-    class Builder(val targetView: View) {
+    class Builder(val targetView: View, val onPlayerInit: ((player: Player) -> Unit) ? = null) {
 
         var uri: String? = null
         var scaleMode: Int? = null
@@ -51,6 +73,7 @@ class ExoPlayerHelper : DefaultLifecycleObserver {
         var playWhenReady = true
         var doOnReadyCallback: (() -> Unit)? = null
         var onErrorCallback: ((error: PlaybackException) -> Unit)? = null
+        private var onPauseWhenBuffering: ((isPlaying: Boolean) -> Unit)? = null
 
         fun uri(uri: String): Builder {
             this.uri = uri
@@ -81,15 +104,23 @@ class ExoPlayerHelper : DefaultLifecycleObserver {
             return this
         }
 
+        fun onPauseWhenBuffering(listener: ((isPlaying: Boolean) -> Unit)?): Builder {
+            this.onPauseWhenBuffering = listener
+            return this
+        }
+
         fun build(): ExoPlayerHelper {
             val helper = ExoPlayerHelper().apply {
                 this.playerViewRef = WeakReference(targetView)
+                this.onPlayerInit = this@Builder.onPlayerInit
+
                 this.uri = this@Builder.uri
                 this.scaleMode = this@Builder.scaleMode
                 this.repeatMode = this@Builder.repeatMode
                 this.playWhenReady = this@Builder.playWhenReady
                 this.doOnReadyCallback = this@Builder.doOnReadyCallback
                 this.onErrorCallback = this@Builder.onErrorCallback
+                this.onPauseWhenBuffering = this@Builder.onPauseWhenBuffering
             }
             return helper
         }
@@ -99,26 +130,62 @@ class ExoPlayerHelper : DefaultLifecycleObserver {
     private fun initializePlayer() {
         Log.d(TAG, "initializePlayer")
         val playerView = (playerView as? PlayerView) ?: return
-        player = ExoPlayer.Builder(playerView.context)
+        player = ExoPlayer.Builder(playerView.context, DefaultRenderersFactory(playerView.context).setEnableDecoderFallback(true))
             .build()
             .also { player ->
                 playerView.player = player
-                uri?.let { uriString ->
-                    val mediaItem = MediaItem.fromUri(uriString)
-                    player.setMediaItem(mediaItem)
+                uri?.let {
+                    player.setMediaItem(MediaItem.fromUri(it))
                 }
                 scaleMode?.let { mode ->
                     player.videoScalingMode = mode
                 }
                 player.playWhenReady = playWhenReady
                 player.repeatMode = repeatMode
+
                 player.addListener(object : Player.Listener {
+                    override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
+                        super.onPlayWhenReadyChanged(playWhenReady, reason)
+                        Log.d(TAG, "onPlayWhenReadyChanged, playWhenReady:$playWhenReady, reason:$reason")
+                    }
+
+                    override fun onIsPlayingChanged(isPlaying: Boolean) {
+                        super.onIsPlayingChanged(isPlaying)
+                        Log.d(TAG, "onIsPlayingChanged, isPlaying:$isPlaying")
+                        when (player.playbackState) {
+                            STATE_BUFFERING -> {
+                                //缓冲中
+                                onPauseWhenBuffering?.invoke(isPlaying)
+                            }
+
+                            else -> {
+                                //缓冲完毕
+                                onPauseWhenBuffering?.invoke(true)
+                            }
+                        }
+                    }
+
+                    override fun onEvents(player: Player, events: Player.Events) {
+                        super.onEvents(player, events)
+                        Log.d(TAG, "onIsPlayingChanged, player:$player, events:$events")
+                    }
 
                     override fun onPlaybackStateChanged(playbackState: Int) {
-                        super.onPlaybackStateChanged(playbackState)
-                        if (Player.STATE_READY == playbackState) {
-                            doOnReadyCallback?.invoke()
-                            doOnReadyCallback = null
+                        Log.d(TAG, "onPlaybackStateChanged, playbackState: $playbackState")
+                        when (playbackState) {
+                            STATE_READY -> {
+                                doOnReadyCallback?.invoke()
+                                doOnReadyCallback = null
+                            }
+                            STATE_BUFFERING -> {
+                                //缓冲中
+                                onPauseWhenBuffering?.invoke(player.isPlaying)
+                            }
+
+                            else -> {
+                                //缓冲完毕
+                                onPauseWhenBuffering?.invoke(true)
+                            }
                         }
                     }
 
@@ -127,13 +194,23 @@ class ExoPlayerHelper : DefaultLifecycleObserver {
                         Log.d(TAG, "onPlayerError, error: $error")
                         onErrorCallback?.invoke(error)
                     }
+
+                    override fun onPlayerErrorChanged(error: PlaybackException?) {
+                        super.onPlayerErrorChanged(error)
+                        Log.d(TAG, "onPlayerErrorChanged, error:$error")
+                    }
+
                 })
+
                 player.prepare()
+                onPlayerInit?.invoke(player)
             }
     }
 
     private fun releasePlayer() {
         Log.d(TAG, "releasePlayer")
+        cache?.release()
+        cache = null
         player?.release()
         player = null
     }
@@ -147,6 +224,29 @@ class ExoPlayerHelper : DefaultLifecycleObserver {
         }
     }
 
+    private fun getCachePath(url: String): String {
+        return "${FilePath.videoPath}${File.separator}${"${url.md5()}.${url.split(".").last()}"}"
+    }
+
+    private fun buildMediaSource(): Pair<Cache, MediaSource>? {
+        val ctx = playerView?.context ?: return null
+        val uriString = uri ?: return null
+
+        //1. 构建缓存文件
+        val cacheFile = File(getCachePath(uriString))
+        //2. 构建缓存实例
+        val cache = SimpleCache(cacheFile, LeastRecentlyUsedCacheEvictor(MAX_CACHE_BYTE), StandaloneDatabaseProvider(ctx))
+        val mediaItem = MediaItem.fromUri(uriString)
+        //3. 构建 DataSourceFactory
+        val cacheDataSourceFactory = CacheDataSource.Factory()
+            .setCache(cache)
+            .setUpstreamDataSourceFactory(DefaultDataSource.Factory(ctx))
+            .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
+        val mediaSource = ProgressiveMediaSource.Factory(cacheDataSourceFactory)
+            .createMediaSource(mediaItem)
+        return Pair(cache, mediaSource)
+    }
+
     override fun onResume(owner: LifecycleOwner) {
         Log.d(TAG, "onResume")
         super.onResume(owner)
@@ -173,6 +273,8 @@ class ExoPlayerHelper : DefaultLifecycleObserver {
 
     companion object {
         private const val TAG = "ExoPlayerHelper"
+
+        private const val MAX_CACHE_BYTE = 50 * 1024 * 1024L
     }
 
 }

+ 1 - 1
app/src/main/java/com/adealink/weparty/module/image/data/ImageData.kt

@@ -8,4 +8,4 @@ import kotlinx.parcelize.Parcelize
  */
 
 @Parcelize
-data class VideoItem(val url: String, val width: Int, val height: Int) : Parcelable
+data class VideoItem(val url: String, val width: Int? = null, val height: Int? = null) : Parcelable

+ 40 - 87
module/image/src/main/java/com/adealink/weparty/image/preview/VideoPreviewFragment.kt

@@ -1,18 +1,22 @@
 package com.adealink.weparty.image.preview
 
+import android.os.Build
 import android.os.Bundle
-import android.view.View
-import android.view.ViewTreeObserver
-import android.webkit.URLUtil.isNetworkUrl
+import androidx.annotation.OptIn
 import androidx.fragment.app.viewModels
-import com.adealink.weparty.commonui.BaseFragment
-import com.adealink.frame.log.Log
+import androidx.media3.common.C
+import androidx.media3.common.Player.REPEAT_MODE_ONE
+import androidx.media3.common.util.UnstableApi
+import com.adealink.frame.ext.isViewBindingValid
 import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.exoplayer.ExoPlayerHelper
 import com.adealink.weparty.image.R
-import com.adealink.weparty.module.image.data.VideoItem
 import com.adealink.weparty.image.databinding.FragmentVideoPreviewBinding
 import com.adealink.weparty.image.viewmodel.VideoViewModel
-import com.qmuiteam.qmui.widget.dialog.QMUITipDialog
+import com.adealink.weparty.module.image.data.VideoItem
 
 /**
  * Created by sunxiaodong on 2022/7/15.
@@ -20,7 +24,6 @@ import com.qmuiteam.qmui.widget.dialog.QMUITipDialog
 class VideoPreviewFragment : BaseFragment(R.layout.fragment_video_preview) {
 
     companion object {
-        private const val TAG_VIDEO_PREVIEW = "tag_video_preview"
         private const val KEY_VIDEO = "video"
 
         fun newInstance(video: VideoItem): VideoPreviewFragment {
@@ -43,100 +46,50 @@ class VideoPreviewFragment : BaseFragment(R.layout.fragment_video_preview) {
 
     override fun initViews() {
         super.initViews()
-        val videoItem = videoItem ?: return
-        if (videoItem.width != 0 || videoItem.height != 0) {
-            if (binding.videoView.viewTreeObserver.isAlive) {
-                binding.videoView.viewTreeObserver.addOnPreDrawListener(object :
-                    ViewTreeObserver.OnPreDrawListener {
-
-                    override fun onPreDraw(): Boolean {
-                        if (binding.videoView.measuredWidth != 0 && binding.videoView.measuredHeight != 0) {
-                            if (binding.videoView.viewTreeObserver.isAlive) {
-                                binding.videoView.viewTreeObserver.removeOnPreDrawListener(this)
-                            }
-
-                            val lp = binding.videoView.layoutParams
-                            val videoViewRatio =
-                                binding.videoView.measuredHeight.toFloat() / binding.videoView.measuredWidth
-                            val videoRatio = videoItem.height.toFloat() / videoItem.width
-                            if (videoViewRatio > videoRatio) {
-                                //宽度撑满
-                                lp.height =
-                                    (binding.videoView.measuredWidth * videoRatio).toInt()
-                            } else {
-                                //高度撑满
-                                lp.width =
-                                    binding.videoView.measuredHeight * videoItem.width / videoItem.height
-                            }
-                            binding.videoView.layoutParams = lp
-                            playVideo()
-                        }
-                        return true
-                    }
-
-                })
-            }
-        } else {
-            playVideo()
-        }
+        playVideo()
     }
 
+    @OptIn(UnstableApi::class)
     private fun playVideo() {
-        val loadingDialog = QMUITipDialog.Builder(context)
-            .setIconType(QMUITipDialog.Builder.ICON_TYPE_LOADING)
-            .create()
-        loadingDialog.show()
-        binding.videoView.setOnPreparedListener { mp ->
-            try {
-                loadingDialog.dismiss()
-                binding.videoView.visibility = View.VISIBLE
-                mp.isLooping = false
-                startVideo()
-            } catch (e: Exception) {
-                Log.e(TAG_VIDEO_PREVIEW, "play, MediaPlayer start exception:$e")
+        ExoPlayerHelper.Builder(binding.videoView)
+            .uri(videoItem?.url ?: "")
+            .scaleMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING)
+            .repeatMode(REPEAT_MODE_ONE)
+            .playWhenReady(true)
+            .onPauseWhenBuffering {
+                onIsPlayingChangedForBuffering(it)
             }
-        }
-        binding.videoView.setOnErrorListener { _, what, extra ->
-            Log.e(TAG_VIDEO_PREVIEW, "play error:$what,$extra")
-            binding.ivPlay.visibility = View.VISIBLE
-            false
-        }
-        binding.videoView.setOnCompletionListener {
-            binding.ivPlay.visibility = View.VISIBLE
-        }
-        binding.videoView.setOnClickListener {
-            if (binding.videoView.isPlaying) {
-                pauseVideo()
-            } else {
-                startVideo()
-            }
-        }
+            .build()
+            .observe(viewLifecycleOwner.lifecycle)
+    }
 
-        val url = videoItem?.url ?: ""
-        if (isNetworkUrl(url)) {
-            videoViewModel.downloadVideo(url).observe(viewLifecycleOwner) {
-                it?.let { path ->
-                    binding.videoView.setVideoPath(path)
-                }
-            }
+    private fun onIsPlayingChangedForBuffering(isPlaying: Boolean) {
+        if (!isViewBindingValid()) {
+            return
+        }
+        if (isPlaying) {
+            binding.vVideoLoading.gone()
         } else {
-            binding.videoView.setVideoPath(url)
+            binding.vVideoLoading.show()
         }
     }
 
     override fun onPause() {
         super.onPause()
-        pauseVideo()
+        if (Build.VERSION.SDK_INT < 24) {
+            releasePlayer()
+        }
     }
 
-    private fun pauseVideo() {
-        binding.videoView.pause()
-        binding.ivPlay.visibility = View.VISIBLE
+    override fun onStop() {
+        super.onStop()
+        if (Build.VERSION.SDK_INT >= 24) {
+            releasePlayer()
+        }
     }
 
-    private fun startVideo() {
-        binding.videoView.start()
-        binding.ivPlay.visibility = View.GONE
+    private fun releasePlayer() {
+        binding.videoView.player = null
     }
 
 }

+ 125 - 0
module/image/src/main/java/com/adealink/weparty/image/view/VideoTimeBar.kt

@@ -0,0 +1,125 @@
+package com.adealink.weparty.image.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.ViewParent
+import android.widget.SeekBar
+import android.widget.SeekBar.OnSeekBarChangeListener
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.ui.TimeBar
+import com.adealink.frame.log.Log
+import com.adealink.frame.util.DisplayUtil
+import com.adealink.weparty.image.databinding.LayoutVideoTimeBarBinding
+import java.util.concurrent.CopyOnWriteArraySet
+
+@UnstableApi
+class VideoTimeBar @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null
+) : ConstraintLayout(context, attrs), TimeBar {
+
+    private val binding = LayoutVideoTimeBarBinding.inflate(LayoutInflater.from(context), this)
+
+    private val listeners: CopyOnWriteArraySet<TimeBar.OnScrubListener> = CopyOnWriteArraySet()
+    private var position: Long = 0
+    private var bufferedPosition: Long = 0
+    private var duration: Long = 0
+
+    init {
+        binding.seekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
+            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
+                if (fromUser) {
+
+                }
+            }
+
+            override fun onStartTrackingTouch(seekBar: SeekBar?) {
+//                seekBar?.progress?.let {
+//                    startScrubbing(it.toLong())
+//                }
+            }
+
+            override fun onStopTrackingTouch(seekBar: SeekBar?) {
+                seekBar?.progress?.let {
+                    stopScrubbing(it.toLong())
+                }
+            }
+        })
+    }
+
+    private fun stopScrubbing(position: Long) {
+        val parent: ViewParent? = parent
+        parent?.requestDisallowInterceptTouchEvent(false)
+        invalidate()
+        for (listener: TimeBar.OnScrubListener in listeners) {
+            listener.onScrubStop(this, position, false)
+        }
+    }
+
+    override fun addListener(listener: TimeBar.OnScrubListener) {
+        listeners.add(listener)
+    }
+
+    override fun removeListener(listener: TimeBar.OnScrubListener) {
+        listeners.remove(listener)
+    }
+
+    override fun setKeyTimeIncrement(time: Long) {
+        //Ntd.
+    }
+
+    override fun setKeyCountIncrement(count: Int) {
+        //Ntd.
+    }
+
+    override fun setPosition(position: Long) {
+        Log.d(TAG, "setPosition:$position")
+        if (this.position == position) {
+            return
+        }
+        this.position = position
+        update()
+    }
+
+    override fun setBufferedPosition(bufferedPosition: Long) {
+        Log.d(TAG, "setBufferedPosition:$bufferedPosition")
+        if (this.bufferedPosition == bufferedPosition) {
+            return
+        }
+        this.bufferedPosition = bufferedPosition
+        update()
+    }
+
+    override fun setDuration(duration: Long) {
+        Log.d(TAG, "setDuration:$duration")
+        if (this.duration == duration) {
+            return
+        }
+        this.duration = duration
+        update()
+    }
+
+    override fun getPreferredUpdateDelay(): Long {
+        val timeBarWidthDp: Int = DisplayUtil.px2dp(binding.seekBar.width.toFloat())
+        return if ((timeBarWidthDp == 0) || (duration == 0L) || (duration == androidx.media3.common.C.TIME_UNSET)) {
+            Long.Companion.MAX_VALUE
+        } else {
+            duration / timeBarWidthDp
+        }
+    }
+
+    override fun setAdGroupTimesMs(adGroupTimesMs: LongArray?, playedAdGroups: BooleanArray?, adGroupCount: Int) {
+        //Ntd.
+    }
+
+    private fun update() {
+        binding.seekBar.max = duration.toInt()
+        binding.seekBar.progress = position.toInt()
+        binding.seekBar.secondaryProgress = bufferedPosition.toInt()
+    }
+
+    companion object {
+        const val TAG = "JackarooVideoTimeBar"
+    }
+}

+ 6 - 0
module/image/src/main/res/drawable/image_video_bottom_bar_bg.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="16dp" />
+    <solid android:color="@color/color_26000000" />
+</shape>

+ 30 - 0
module/image/src/main/res/drawable/image_video_time_bar.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <!--定义滑动条的底色-->
+    <item android:id="@android:id/background">
+        <shape>
+            <corners android:radius="2dp" />
+            <solid android:color="@color/color_80FFFFFF" />
+        </shape>
+    </item>
+
+    <!-- 定义缓存颜色 -->
+    <item android:id="@android:id/secondaryProgress">
+        <clip>
+            <shape>
+                <corners android:radius="2dp" />
+                <solid android:color="@color/color_CCFFFFFF" />
+            </shape>
+        </clip>
+    </item>
+
+    <!--定义seekbar滑动条进度颜色-->
+    <item android:id="@android:id/progress">
+        <clip>
+            <shape>
+                <corners android:radius="2dp" />
+                <solid android:color="@color/color_FFA576FF" />
+            </shape>
+        </clip>
+    </item>
+</layer-list>

+ 11 - 0
module/image/src/main/res/drawable/image_video_time_bar_thumb.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <size
+        android:width="9dp"
+        android:height="9dp" />
+    <solid android:color="@color/white" />
+    <stroke
+        android:width="1dp"
+        android:color="@color/white" />
+</shape>

+ 19 - 11
module/image/src/main/res/layout/fragment_video_preview.xml

@@ -4,24 +4,32 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <com.adealink.frame.effect.video.WPVideoView
+    <androidx.media3.ui.PlayerView
         android:id="@+id/video_view"
         android:layout_width="0dp"
         android:layout_height="0dp"
+        app:controller_layout_id="@layout/layout_video_preview_controller"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
+        app:layout_constraintTop_toTopOf="parent"
+        app:resize_mode="fit"
+        app:show_buffering="when_playing"
+        app:surface_type="surface_view"
+        app:use_controller="true" />
 
-    <androidx.appcompat.widget.AppCompatImageView
-        android:id="@+id/iv_play"
-        android:layout_width="48dp"
-        android:layout_height="48dp"
-        android:src="@drawable/common_video_play_ic"
+    <com.qmuiteam.qmui.widget.QMUILoadingView
+        android:id="@+id/v_video_loading"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="12dp"
+        android:color="@color/color_CCFFFFFF"
         android:visibility="gone"
-        app:layout_constraintBottom_toBottomOf="@+id/video_view"
-        app:layout_constraintEnd_toEndOf="@+id/video_view"
-        app:layout_constraintStart_toStartOf="@+id/video_view"
-        app:layout_constraintTop_toTopOf="@+id/video_view" />
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:qmui_loading_view_size="36dp" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 80 - 0
module/image/src/main/res/layout/layout_video_preview_controller.xml

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:parentTag="android.widget.FrameLayout"
+    tools:viewBindingIgnore="true">
+
+    <!-- ExoPlayer自定义操作控件, StyledPlayerControlView, exo_styled_player_control_view.xml -->
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginBottom="60dp">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@id/exo_bottom_bar"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginHorizontal="8dp"
+            android:layout_marginBottom="6dp"
+            android:background="@drawable/image_video_bottom_bar_bg"
+            android:paddingHorizontal="8dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent">
+
+            <androidx.appcompat.widget.AppCompatImageView
+                android:id="@id/exo_play_pause"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@id/exo_position"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="4dp"
+                android:includeFontPadding="false"
+                android:textColor="@color/color_FFFFFF"
+                android:textSize="12sp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toEndOf="@id/exo_play_pause"
+                app:layout_constraintTop_toTopOf="parent"
+                tools:minWidth="50dp" />
+
+            <com.adealink.weparty.image.view.VideoTimeBar
+                android:id="@id/exo_progress"
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:layout_marginHorizontal="4dp"
+                app:bar_height="8dp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toStartOf="@id/exo_duration"
+                app:layout_constraintStart_toEndOf="@id/exo_position"
+                app:layout_constraintTop_toTopOf="parent"
+                app:played_color="@color/color_FFA576FF"
+                app:scrubber_drawable="@drawable/image_video_time_bar_thumb"
+                app:unplayed_color="@color/color_80FFFFFF" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@id/exo_duration"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:includeFontPadding="false"
+                android:textColor="@color/color_FFFFFF"
+                android:textSize="12sp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                tools:minWidth="50dp" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</merge>

+ 25 - 0
module/image/src/main/res/layout/layout_video_time_bar.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
+
+    <androidx.appcompat.widget.AppCompatSeekBar
+        android:id="@+id/seek_bar"
+        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="10dp"
+        android:clipChildren="false"
+        android:max="100"
+        android:paddingStart="4.5dp"
+        android:paddingEnd="4.5dp"
+        android:maxHeight="4dp"
+        tools:progress="20"
+        android:progressDrawable="@drawable/image_video_time_bar"
+        android:thumb="@drawable/image_video_time_bar_thumb"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</merge>

+ 2 - 2
module/message/src/main/java/com/adealink/weparty/message/conversation/extension/gallery/GalleryBoardComp.kt

@@ -126,7 +126,7 @@ class GalleryBoardComp(
             onTakePhoto()
         }
         binding.gallerySelectBtn.setOnClickListener {
-            selectImageObserver.launch(SOURCE_SELECT_PICTURE, null, maxNum = MAX_SELECT_NUM, gifOpt = true, selectedMediaList = checkedMedias)
+            selectImageObserver.launch(SOURCE_SELECT_PICTURE, mediaType = MediaType.ALL, maxNum = MAX_SELECT_NUM, gifOpt = true, selectedMediaList = checkedMedias)
         }
         binding.confirmBtn.isEnabled = checkedMedias.size > 0
         binding.confirmBtn.setOnClickListener {
@@ -155,7 +155,7 @@ class GalleryBoardComp(
     }
 
     private fun loadData() {
-        mediaSelectViewModel.loadMedia(LoadMediaConfig(MediaType.IMAGE, filterGift = false))
+        mediaSelectViewModel.loadMedia(LoadMediaConfig(MediaType.ALL, filterGift = false))
     }
 
     @SuppressLint("CheckResult")

+ 0 - 1
module/message/src/main/java/com/adealink/weparty/message/conversation/provider/ImageMessageItemProvider.kt

@@ -16,7 +16,6 @@ import com.adealink.weparty.message.R
 import com.adealink.weparty.message.data.UiMessage
 import com.facebook.drawee.backends.pipeline.Fresco
 import com.facebook.drawee.controller.BaseControllerListener
-import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
 import com.facebook.imagepipeline.image.ImageInfo
 import io.rong.imlib.model.Message
 import io.rong.imlib.model.MessageContent

+ 1 - 1
module/message/src/main/java/com/adealink/weparty/message/conversation/provider/SightMessageItemProvider.kt

@@ -26,7 +26,7 @@ class SightMessageItemProvider : BaseMessageItemProvider<SightMessage>() {
 
     init {
         config.showReadState = true
-        config.showContentBubble = false
+        config.showContentBubble = true
         config.showProgress = false
     }
 

+ 2 - 3
module/message/src/main/java/com/adealink/weparty/message/conversation/viewmodel/WeConversationViewModel.kt

@@ -210,16 +210,15 @@ class WeConversationViewModel : ConversationViewModel(), CoroutineScope {
     }
 
     override fun onVideoPreviewClick(uiMessage: UiMessage) {
-        //TODO wtr 视频宽高获取
         val act = AppUtil.currentActivity ?: return
         val sightContent = uiMessage.content as? SightMessage ?: return
         val localPath = sightContent.localPath
         if (localPath != null) {
-            goVideoPreviewActivity(act, arrayListOf(VideoItem(localPath.toString(), 0, 0)))
+            goVideoPreviewActivity(act, arrayListOf(VideoItem(localPath.toString())))
             return
         }
         val remoteUrl = sightContent.mediaUrl?.toString() ?: return
-        goVideoPreviewActivity(act, arrayListOf(VideoItem(remoteUrl, 0, 0)))
+        goVideoPreviewActivity(act, arrayListOf(VideoItem(remoteUrl)))
     }
 
 }

+ 6 - 5
module/message/src/main/res/layout/im_item_sight_message.xml

@@ -4,7 +4,8 @@
     android:layout_height="wrap_content"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:clipChildren="false"
-    android:orientation="horizontal">
+    android:orientation="horizontal"
+    android:layout_margin="6dp">
 
     <RelativeLayout
         android:id="@+id/im_sight_operation"
@@ -30,8 +31,8 @@
 
         <com.adealink.frame.image.view.NetworkImageView
             android:id="@+id/im_sight_thumb"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
+            android:layout_width="120dp"
+            android:layout_height="120dp"
             android:scaleType="centerCrop"
             app:actualImageScaleType="centerCrop"
             app:roundedCornerRadius="10dp" />
@@ -63,9 +64,9 @@
             style="@style/IMTextStyle.Alignment"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_gravity="bottom|end"
+            android:layout_gravity="bottom|start"
             android:layout_marginBottom="4dp"
-            android:layout_marginEnd="2dp"
+            android:layout_marginStart="2dp"
             android:textColor="@android:color/white"
             android:textSize="12sp" />
     </com.adealink.frame.imkit.widget.IMMessageFrameLayout>

+ 5 - 10
module/userprotect/src/main/java/com/adealink/weparty/userprotect/report/UploadReportEvidenceActivity.kt

@@ -8,7 +8,6 @@ import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.BindExtra
 import com.adealink.frame.router.annotation.RouterUri
-import com.adealink.frame.util.ImageUtil
 import com.adealink.frame.util.getFileSizeKB
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.commonui.toast.util.showToast
@@ -30,7 +29,6 @@ import com.adealink.weparty.util.goVideoPreviewActivity
 import com.qmuiteam.qmui.widget.util.QMUIStatusBarHelper
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
 import java.io.File
 import com.adealink.weparty.R as APP_R
 
@@ -125,14 +123,11 @@ class UploadReportEvidenceActivity : BaseActivity() {
             override fun onPreview(uriList: ArrayList<String>, pathList: ArrayList<String>, currentIndex: Int) {
                 CoroutineScope(Dispatcher.UI).launch {
                     val videoList = arrayListOf<VideoItem>()
-                    withContext(Dispatcher.WENEXT_THREAD_POOL) {
-                        pathList
-                            .map {
-                                val size = ImageUtil.getVideoSize(it)
-                                VideoItem(it, size.first ?: 0, size.second ?: 0)
-                            }
-                            .toCollection(videoList)
-                    }
+                    pathList
+                        .map {
+                            VideoItem(it)
+                        }
+                        .toCollection(videoList)
                     goVideoPreviewActivity(
                         this@UploadReportEvidenceActivity,
                         videoList,