Parcourir la source

1、保存图传数据、遥控器硬件拍照、切换均已完成

不会爬树的猴 il y a 1 an
Parent
commit
ae5915b618

+ 12 - 0
.idea/deploymentTargetDropDown.xml

@@ -1,6 +1,18 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="deploymentTargetDropDown">
+    <runningDeviceTargetSelectedWithDropDown>
+      <Target>
+        <type value="RUNNING_DEVICE_TARGET" />
+        <deviceKey>
+          <Key>
+            <type value="SERIAL_NUMBER" />
+            <value value="10.88.88.112:5555" />
+          </Key>
+        </deviceKey>
+      </Target>
+    </runningDeviceTargetSelectedWithDropDown>
+    <timeTargetWasSelectedWithDropDown value="2023-08-05T00:47:36.314218Z" />
     <runningDeviceTargetsSelectedWithDialog>
       <Target>
         <type value="RUNNING_DEVICE_TARGET" />

+ 50 - 10
app/src/main/java/com/cr/common/CrAudioUtil.kt

@@ -1,7 +1,9 @@
 package com.cr.common
 
 import android.content.Context
+import android.media.AudioManager
 import android.media.MediaPlayer
+import com.cr.data.CrUtil
 
 /**
  * 操作系统:MAC系统
@@ -13,26 +15,64 @@ class CrAudioUtil {
     /**
      * 静态方法
      */
-    companion object{
-        private var MIN_RATIO:Float = 0.3f
+    companion object {
+        private var MIN_RATIO: Float = 0.3f
+
         // define: 2023/8/4 播放器
-        private var player:MediaPlayer?=null
-        private fun playSound(context: Context,resID:Int,ignoreWhenBusy:Boolean){
-            try{
-                if(player != null){
+        private var player: MediaPlayer? = null
+
+        /**
+         * 播放音频
+         * @param context Context 上下文
+         * @param resID Int 音频资源
+         */
+        fun playSound(context: Context, resID: Int){
+            playSound(context,resID,false)
+        }
+
+        /**
+         * 播放声音
+         * @param context Context 上下文
+         * @param resID Int 音频资源Id
+         * @param ignoreWhenBusy Boolean
+         */
+        private fun playSound(context: Context, resID: Int, ignoreWhenBusy: Boolean) {
+            try {
+                // todo: 2023/8/5 如果不为null 则至null
+                if (player != null) {
                     player?.let {
                         // todo: 2023/8/4 如果正在播放则停止
-                        if(it.isPlaying){
-                            if(ignoreWhenBusy){
+                        if (it.isPlaying) {
+                            if (ignoreWhenBusy) {
                                 return
                             }
                             it.stop()
                         }
                         it.release()
+                        player = null
                     }
                 }
-            }catch (e:java.lang.Exception){
-
+                // todo: 2023/8/5 创建播放器
+                player = MediaPlayer.create(context, resID)
+                player?.setOnCompletionListener { mp ->
+                    {
+                        player?.let {
+                            it.release()
+                            player = null
+                        }
+                    }
+                }
+                var am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+                var audioMaxVolume = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
+                var audioCurrentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC)
+                var volumeRatio = (audioCurrentVolume/audioMaxVolume)*1f
+                if(volumeRatio < MIN_RATIO){
+                    volumeRatio = MIN_RATIO
+                }
+                player?.setVolume(volumeRatio,volumeRatio)
+                player?.start()
+            } catch (e: java.lang.Exception) {
+                CrUtil.print("播放错误:${e.message}")
             }
         }
     }

+ 118 - 0
app/src/main/java/com/cr/common/CrKeyManager.kt

@@ -0,0 +1,118 @@
+package com.cr.common
+
+import dji.sdk.keyvalue.key.DJIActionKeyInfo
+import dji.sdk.keyvalue.key.DJIKey
+import dji.sdk.keyvalue.key.DJIKey.ActionKey
+import dji.sdk.keyvalue.key.DJIKeyInfo
+import dji.sdk.keyvalue.key.KeyTools
+import dji.sdk.keyvalue.value.common.EmptyMsg
+import dji.v5.common.callback.CommonCallbacks
+import dji.v5.common.error.IDJIError
+import dji.v5.manager.KeyManager
+
+/**
+ * 操作系统:MAC系统
+ * 创建者:王成
+ * 创建日期:2023/8/5 08:51
+ * 描述:大疆动作或参数执行管理
+ */
+class CrKeyManager {
+    /**
+     * 完成回调
+     */
+    interface ICompletion<T> {
+        /**
+         * 成功回调
+         */
+        fun onSuccess(t:T)
+
+        /**
+         * 错误回调
+         * @param error String 错误消息
+         */
+        fun onFailure(error: String)
+    }
+
+    /**
+     * 静态方法
+     */
+    companion object {
+        /**
+         * 执行不带参的动作
+         * @param keyInfo DJIActionKeyInfo<P, R>
+         * @param callback ICompletion?
+         */
+        fun <P,R> performAction(keyInfo:DJIActionKeyInfo<P,R>, callback: ICompletion<R>?) {
+            var key:ActionKey<P,R> = KeyTools.createKey(keyInfo)
+            KeyManager.getInstance().performAction(key,
+                object : CommonCallbacks.CompletionCallbackWithParam<R> {
+                    // todo: 2023/8/5 执行成功
+                    override fun onSuccess(t: R) {
+                        callback?.let {
+                            it.onSuccess(t)
+                        }
+                    }
+
+                    // todo: 2023/8/5 执行错误
+                    override fun onFailure(error: IDJIError) {
+                        callback?.let {
+                            it.onFailure(error.description())
+                        }
+                    }
+                })
+        }
+
+        /**
+         * 设置参数
+         * @param keyInfo DJIKeyInfo<P> DJI KEY
+         * @param param P 参数
+         * @param callback ICompletion<String>? 回调
+         */
+        fun <P> setValue(keyInfo:DJIKeyInfo<P>,param:P,callback:ICompletion<String>?){
+            // todo: 2023/8/5 创建Key
+            var key = KeyTools.createKey(keyInfo)
+            // todo: 2023/8/5 设置参数
+            KeyManager.getInstance().setValue(key,param,object:CommonCallbacks.CompletionCallback{
+                // todo: 2023/8/5 设置成功
+                override fun onSuccess() {
+                    callback?.let {
+                        it.onSuccess("成功")
+                    }
+                }
+
+                // todo: 2023/8/5 设置失败
+                override fun onFailure(error: IDJIError) {
+                    callback?.let {
+                        it.onFailure(error.description())
+                    }
+                }
+
+            })
+        }
+
+        /**
+         * 异步从硬件获取参数
+         * @param keyInfo DJIKeyInfo<P> DJI KEY
+         * @param callback ICompletion<P>? 回调
+         */
+        fun <P> getValue(keyInfo:DJIKeyInfo<P>,callback:ICompletion<P>?){
+            var key = KeyTools.createKey(keyInfo)
+            KeyManager.getInstance().getValue(key,object:CommonCallbacks.CompletionCallbackWithParam<P>{
+                // todo: 2023/8/5 获取成功
+                override fun onSuccess(t: P) {
+                    callback?.let {
+                        it.onSuccess(t)
+                    }
+                }
+
+                // todo: 2023/8/5 获取失败
+                override fun onFailure(error: IDJIError) {
+                    callback?.let {
+                        it.onFailure(error.description())
+                    }
+                }
+
+            })
+        }
+    }
+}

+ 4 - 6
app/src/main/java/com/cr/cruav/AvLogin.kt

@@ -2,11 +2,9 @@ package com.cr.cruav
 
 import android.Manifest
 import android.app.Activity
-import android.content.Context
 import android.content.pm.PackageManager
 import android.os.Build
 import android.os.Bundle
-import android.util.DisplayMetrics
 import android.util.Log
 import android.view.View
 import android.view.View.OnClickListener
@@ -242,25 +240,25 @@ class AvLogin : CrActivity(), OnClickListener {
         // todo: 2023/3/31 检查并创建工程文件夹
         if (!CrFileManager.isExists(CrUtil.PROJECT_PATH)) {
             if (!CrFileManager.createFolder(CrUtil.PROJECT_PATH)) {
-                CrUtil.showMessage("工程文件夹创建失败,请检查权限!")
+                CrUtil.showToast("工程文件夹创建失败,请检查权限!")
             }
         }
         // todo: 2023/3/31 检查并创建资源文件夹
         if (!CrFileManager.isExists(CrUtil.IMAGE_PATH)) {
             if (!CrFileManager.createFolder(CrUtil.IMAGE_PATH)) {
-                CrUtil.showMessage("资源文件夹创建失败,请检查权限!")
+                CrUtil.showToast("资源文件夹创建失败,请检查权限!")
             }
         }
         // todo: 2023/3/31 检查并创建缓存文件夹
         if (!CrFileManager.isExists(CrUtil.PROJECT_CACHE_PATH)) {
             if (!CrFileManager.createFolder(CrUtil.PROJECT_CACHE_PATH)) {
-                CrUtil.showMessage("缓存文件夹创建失败,请检查权限!")
+                CrUtil.showToast("缓存文件夹创建失败,请检查权限!")
             }
         }
         // todo: 2023/3/31 检查并创建配置文件夹
         if (!CrFileManager.isExists(CrUtil.CONFIG_PATH)) {
             if (!CrFileManager.createFolder(CrUtil.CONFIG_PATH)) {
-                CrUtil.showMessage("配置文件夹创建失败,请检查权限!")
+                CrUtil.showToast("配置文件夹创建失败,请检查权限!")
             }
         }
         // todo: 2023/4/3 拷贝配置库

+ 17 - 0
app/src/main/java/com/cr/cruav/AvMain.kt

@@ -8,6 +8,8 @@ import androidx.activity.viewModels
 import androidx.fragment.app.commit
 import com.cr.data.CrConfig
 import com.cr.data.CrUtil
+import com.cr.event.CrCommonAction
+import com.cr.event.EventCommon
 import com.cr.event.EventFragmentBarAction
 import com.cr.map.CaseModel
 import com.cr.map.EventMap
@@ -502,4 +504,19 @@ class AvMain : CrActivity(), View.OnClickListener {
     fun onFragmentBar(event: EventFragmentBarAction) {
         hideFragment(event.fragment!!)
     }
+
+    /**
+     * 订阅视频YUV存储事件
+     * @param event EventCommon<String>
+     */
+    @Subscribe
+    fun onVideoYUVSave(event:EventCommon<String>){
+        event?.let {
+            if(it.action == CrCommonAction.VIDEO_SAVE_YUV_FAILURE){
+                CrApplication.getEventBus().post(EventCommon<String>(CrCommonAction.SHOW_MESSAGE,it.param))
+            }else if(it.action == CrCommonAction.VIDEO_SAVE_YUV_SUCCESS){
+                CrApplication.getEventBus().post(EventCommon<String>(CrCommonAction.SHOW_MESSAGE,it.param))
+            }
+        }
+    }
 }

+ 3 - 5
app/src/main/java/com/cr/data/utils.kt

@@ -18,8 +18,6 @@ import com.cr.dialog.DialogNormal
 import dji.sdk.keyvalue.value.camera.CameraExposureCompensation
 import dji.sdk.keyvalue.value.camera.CameraISO
 import dji.sdk.keyvalue.value.camera.CameraShutterSpeed
-import dji.v5.utils.common.ContextUtil
-import dji.v5.utils.common.StringUtils
 import java.text.SimpleDateFormat
 import java.util.*
 import java.util.regex.Pattern
@@ -35,7 +33,7 @@ const val MAIN_FRAGMENT_PAGE_TITLE = "MAIN_FRAGMENT_PAGE_TITLE"
 const val MEDIA_FILE_DETAILS_STR = "MEDIA_FILE_DETAILS"
 const val REGISTER_SUCCESS = "Cr"
 const val REGISTER_FAILED = "Err"
-const val TAG: String = "CrUAV"
+const val TAG: String = "CrUAVMessage"
 const val RES_ERR_FLAG = "RES_ERR"
 const val RES_NULL_FLAG = "RES_NO"
 const val RES_SUCCESS_FLAG = "RES_OK"
@@ -595,13 +593,13 @@ class CrUtil {
          * @param message String 消息内容
          */
         @JvmStatic
-        fun showMessage(message: String) {
+        fun showToast(message: String) {
             var mainHandler = Handler(Looper.getMainLooper())
             mainHandler.post(Runnable {
                 if (toast != null) {
                     toast!!.cancel()
                 }
-                toast = Toast.makeText(ContextUtil.getContext(), message, Toast.LENGTH_LONG)
+                toast = Toast.makeText(CrApplication.getContext(), message, Toast.LENGTH_LONG)
                 toast!!.duration = Toast.LENGTH_LONG
                 toast!!.setText(message)
                 toast!!.show()

+ 2 - 6
app/src/main/java/com/cr/dialog/DialogInput.kt

@@ -2,19 +2,15 @@ package com.cr.dialog
 
 import android.app.Dialog
 import android.content.Context
-import android.opengl.Visibility
 import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup
 import android.widget.Button
 import android.widget.LinearLayout
 import android.widget.TextView
-import androidx.core.view.get
-import com.cr.common.CrUnitManager
 import com.cr.cruav.R
 import com.cr.data.CrUtil
 import com.cr.widget.CrEditTextWidget
-import org.w3c.dom.Text
 
 /**
  * 操作系统:MAC系统
@@ -221,11 +217,11 @@ class DialogInput : Dialog, View.OnClickListener {
         when (p0?.id) {
             R.id.dig_btn_ok -> {
                 if(panelOne?.visibility == View.VISIBLE && (txtOne!!.getContent() == null || txtOne!!.getContent() == "")){
-                    CrUtil.showMessage("请输入内容!")
+                    CrUtil.showToast("请输入内容!")
                     return
                 }
                 if(panelTwo?.visibility == View.VISIBLE && (txtTwo!!.getContent() == null || txtTwo!!.getContent() == "")){
-                    CrUtil.showMessage("请输入内容!")
+                    CrUtil.showToast("请输入内容!")
                     return
                 }
                 listener?.completion(txtOne!!.getContent(),txtTwo!!.getContent(),self!!)

+ 29 - 0
app/src/main/java/com/cr/event/CrCommonAction.kt

@@ -0,0 +1,29 @@
+package com.cr.event
+
+/**
+ * 操作系统:MAC系统
+ * 创建者:王成
+ * 创建日期:2023/8/7 14:09
+ * 描述:大疆动作
+ */
+enum class CrCommonAction {
+    /**
+     * 保存当前帧的YUV数据
+     */
+    VIDEO_SAVE_YUV,
+
+    /**
+     * YUV数据保存成功
+     */
+    VIDEO_SAVE_YUV_SUCCESS,
+
+    /**
+     * YUV数据保存失败
+     */
+    VIDEO_SAVE_YUV_FAILURE,
+
+    /**
+     * 显示消息
+     */
+    SHOW_MESSAGE,
+}

+ 23 - 0
app/src/main/java/com/cr/event/EventCommon.kt

@@ -0,0 +1,23 @@
+package com.cr.event
+
+/**
+ * 操作系统:MAC系统
+ * 创建者:王成
+ * 创建日期:2023/8/7 14:08
+ * 描述:
+ */
+class EventCommon<P> @JvmOverloads constructor(
+    action: CrCommonAction,
+    param: P? = null
+) {
+    var action: CrCommonAction? = null  // define: 2023/8/7 大疆动作
+    var param: P? = null  // define: 2023/8/7 携带参数
+
+    /**
+     * 初始化
+     */
+    init {
+        this.action = action
+        this.param = param
+    }
+}

+ 23 - 0
app/src/main/java/com/cr/pages/FragmentDynamicInfo.kt

@@ -8,14 +8,19 @@ import android.widget.TextView
 import androidx.fragment.app.activityViewModels
 import androidx.transition.Visibility
 import com.cr.common.CrCameraInfo
+import com.cr.cruav.CrApplication
 import com.cr.cruav.R
 import com.cr.data.CrUtil
+import com.cr.event.CrCommonAction
+import com.cr.event.EventCommon
 import com.cr.viewmodel.CrCameraVM
+import com.squareup.otto.Subscribe
 import dji.sdk.keyvalue.key.CameraKey
 import dji.sdk.keyvalue.key.KeyTools
 import dji.sdk.keyvalue.value.common.EmptyMsg
 import dji.v5.common.callback.CommonCallbacks
 import dji.v5.common.error.IDJIError
+import org.w3c.dom.Text
 
 /**
  * 操作系统:MAC系统
@@ -39,6 +44,8 @@ class FragmentDynamicInfo : CrFragment(), View.OnClickListener {
     private var flagPhoto: TextView? = null  // define: 2023/8/3 照片模式标志
     private var flagVideo: TextView? = null // define: 2023/8/3 视频模式标志
 
+    private var lblMessage:TextView?=null // define: 2023/8/7 消息
+
     /**
      * 初始化
      * @param inflater LayoutInflater
@@ -55,6 +62,8 @@ class FragmentDynamicInfo : CrFragment(), View.OnClickListener {
         mainView = inflater.inflate(R.layout.frag_dynamic_info, null)
         // todo: 2023/8/2 挂载控件
         joinControls()
+        // todo: 2023/8/7 注册事件
+        CrApplication.getEventBus().register(this)
         return mainView
     }
 
@@ -81,6 +90,9 @@ class FragmentDynamicInfo : CrFragment(), View.OnClickListener {
             flagVideo = it.findViewById(R.id.camera_model_video)
             flagVideo?.visibility = View.GONE
 
+            // todo: 2023/8/7 挂载消息控件
+            lblMessage = it.findViewById(R.id.lbl_message)
+
             // todo: 2023/8/2 挂载事件
             lblSDPhotoCount?.setOnClickListener(this)
         }
@@ -101,6 +113,8 @@ class FragmentDynamicInfo : CrFragment(), View.OnClickListener {
      * 销毁
      */
     override fun onDestroy() {
+        // todo: 2023/8/7 取消事件监听
+        CrApplication.getEventBus().unregister(this)
         super.onDestroy()
     }
 
@@ -162,4 +176,13 @@ class FragmentDynamicInfo : CrFragment(), View.OnClickListener {
             }
         }
     }
+
+    @Subscribe
+    fun onMessage(event:EventCommon<String>){
+        event?.let {
+            if(it.action == CrCommonAction.SHOW_MESSAGE){
+                lblMessage?.text = event.param
+            }
+        }
+    }
 }

+ 271 - 24
app/src/main/java/com/cr/pages/FragmentFPV.kt

@@ -1,5 +1,10 @@
 package com.cr.pages
 
+import android.graphics.ImageFormat
+import android.graphics.Rect
+import android.graphics.YuvImage
+import android.media.MediaCodecInfo
+import android.media.MediaFormat
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.SurfaceHolder
@@ -9,15 +14,28 @@ import android.view.ViewGroup
 import android.widget.Button
 import android.widget.TextView
 import androidx.fragment.app.activityViewModels
+import com.cr.common.CrUnitManager
+import com.cr.cruav.CrApplication
 import com.cr.cruav.R
+import com.cr.data.CrUtil
+import com.cr.event.CrCommonAction
+import com.cr.event.EventCommon
 import com.cr.viewmodel.CrFlightControlVM
 import com.cr.viewmodel.CrVideoChannelListener
 import com.cr.viewmodel.CrVideoChannelVM
+import com.squareup.otto.Subscribe
 import dji.v5.common.video.channel.VideoChannelType
 import dji.v5.common.video.decoder.DecoderOutputMode
 import dji.v5.common.video.decoder.DecoderState
 import dji.v5.common.video.decoder.VideoDecoder
 import dji.v5.common.video.interfaces.*
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.OutputStream
 
 
 /**
@@ -30,10 +48,11 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
     /**
      * FPV窗口监听
      */
-    interface FPVListener{
+    interface FPVListener {
         // todo: 2023/3/13 切换窗口
         fun triggerWindow()
     }
+
     // define: 2023/3/10 延迟初始化一个通道模型
     private val videoChannelVM: CrVideoChannelVM by activityViewModels()
 
@@ -43,7 +62,7 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
     // define: 2023/3/10 延迟初始化一个视频画布
     private lateinit var surfaceView: SurfaceView
     private var btnTrigger: Button? = null
-    private var lblInfo:TextView?=null // define: 2023/8/4 显示信息
+    private var lblInfo: TextView? = null // define: 2023/8/4 显示信息
 
     // define: 2023/3/11 视频通道
     private var curVideoChannel: IVideoChannel? = null
@@ -52,14 +71,14 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
     private var videoDecoder: IVideoDecoder? = null
 
     // todo: 2023/8/4 视频相关参数
-    private var videoWidth:Int = -1
-    private var videoHeight:Int = -1
-    private var widthChanged:Boolean = false
-    private var heightChange:Boolean = false
-    private var fps:Int = -1
+    private var videoWidth: Int = -1
+    private var videoHeight: Int = -1
+    private var widthChanged: Boolean = false
+    private var heightChange: Boolean = false
+    private var fps: Int = -1
 
     // todo: 2023/3/13 监听
-    lateinit var listener:FPVListener
+    lateinit var listener: FPVListener
 
     /**
      * 创建视图
@@ -75,7 +94,7 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
     ): View? {
         var view = inflater.inflate(R.layout.frag_fpv, container, false)
         // todo: 2023/8/4 关闭 View Layer。 View Layer 可以加速无 invalidate() 时的刷新效率,但对于需要调用 invalidate() 的刷新无法加速
-        view.setLayerType(View.LAYER_TYPE_NONE,null)
+        view.setLayerType(View.LAYER_TYPE_NONE, null)
         // todo: 2023/3/13 给View挂接事件
         view.setOnClickListener(clickListener)
         // todo: 2023/3/10 挂接图传视图控件
@@ -86,6 +105,8 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
         lblInfo = view.findViewById(R.id.fpv_info)
         // todo: 2023/3/10 设置视图控件监听
         surfaceView.holder.addCallback(this)
+        // todo: 2023/8/7 注册事件监听
+        CrApplication.getEventBus().register(this)
         return view
     }
 
@@ -104,12 +125,12 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
      * 点击事件
      */
     private var clickListener = View.OnClickListener {
-        when(it.id){
+        when (it.id) {
             R.id.fpv_trigger -> {
                 // todo: 2023/3/13 切换视频通道
                 videoChannelVM.triggerStreamSource()
             }
-            R.id.fpv_layout ->{
+            R.id.fpv_layout -> {
                 // todo: 2023/3/13 切换窗口
                 listener?.triggerWindow()
             }
@@ -121,9 +142,10 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
      */
     private fun init() {
         // todo: 2023/8/4 注册订阅
-        videoChannelVM.videoChannelInfo.observe(viewLifecycleOwner){
+        videoChannelVM.videoChannelInfo.observe(viewLifecycleOwner) {
             it?.let {
-                val videoStreamInfo = "fps:[${it.fps}] width:[${videoWidth}] height:[${videoHeight}]"
+                val videoStreamInfo =
+                    "fps:[${it.fps}] width:[${videoWidth}] height:[${videoHeight}]"
                 lblInfo?.text = videoStreamInfo
             }
         }
@@ -158,7 +180,7 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
      * 变更通道类型
      * @param channelType VideoChannelType
      */
-    private fun changeVideoDecoder(channelType:VideoChannelType){
+    private fun changeVideoDecoder(channelType: VideoChannelType) {
         videoDecoder?.videoChannelType = channelType
         surfaceView.invalidate()
     }
@@ -201,26 +223,27 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
      */
     private val streamDataListener = StreamDataListener {
         it?.let {
-            if(fps != it.fps){
+            if (fps != it.fps) {
                 fps = it.fps
-                mainHandler.post{
+                mainHandler.post {
                     videoChannelVM.videoChannelInfo.value?.fps = fps
                     videoChannelVM.refreshVideoChannelInfo()
                 }
             }
-            if(videoWidth != it.width){
+            if (videoWidth != it.width) {
                 videoWidth = it.width
                 widthChanged = true
             }
-            if(videoHeight != it.height){
+            if (videoHeight != it.height) {
                 videoHeight = it.height
                 heightChange = true
             }
-            if(widthChanged || heightChange){
+            if (widthChanged || heightChange) {
                 widthChanged = false
                 heightChange = false
-                mainHandler.post{
-                    videoChannelVM.videoChannelInfo.value?.resolution = "${videoWidth}*${videoHeight}"
+                mainHandler.post {
+                    videoChannelVM.videoChannelInfo.value?.resolution =
+                        "${videoWidth}*${videoHeight}"
                     videoChannelVM.refreshVideoChannelInfo()
                 }
             }
@@ -256,15 +279,239 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
     }
 
     /**
-     * 覆写视图释放
+     * 释放资源
      */
-    override fun onDestroyView() {
-        super.onDestroyView()
+    private fun releaseResource(){
+        // todo: 2023/8/7 解除监听
         curVideoChannel?.removeStreamDataListener(streamDataListener)
+        videoDecoder?.removeDecoderStateChangeListener(decoderStateChangeListener)
+        videoDecoder?.removeYuvDataListener(yuvDataListener)
         // todo: 2023/3/10 释放编码资源
         if (videoDecoder != null) {
             videoDecoder?.destroy()
             videoDecoder = null
         }
     }
+
+    /**
+     * 覆写视图释放
+     */
+    override fun onDestroyView() {
+        super.onDestroyView()
+        // todo: 2023/8/7 释放资源
+        releaseResource()
+        // todo: 2023/8/7 解除事件监听
+        CrApplication.getEventBus().unregister(this)
+    }
+
+    /**
+     * 生命周期函数
+     * 转入前台可见
+     */
+    override fun onResume() {
+        super.onResume()
+        // todo: 2023/8/7 添加监听
+        curVideoChannel?.addStreamDataListener(streamDataListener)
+        // todo: 2023/8/7 启动图传
+        handlerYUV(true)
+    }
+
+    /**
+     * 转入后台不可见
+     */
+    override fun onStop() {
+        super.onStop()
+        // todo: 2023/8/7 释放资源
+        releaseResource()
+    }
+
+    /**
+     * 模式切换 true表示切换到图传模式 false表示切换到YUV模式
+     * @param isSelected Boolean
+     */
+    private fun handlerYUV(isSelected: Boolean) {
+        if (!isSelected) {
+            // todo: 2023/8/7 如果解码器存在 则停用并置null
+            videoDecoder?.let {
+                videoDecoder!!.onPause()
+                videoDecoder!!.destroy()
+                videoDecoder = null
+            }
+            // todo: 2023/8/7 重新定义解码器
+            videoDecoder =
+                VideoDecoder(this@FragmentFPV.context, curVideoChannel!!.videoChannelType)
+            videoDecoder?.addDecoderStateChangeListener(decoderStateChangeListener)
+            videoDecoder?.addYuvDataListener(yuvDataListener)
+        } else {
+            // todo: 2023/8/7 如果解码器存在 则停用并置null
+            videoDecoder?.let {
+                videoDecoder!!.onPause()
+                videoDecoder!!.destroy()
+                videoDecoder = null
+            }
+            // todo: 2023/8/7 重新定义解码器
+            videoDecoder = VideoDecoder(
+                this@FragmentFPV.context,
+                curVideoChannel!!.videoChannelType,
+                DecoderOutputMode.SURFACE_MODE,
+                surfaceView.holder
+            )
+            videoDecoder?.addDecoderStateChangeListener(decoderStateChangeListener)
+            videoDecoder?.removeYuvDataListener(yuvDataListener)
+        }
+    }
+
+    /**
+     * yuv数据监听
+     */
+    private val yuvDataListener =
+        YuvDataListener { mediaFormat, data, width, height ->
+            // todo: 2023/8/7 创建立即执行的协程
+            data?.let {
+                GlobalScope.launch {
+                    // todo: 2023/8/7 执行一次保存 就恢复图传
+                    handlerYUV(true)
+                    // todo: 2023/8/7 保存图片
+                    saveYuvData(mediaFormat,data,width,height)
+                }
+            }
+        }
+
+    /**
+     * 保存当前帧Yuv数据
+     * @param mediaFormat MediaFormat? 媒体格式
+     * @param data ByteArray? 数据
+     * @param width Int 宽度
+     * @param height Int 高度
+     */
+    private fun saveYuvData(mediaFormat: MediaFormat?, data: ByteArray?, width: Int, height: Int) {
+        data?.let {
+            mediaFormat?.let {
+                when(it.getInteger(MediaFormat.KEY_COLOR_FORMAT)){
+                    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar->{
+                        newSaveYuvDataToJPEG420P(data,width,height)
+                    }
+                    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar->{
+                        newSaveYuvDataToJPEG(data,width,height)
+                    }
+                }
+            }?:sendFailureMessage("数据格式异常")
+        }?:sendFailureMessage("无图像数据")
+    }
+
+    /**
+     * 发送错误消息事件
+     * @param error String 错误消息
+     */
+    private fun sendFailureMessage(error:String){
+        CrApplication.getEventBus().post(EventCommon<String>(CrCommonAction.VIDEO_SAVE_YUV_FAILURE,error))
+    }
+
+    /**
+     * 发送YUA数据保存成功消息
+     * @param fileName String 保存的文件名称
+     */
+    private fun sendSaveYUVSuccessMessage(fileName:String){
+        CrApplication.getEventBus().post(EventCommon<String>(CrCommonAction.VIDEO_SAVE_YUV_SUCCESS,fileName))
+    }
+
+    /**
+     * 保存yuv数据到JPEG420P
+     * @param yuvFrame ByteArray 帧数据
+     * @param width Int 宽度
+     * @param height Int 高度
+     */
+    private fun newSaveYuvDataToJPEG420P(yuvFrame: ByteArray, width: Int, height: Int) {
+        if (yuvFrame.size < width * height) {
+            sendFailureMessage("帧数据异常")
+            return
+        }
+        val length = width * height
+        val u = ByteArray(width * height / 4)
+        val v = ByteArray(width * height / 4)
+        for (i in u.indices) {
+            u[i] = yuvFrame[length + i]
+            v[i] = yuvFrame[length + u.size + i]
+        }
+        for (i in u.indices) {
+            yuvFrame[length + 2 * i] = v[i]
+            yuvFrame[length + 2 * i + 1] = u[i]
+        }
+        screenShot(
+            yuvFrame,
+            width,
+            height
+        )
+    }
+
+    /**
+     * 保存Yuv数据到JPEG
+     * @param yuvFrame ByteArray 帧数据
+     * @param width Int 宽度
+     * @param height Int 高度
+     */
+    private fun newSaveYuvDataToJPEG(yuvFrame: ByteArray, width: Int, height: Int) {
+        if (yuvFrame.size < width * height) {
+            sendFailureMessage("帧数据异常")
+            return
+        }
+        val length = width * height
+        val u = ByteArray(width * height / 4)
+        val v = ByteArray(width * height / 4)
+        for (i in u.indices) {
+            v[i] = yuvFrame[length + 2 * i]
+            u[i] = yuvFrame[length + 2 * i + 1]
+        }
+        for (i in u.indices) {
+            yuvFrame[length + 2 * i] = u[i]
+            yuvFrame[length + 2 * i + 1] = v[i]
+        }
+        screenShot(
+            yuvFrame,
+            width,
+            height
+        )
+    }
+    /**
+     * 保存图片
+     * @param buf ByteArray 数据
+     * @param width Int 宽度
+     * @param height Int 高度
+     */
+    private fun screenShot(buf:ByteArray,width:Int,height:Int){
+        // todo: 2023/8/7 获取图片
+        val yuvImage = YuvImage(buf,ImageFormat.NV21,width,height,null)
+        // todo: 2023/8/7 保存文件
+        val outputFile:OutputStream
+        var fileName = "uav_${CrUnitManager.toSystemDate()}.jpg"
+        var path = "${CrUtil.IMAGE_PATH}${fileName}"
+        outputFile = try{
+            FileOutputStream(File(path))
+        }catch (e:FileNotFoundException){
+            sendFailureMessage("异常:${e.message}")
+            return
+        }
+        // todo: 2023/8/7 压缩保存图片
+        yuvImage.compressToJpeg(Rect(0,0,width,height),100,outputFile)
+        try{
+            outputFile.close()
+            // todo: 2023/8/7 发送正确消息
+            sendSaveYUVSuccessMessage(fileName)
+        }catch (e:IOException){
+            sendFailureMessage("IO错误:${e.message}")
+        }
+    }
+
+    // todo: 2023/8/7 订阅事件
+    @Subscribe
+    fun onChanged(event:EventCommon<String>){
+        event?.let {
+            when(it.action){
+                CrCommonAction.VIDEO_SAVE_YUV->{
+                    // todo: 2023/8/7 切换到YUV模式
+                    handlerYUV(false)
+                }
+            }
+        }
+    }
 }

+ 22 - 22
app/src/main/java/com/cr/pages/FragmentMap.kt

@@ -887,7 +887,7 @@ class FragmentMap : CrAnimationFragment() {
         // todo: 2023/4/19 创建要素
         var markGraphic = Graphic(mapPoint, markSymbol)
         gLayerIco?.graphics?.add(markGraphic)
-        CrUtil.showMessage("标志添加成功!")
+        CrUtil.showToast("标志添加成功!")
     }
 
     /**
@@ -954,7 +954,7 @@ class FragmentMap : CrAnimationFragment() {
      * 测量长度
      */
     private fun measureLength() {
-        CrUtil.showMessage("地图上点击开始测量!")
+        CrUtil.showToast("地图上点击开始测量!")
         sketchEditor?.let {
             it.stop()
             it.removeGeometryChangedListener(sketchGeometryChangeListener)
@@ -974,7 +974,7 @@ class FragmentMap : CrAnimationFragment() {
      * 面积测量
      */
     private fun measureArea() {
-        CrUtil.showMessage("地图上点击开始测量!")
+        CrUtil.showToast("地图上点击开始测量!")
         sketchEditor?.let {
             it.stop()
             it.removeGeometryChangedListener(sketchGeometryChangeListener)
@@ -998,9 +998,9 @@ class FragmentMap : CrAnimationFragment() {
             // todo: 2023/6/7 点击完成按钮回调
             override fun completion(valueOne: String, valueTwo: String, self: DialogInput) {
                 if (!CrUnitManager.checkLongitude(valueOne)) {
-                    CrUtil.showMessage("输入的经度值不符合要求!")
+                    CrUtil.showToast("输入的经度值不符合要求!")
                 } else if (!CrUnitManager.checkLatitude(valueTwo)) {
-                    CrUtil.showMessage("输入的纬度值不符合要求!")
+                    CrUtil.showToast("输入的纬度值不符合要求!")
                 } else {
                     self.dismiss()
                     // todo: 2023/6/8 添加到地图中
@@ -1017,7 +1017,7 @@ class FragmentMap : CrAnimationFragment() {
      * 开始绘制案件图斑
      */
     private fun caseStartDraw() {
-        CrUtil.showMessage("地图上点击开始绘制!")
+        CrUtil.showToast("地图上点击开始绘制!")
         sketchEditor?.let {
             it.stop()
             it.removeGeometryChangedListener(sketchGeometryChangeListener)
@@ -1153,7 +1153,7 @@ class FragmentMap : CrAnimationFragment() {
                     caseUpdatePolygonByFeature(caseFeature, object : ICompletion<String> {
                         override fun onCompletion(completion: CompletionModel<String>) {
                             if (completion.isSuccess == true) {
-                                CrUtil.showMessage(
+                                CrUtil.showToast(
                                     String.format(
                                         "案件点追加成功 %s",
                                         caseFeature.attributes[FIELD_CASE_NAME]
@@ -1308,7 +1308,7 @@ class FragmentMap : CrAnimationFragment() {
                                 caseRemoveWaypointAndUpdateCasePolygon(features,object:ICompletion<String>{
                                     override fun onCompletion(completion: CompletionModel<String>) {
                                         if(completion.isSuccess == true){
-                                            CrUtil.showMessage("删除成功!")
+                                            CrUtil.showToast("删除成功!")
                                         }else{
                                             showError(completion.result!!)
                                         }
@@ -1405,7 +1405,7 @@ class FragmentMap : CrAnimationFragment() {
      * 移动案件点
      */
     private fun caseMoveWaypoint() {
-        CrUtil.showMessage("选择需要移动的案件点!")
+        CrUtil.showToast("选择需要移动的案件点!")
         mapTouch?.setAction(MapAction.MapTapStartMoveWaypoing)
     }
 
@@ -1431,11 +1431,11 @@ class FragmentMap : CrAnimationFragment() {
                         features.add(fea)
                     }
                     if (features.size == 0) {
-                        CrUtil.showMessage("未查询到任何违建点!")
+                        CrUtil.showToast("未查询到任何违建点!")
                     } else {
                         fLayerMedia?.clearSelection()
                         fLayerMedia?.selectFeature(features[0])
-                        CrUtil.showMessage("地图点击确定移动位置!")
+                        CrUtil.showToast("地图点击确定移动位置!")
                         mapTouch?.setAction(MapAction.MapTapStopMoveWaypoint)
                         // todo: 2023/6/15 利用编辑工具
                         sketchEditor?.start(location)
@@ -1472,7 +1472,7 @@ class FragmentMap : CrAnimationFragment() {
                                     caseUpdatePolygonByFeature(features[0], object : ICompletion<String> {
                                         override fun onCompletion(completion: CompletionModel<String>) {
                                             if (completion.isSuccess == true) {
-                                                CrUtil.showMessage("移动完成!")
+                                                CrUtil.showToast("移动完成!")
                                                 sketchEditor?.removeGeometryChangedListener(
                                                     sketchGeometryChangeListener
                                                 )
@@ -1508,7 +1508,7 @@ class FragmentMap : CrAnimationFragment() {
                 if (asyncQuery.isDone) {
                     var elements = asyncQuery.get().elements
                     if (elements.size <= 0) {
-                        CrUtil.showMessage("未选择上传案件!")
+                        CrUtil.showToast("未选择上传案件!")
                     } else {
                         fLayerMedia?.selectFeature(elements.last() as Feature)
                         if(eventListener != null) eventListener?.onCaseUpdate(CaseModel(elements.last().attributes))
@@ -1530,7 +1530,7 @@ class FragmentMap : CrAnimationFragment() {
         when (event.action) {
             // todo: 2023/4/17 绘制涂鸦
             MapAction.MapTapDrawDoodle -> {
-                CrUtil.showMessage("地图接收绘制命令")
+                CrUtil.showToast("地图接收绘制命令")
                 doodleStartDraw()
             }
             // todo: 2023/4/17 清除涂鸦
@@ -1555,7 +1555,7 @@ class FragmentMap : CrAnimationFragment() {
             }
             // todo: 2023/4/18 选择涂鸦
             MapAction.MapTapSelectDoodle -> {
-                CrUtil.showMessage("地图上点击需要选择的涂鸦")
+                CrUtil.showToast("地图上点击需要选择的涂鸦")
                 mapTouch?.setQueryLayer(fLayerDoodle!!, MapAction.MapTapSelectDoodle)
             }
             // todo: 2023/4/18 停止Touch
@@ -1565,7 +1565,7 @@ class FragmentMap : CrAnimationFragment() {
             }
             // todo: 2023/4/19 绘制标志
             MapAction.MapTapAppendMark -> {
-                CrUtil.showMessage("地图上单击创建标志!")
+                CrUtil.showToast("地图上单击创建标志!")
                 if (event.owner is FragmentMark) {
                     markChange = (event.owner as FragmentMark).getMark()
                     mapTouch?.setAction(MapAction.MapTapAppendMark)
@@ -1575,7 +1575,7 @@ class FragmentMap : CrAnimationFragment() {
             MapAction.EventMarkClear -> {
                 gLayerIco?.let {
                     it.graphics?.clear()
-                    CrUtil.showMessage("标志清除完成!")
+                    CrUtil.showToast("标志清除完成!")
                 }
             }
             // todo: 2023/4/19 标志保存
@@ -1588,7 +1588,7 @@ class FragmentMap : CrAnimationFragment() {
             }
             // todo: 2023/4/21 选择标志
             MapAction.MapTapSelectMark -> {
-                CrUtil.showMessage("地图上点击需要选择的标志")
+                CrUtil.showToast("地图上点击需要选择的标志")
                 mapTouch?.setQueryLayer(fLayerMark!!, MapAction.MapTapSelectMark)
             }
             // todo: 2023/4/21 删除选择的标志
@@ -1615,7 +1615,7 @@ class FragmentMap : CrAnimationFragment() {
             }
             // todo: 2023/6/7 获取地图位置
             MapAction.MapTapGetLocation -> {
-                CrUtil.showMessage("地图上点击查询地理位置")
+                CrUtil.showToast("地图上点击查询地理位置")
                 mapTouch?.setAction(MapAction.MapTapGetLocation)
             }
             // todo: 2023/6/7 输入坐标定位
@@ -1652,12 +1652,12 @@ class FragmentMap : CrAnimationFragment() {
             }
             // todo: 2023/6/12 地图点击添加违建点
             MapAction.MapTapAddWaypoint -> {
-                CrUtil.showMessage("地图上点击添加案件点!")
+                CrUtil.showToast("地图上点击添加案件点!")
                 mapTouch?.setAction(MapAction.MapTapAddWaypoint)
             }
             // todo: 2023/6/13 删除案件点
             MapAction.MapTapDeleteWaypoint -> {
-                CrUtil.showMessage("地图上选择需要删除的违建点后删除!")
+                CrUtil.showToast("地图上选择需要删除的违建点后删除!")
                 mapTouch?.setAction(MapAction.MapTapDeleteWaypoint)
             }
             // todo: 2023/6/14 移动案件点
@@ -1666,7 +1666,7 @@ class FragmentMap : CrAnimationFragment() {
             }
             // todo: 2023/6/15 案件分享及上传
             MapAction.MapTapCaseWxAndUpload -> {
-                CrUtil.showMessage("地图上选择需要上传或分享的案件!")
+                CrUtil.showToast("地图上选择需要上传或分享的案件!")
                 mapTouch?.setAction(MapAction.MapTapCaseWxAndUpload)
             }
         }

+ 2 - 2
app/src/main/java/com/cr/pages/FragmentTopInfo.kt

@@ -105,7 +105,7 @@ open class FragmentTopInfo : Fragment() {
         // todo: 2023/3/9 订阅MSDK版本信息
         msdkInfoVm.msdkInfo.observe(requireActivity()) {
             if (msdkRegisterVm.registerInfo.value?.isRegister == true) {
-                dji_sdk_version.text = it.SDKVersion
+                dji_sdk_version?.text = it.SDKVersion
             }
         }
 
@@ -143,7 +143,7 @@ open class FragmentTopInfo : Fragment() {
         flightControlVm.flightControlInfo.observe(requireActivity()) {
             it?.let {
                 if (msdkRegisterVm.registerInfo.value?.isRegister == true) {
-                    dji_product_connection.text = it.productName
+                    dji_product_connection?.text = it.productName
                     if (it.batteryPercent > 0) {
                         dji_battery_value.text =
                             resources.getString(R.string.value_battery_percent, it.batteryPercent)

+ 89 - 18
app/src/main/java/com/cr/viewmodel/CrRemoteControlVM.kt

@@ -1,15 +1,18 @@
 package com.cr.viewmodel
 
 import androidx.lifecycle.MutableLiveData
+import com.cr.common.CrAudioUtil
+import com.cr.common.CrKeyManager
 import com.cr.common.CrRemoteControlInfo
+import com.cr.cruav.CrApplication
 import com.cr.cruav.R
 import com.cr.data.CrUtil
+import com.cr.event.CrCommonAction
+import com.cr.event.EventCommon
 import dji.sdk.keyvalue.key.CameraKey
-import dji.sdk.keyvalue.key.KeyTools
 import dji.sdk.keyvalue.key.RemoteControllerKey
+import dji.sdk.keyvalue.value.camera.CameraMode
 import dji.sdk.keyvalue.value.common.EmptyMsg
-import dji.v5.common.callback.CommonCallbacks
-import dji.v5.common.error.IDJIError
 import dji.v5.et.create
 import dji.v5.et.listen
 import dji.v5.utils.common.ContextUtil
@@ -25,6 +28,7 @@ class CrRemoteControlVM : CrViewModel(){
     // define: 2023/3/9 遥控器信息
     var remoteControlInfo = MutableLiveData<CrRemoteControlInfo>()
 
+
     /**
      * 初始化
      */
@@ -48,7 +52,7 @@ class CrRemoteControlVM : CrViewModel(){
                         R.string.value_rc_connection)
                     refresh(remoteControlInfo)
                 }else{
-                    remoteControlInfo.value?.isConnection = true
+                    remoteControlInfo.value?.isConnection = false
                     remoteControlInfo.value?.connectionInfo = StringUtils.getResStr(ContextUtil.getContext(),
                         R.string.value_product_remote_controller)
                     remoteControlInfo.value?.batteryPercent = 0
@@ -67,28 +71,95 @@ class CrRemoteControlVM : CrViewModel(){
         RemoteControllerKey.KeyShutterButtonDown.create().listen(this){
             it?.let {
                 if(it){
-                    CrUtil.print("拍照按钮按下");
-                    dji.v5.manager.KeyManager.getInstance()
-                        .performAction(
-                            KeyTools.createKey(CameraKey.KeyStartShootPhoto),
-                            object : CommonCallbacks.CompletionCallbackWithParam<EmptyMsg> {
-                                override fun onSuccess(t: EmptyMsg?) {
-                                    CrUtil.print("拍照成功${t.toString()}")
-                                }
-
-                                override fun onFailure(error: IDJIError) {
-                                    CrUtil.print("拍照失败${error.description()}}")
-                                }
-                            })
+                    photo()
                 }
             }
         }
+        // todo: 2023/8/5 切换相机模式
         RemoteControllerKey.KeyRCSwitchButtonDown.create().listen(this){
             it?.let {
                 if(it){
-                    CrUtil.print("切换")
+                    photoOrVideo()
                 }
             }
         }
     }
+
+    /**
+     * 拍照
+     */
+    private fun photo(){
+        CrKeyManager.getValue(CameraKey.KeyCameraMode,object:CrKeyManager.ICompletion<CameraMode>{
+            // todo: 2023/8/5 获取成功
+            override fun onSuccess(t: CameraMode) {
+                if(t.isPhotoMode){
+                    // todo: 2023/8/5 执行拍照动作
+                    CrKeyManager.performAction(CameraKey.KeyStartShootPhoto,object:CrKeyManager.ICompletion<EmptyMsg>{
+                        // todo: 2023/8/5 成功
+                        override fun onSuccess(t: EmptyMsg) {
+                            CrAudioUtil.playSound(CrApplication.getContext(),R.raw.cr_shutter_1)
+                            // todo: 2023/8/7 拍照成功 保存当前数据帧YUV数据为图片
+                            CrApplication.getEventBus().post(EventCommon<String>(CrCommonAction.VIDEO_SAVE_YUV))
+                        }
+
+                        // todo: 2023/8/5 错误
+                        override fun onFailure(error: String) {
+                            CrUtil.showToast("拍照失败:${error}")
+                        }
+                    })
+                }else{
+                    CrUtil.showToast("请切换到拍照模式")
+                }
+            }
+
+            // todo: 2023/8/5 获取失败
+            override fun onFailure(error: String) {
+                CrUtil.showToast("获取失败:${error}")
+            }
+        })
+    }
+
+    /**
+     * 切换拍照或录像
+     */
+    private fun photoOrVideo(){
+        CrKeyManager.getValue(CameraKey.KeyCameraMode,object:CrKeyManager.ICompletion<CameraMode>{
+            // todo: 2023/8/5 获取成功
+            override fun onSuccess(t: CameraMode) {
+                if(t.isPhotoMode){
+                    // todo: 2023/8/5 切换到录像模式
+                    CrKeyManager.setValue(CameraKey.KeyCameraMode,CameraMode.VIDEO_NORMAL,object:CrKeyManager.ICompletion<String>{
+                        // todo: 2023/8/5 设置成功
+                        override fun onSuccess(t: String) {
+                            CrUtil.showToast("切换到录像模式")
+                        }
+
+                        // todo: 2023/8/5 设置失败
+                        override fun onFailure(error: String) {
+                            CrUtil.showToast("切换失败:${error}")
+                        }
+
+                    })
+                }else if(t.isVideoMode){
+                    // todo: 2023/8/5 切换到拍照模式
+                    CrKeyManager.setValue(CameraKey.KeyCameraMode,CameraMode.PHOTO_NORMAL,object:CrKeyManager.ICompletion<String>{
+                        // todo: 2023/8/5 切换成功
+                        override fun onSuccess(t: String) {
+                            CrUtil.showToast("切换到拍照模式")
+                        }
+
+                        override fun onFailure(error: String) {
+                            CrUtil.showToast("切换失败:${error}")
+                        }
+
+                    })
+                }
+            }
+
+            // todo: 2023/8/5 获取失败
+            override fun onFailure(error: String) {
+                CrUtil.showToast("获取失败:${error}")
+            }
+        })
+    }
 }

+ 2 - 1
app/src/main/res/layout/frag_dynamic_info.xml

@@ -215,7 +215,8 @@
                 android:text="@string/cr_string_default_value"
                 android:textColor="@color/white"
                 android:textSize="@dimen/sp_14"
-                android:gravity="center"/>
+                android:gravity="center"
+                android:id="@+id/lbl_message"/>
         </LinearLayout>
     </LinearLayout>
 </LinearLayout>

BIN
app/src/main/res/raw/cr_end_video_record.mp3


BIN
app/src/main/res/raw/cr_shutter_1.mp3


BIN
app/src/main/res/raw/cr_shutter_10.mp3


BIN
app/src/main/res/raw/cr_shutter_14.mp3


BIN
app/src/main/res/raw/cr_shutter_3.mp3


BIN
app/src/main/res/raw/cr_shutter_5.mp3


BIN
app/src/main/res/raw/cr_shutter_7.mp3