Browse Source

1、直播功能基本完成
2、修复使用直播解码导致图传窗口显示异常、直播解码导致切换图传窗口异常、切换图传窗口导致直播停止推流
3、解决软件通过代码重启或挂接无人机USB挂件后无人重新初始化大疆MSDK问题

不会爬树的猴 1 year ago
parent
commit
14184dbb30
41 changed files with 1904 additions and 191 deletions
  1. 1 1
      .idea/deploymentTargetDropDown.xml
  2. 1 1
      .idea/misc.xml
  3. 156 0
      app/src/main/java/com/cr/common/CrLiveManager.kt
  4. 25 0
      app/src/main/java/com/cr/common/CrLiveStreamStateInfo.kt
  5. 32 0
      app/src/main/java/com/cr/common/CrSaveManager.kt
  6. 90 0
      app/src/main/java/com/cr/common/CrTimerManager.kt
  7. 20 0
      app/src/main/java/com/cr/common/CrUnitManager.kt
  8. 100 64
      app/src/main/java/com/cr/cruav/AvMain.kt
  9. 57 15
      app/src/main/java/com/cr/cruav/CrActivity.kt
  10. 12 1
      app/src/main/java/com/cr/dialog/DialogInput.kt
  11. 10 0
      app/src/main/java/com/cr/event/CrCommonAction.kt
  12. 11 0
      app/src/main/java/com/cr/pages/CrFragment.kt
  13. 105 34
      app/src/main/java/com/cr/pages/FragmentFPV.kt
  14. 1 1
      app/src/main/java/com/cr/pages/FragmentLayerControl.kt
  15. 451 0
      app/src/main/java/com/cr/pages/FragmentLiveStream.kt
  16. 26 25
      app/src/main/java/com/cr/pages/FragmentMap.kt
  17. 4 4
      app/src/main/java/com/cr/pages/FragmentSet.kt
  18. 14 14
      app/src/main/java/com/cr/pages/FragmentTopInfo.kt
  19. 0 1
      app/src/main/java/com/cr/pages/FragmentUploadAction.kt
  20. 242 0
      app/src/main/java/com/cr/viewmodel/CrLiveStreamVM.kt
  21. 14 2
      app/src/main/java/com/cr/viewmodel/CrMSDKRegisterVM.kt
  22. 4 0
      app/src/main/java/com/cr/widget/CrLinearLayoutWidget.kt
  23. 140 0
      app/src/main/java/com/cr/widget/CrLiveInfoWidget.kt
  24. 2 2
      app/src/main/res/drawable/btn_tools_normal.xml
  25. 1 1
      app/src/main/res/drawable/btn_tools_select.xml
  26. BIN
      app/src/main/res/drawable/ico_fps.png
  27. BIN
      app/src/main/res/drawable/ico_packet.png
  28. BIN
      app/src/main/res/drawable/ico_resolution.png
  29. BIN
      app/src/main/res/drawable/ico_rtt.png
  30. BIN
      app/src/main/res/drawable/ico_timer.png
  31. BIN
      app/src/main/res/drawable/ico_vbps.png
  32. 15 3
      app/src/main/res/layout/av_main.xml
  33. 3 3
      app/src/main/res/layout/dig_input.xml
  34. 13 10
      app/src/main/res/layout/frag_fpv.xml
  35. 136 0
      app/src/main/res/layout/frag_live_stream.xml
  36. 20 1
      app/src/main/res/layout/tools_top.xml
  37. 102 0
      app/src/main/res/layout/wg_live.xml
  38. 9 0
      app/src/main/res/values/colors.xml
  39. 2 0
      app/src/main/res/values/dimens.xml
  40. 26 3
      app/src/main/res/values/strings.xml
  41. 59 5
      app/src/main/res/values/themes.xml

+ 1 - 1
.idea/deploymentTargetDropDown.xml

@@ -12,7 +12,7 @@
         </deviceKey>
         </deviceKey>
       </Target>
       </Target>
     </runningDeviceTargetSelectedWithDropDown>
     </runningDeviceTargetSelectedWithDropDown>
-    <timeTargetWasSelectedWithDropDown value="2023-09-01T00:33:47.138494Z" />
+    <timeTargetWasSelectedWithDropDown value="2023-09-13T00:36:24.238874Z" />
     <runningDeviceTargetsSelectedWithDialog>
     <runningDeviceTargetsSelectedWithDialog>
       <Target>
       <Target>
         <type value="RUNNING_DEVICE_TARGET" />
         <type value="RUNNING_DEVICE_TARGET" />

+ 1 - 1
.idea/misc.xml

@@ -9,7 +9,7 @@
   <component name="VisualizationToolProject">
   <component name="VisualizationToolProject">
     <option name="state">
     <option name="state">
       <ProjectState>
       <ProjectState>
-        <option name="scale" value="0.165" />
+        <option name="scale" value="0.375" />
       </ProjectState>
       </ProjectState>
     </option>
     </option>
   </component>
   </component>

+ 156 - 0
app/src/main/java/com/cr/common/CrLiveManager.kt

@@ -0,0 +1,156 @@
+package com.cr.common
+
+import dji.v5.common.video.channel.VideoChannelType
+import dji.v5.manager.datacenter.livestream.StreamQuality
+
+/**
+ * 操作系统:MAC系统
+ * 创建者:王成
+ * 创建日期:2023/9/12 15:11
+ * 描述:直播管理器 该类与DJI高度相关
+ */
+class CrLiveManager {
+
+    // define: 2023/9/12 质量列表
+    private var qualityList:MutableList<String> = mutableListOf()
+
+    /**
+     * 单例必备
+     */
+    object InstanceHelper{
+        var self = CrLiveManager()
+    }
+
+    companion object{
+        /**
+         * 获取单例
+         * @return CrLiveManager
+         */
+        fun getInstance() = InstanceHelper.self
+    }
+
+    /**
+     * 获取直播视频质量的描述信息
+     * @param streamQuality StreamQuality
+     * @return String
+     */
+    fun getQualityTitle(streamQuality:StreamQuality):String{
+        return when(streamQuality){
+            StreamQuality.SD->{
+                "标清(960*540)"
+            }
+            StreamQuality.HD->{
+                "高清(1280*720)"
+            }
+            StreamQuality.FULL_HD->{
+                "超高清(1920*1080)"
+            }
+            StreamQuality.UNKNOWN->{
+                "未知"
+            }
+        }
+    }
+
+    /**
+     * 根据视频质量获取视频的分辨率
+     * @param streamQuality StreamQuality
+     * @return List<Int>
+     */
+    fun getQualityToVideoResolution(streamQuality:StreamQuality):List<Int>{
+        return when(streamQuality){
+            StreamQuality.SD->{
+                mutableListOf(960,540)
+            }
+            StreamQuality.HD->{
+                mutableListOf(1280,720)
+            }
+            StreamQuality.FULL_HD->{
+                mutableListOf(1920,1080)
+            }
+            StreamQuality.UNKNOWN->{
+                mutableListOf(0,0)
+            }
+        }
+    }
+
+    /**
+     * 根据直播视频质量描述获取视频质量
+     * @param titleQuality String
+     * @return StreamQuality
+     */
+    fun getQuality(titleQuality:String):StreamQuality{
+        return when(titleQuality){
+            "标清(960*540)"->{
+                StreamQuality.SD
+            }
+            "高清(1280*720)"->{
+                StreamQuality.HD
+            }
+            "超高清(1920*1080)"->{
+                StreamQuality.FULL_HD
+            }
+            else->
+                StreamQuality.UNKNOWN
+        }
+    }
+
+    /**
+     * 获取码流通道描述获取码流通道
+     * @param videoChannelType VideoChannelType
+     * @return String
+     */
+    fun getChannelTypeTitle(videoChannelType: VideoChannelType):String{
+        return when(videoChannelType){
+            VideoChannelType.PRIMARY_STREAM_CHANNEL->{
+                "主码流通道"
+            }
+            VideoChannelType.SECONDARY_STREAM_CHANNEL->{
+                "副码流通道"
+            }
+            VideoChannelType.EXTENDED_STREAM_CHANNEL->{
+                "扩展码流通道"
+            }
+            else->{
+                "主码流通道"
+            }
+        }
+    }
+
+    /**
+     * 根据码流描述获取视频通道
+     * @param titleChannelType String
+     * @return VideoChannelType
+     */
+    fun getChannelType(titleChannelType:String):VideoChannelType{
+        return when(titleChannelType){
+            "主码流通道"->{
+                VideoChannelType.PRIMARY_STREAM_CHANNEL
+            }
+            "副码流通道"->{
+                VideoChannelType.SECONDARY_STREAM_CHANNEL
+            }
+            "扩展码流通道"->{
+                VideoChannelType.EXTENDED_STREAM_CHANNEL
+            }
+            else -> {
+                VideoChannelType.PRIMARY_STREAM_CHANNEL
+            }
+        }
+    }
+
+    /**
+     * 获取直播视频质量描述列表
+     * @return List<String>
+     */
+    fun getQualityList():List<String>{
+        return mutableListOf("标清(960*540)","高清(1280*720)","超高清(1920*1080)")
+    }
+
+    /**
+     * 获取码流通道列表
+     * @return List<String>
+     */
+    fun getChannelTypeList():List<String>{
+        return mutableListOf("主码流通道","副码流通道","扩展码流通道")
+    }
+}

+ 25 - 0
app/src/main/java/com/cr/common/CrLiveStreamStateInfo.kt

@@ -0,0 +1,25 @@
+package com.cr.common
+
+/**
+ * 操作系统:MAC系统
+ * 创建者:王成
+ * 创建日期:2023/9/11 17:28
+ * 描述:直播流状态模型
+ */
+class CrLiveStreamStateInfo(var isStreaming:Boolean = false) {
+    var fps:Int = 0  // define: 2023/9/11 直播视频的帧率
+    var vbps:Int = 0  // define: 2023/9/11 直播视频的码率
+    var packetLoss:Int = 0  // define: 2023/9/12 直播视频的丢包率
+    var packetCacheLength:Int = 0  // define: 2023/9/12 直播包缓冲队列长度
+    var rtt:Int = 0  // define: 2023/9/12 返回时间延迟
+    var videoWidth:Int = 0  // define: 2023/9/12 直播视频流分辨率宽度
+    var videoHeight:Int = 0  // define: 2023/9/12 直播视频流分辨率高度
+
+    /**
+     * 覆写转换字符串
+     * @return String
+     */
+    override fun toString(): String {
+        return "是否直播:${this.isStreaming} 帧率:${this.fps} 码率:${this.vbps} 丢包率:${this.packetLoss} 返时延迟:${this.rtt} 视频宽:${this.videoWidth} 视频高:${this.videoHeight}"
+    }
+}

+ 32 - 0
app/src/main/java/com/cr/common/CrSaveManager.kt

@@ -13,6 +13,7 @@ import com.cr.cruav.CrApplication
 class CrSaveManager {
 class CrSaveManager {
     // define: 2023/4/11 key对应变量
     // define: 2023/4/11 key对应变量
     val KEY_MAP_CENTER:String = "mapCenter"
     val KEY_MAP_CENTER:String = "mapCenter"
+    val KEY_LIVE_URL:String = "liveUrl"
 
 
     // define: 2023/4/11 本地化存储实例
     // define: 2023/4/11 本地化存储实例
     var preferences:SharedPreferences ?=null
     var preferences:SharedPreferences ?=null
@@ -58,4 +59,35 @@ class CrSaveManager {
         editor.putBoolean(KEY_MAP_CENTER,isCenter)
         editor.putBoolean(KEY_MAP_CENTER,isCenter)
         editor.commit()
         editor.commit()
     }
     }
+
+    /**
+     * 获取存储的直播地址列表
+     * @return List<String>
+     */
+    fun getLiveUrlList():MutableList<String>{
+        var urls = preferences?.getString(KEY_LIVE_URL,"")
+        var urlSplit = urls?.split(";")
+        var resList:MutableList<String> = mutableListOf()
+        for(i in 0 until urlSplit!!.size){
+            resList.add(urlSplit[i])
+        }
+        return resList
+    }
+
+    /**
+     * 存储url并返回最新集合
+     * @param url String
+     * @return MutableList<String>
+     */
+    fun saveLiveUrl(url:String):MutableList<String>{
+        // todo: 2023/9/12 先获取
+        var urlList = getLiveUrlList()
+        if(urlList.indexOf(url) != -1) return urlList
+        urlList.add(url)
+        var editor:SharedPreferences.Editor = preferences!!.edit()
+        editor.putString(KEY_LIVE_URL,urlList.joinToString(";"))
+        editor.commit()
+        return getLiveUrlList()
+    }
+
 }
 }

+ 90 - 0
app/src/main/java/com/cr/common/CrTimerManager.kt

@@ -0,0 +1,90 @@
+package com.cr.common
+
+import java.text.SimpleDateFormat
+import java.util.*
+import kotlin.concurrent.scheduleAtFixedRate
+
+/**
+ * 操作系统:MAC系统
+ * 创建者:王成
+ * 创建日期:2023/9/14 10:18
+ * 描述:时间管理器
+ */
+class CrTimerManager {
+    private var timeCount: Long = 0  // define: 2023/9/14 计数器数量
+    private var timer: Timer? = null  // define: 2023/9/14 计数器
+    private var startDateTime:Long = 0  // define: 2023/9/14 开始计时时间
+    private var nd:Long = 0  // define: 2023/9/14 一天的毫秒数
+    private var nh:Long = 0  // define: 2023/9/14 一小时的毫秒数
+    private var nm:Long = 0  // define: 2023/9/14 一分钟的毫秒数
+    private var ns:Long = 0  // define: 2023/9/14 一秒钟的毫秒数
+
+    /**
+     * 接口
+     */
+    interface ITimerCountListener {
+        // todo: 2023/9/14 计数器变更回调
+        fun onTimer(count: Long, timeValue: String)
+    }
+
+    /**
+     * 单例函数必备
+     */
+    object InstanceHelper {
+        var self = CrTimerManager()
+    }
+
+    /**
+     * 初始化
+     */
+    init {
+        timer = Timer()
+        nd = (1000 * 24 * 60 * 60).toLong()
+        nh = (1000 * 60 * 60).toLong()
+        nm = (1000 * 60).toLong()
+        ns = 1000
+
+    }
+
+    companion object {
+        /**
+         * 获取单例
+         * @return CrTimerManager
+         */
+        fun getInstance() = InstanceHelper.self
+    }
+
+    /**
+     * 开始计数
+     * @param delay 计数周期 单位是秒
+     * @param callback ITimerCountListener 回调
+     */
+    fun startTimer(delay: Long, callback: ITimerCountListener) {
+        // todo: 2023/9/14 先停止计数
+        timer?.cancel()
+        timer = Timer()
+        // todo: 2023/9/14 计数器清零
+        timeCount = 0
+        startDateTime = System.currentTimeMillis()
+        // todo: 2023/9/14 开始计数
+        timer?.scheduleAtFixedRate(0, delay * 1000) {
+            timeCount++
+            // todo: 2023/9/14 计算毫秒时间差
+            var diff = Date(System.currentTimeMillis()).time - Date(startDateTime).time
+            val hour: Long = diff % nd / nh  // todo: 2023/9/14 计算差多少小时
+            val min: Long = diff % nd % nh / nm // todo: 2023/9/14 计算差多少分钟
+            val sec: Long = diff % nd % nh % nm / ns  // todo: 2023/9/14 计算差多少秒
+            val hourStr = if(hour <10) "0${hour}" else "$hour"
+            val miniStr = if(min <10) "0${min}" else "$min"
+            val secStr = if(sec <10) "0${sec}" else "$sec"
+            callback.onTimer(timeCount,"$hourStr:$miniStr:$secStr")
+        }
+    }
+
+    /**
+     * 停止计数
+     */
+    fun stopTimer() {
+        timer?.cancel()
+    }
+}

+ 20 - 0
app/src/main/java/com/cr/common/CrUnitManager.kt

@@ -2,13 +2,18 @@ package com.cr.common
 
 
 import android.content.Context
 import android.content.Context
 import android.content.res.Resources
 import android.content.res.Resources
+import android.graphics.Color
 import android.graphics.Point
 import android.graphics.Point
 import android.os.Build
 import android.os.Build
 import android.util.DisplayMetrics
 import android.util.DisplayMetrics
 import android.view.WindowManager
 import android.view.WindowManager
+import com.contrarywind.listener.OnItemSelectedListener
 import com.cr.cruav.CrActivity
 import com.cr.cruav.CrActivity
 import com.cr.cruav.CrApplication
 import com.cr.cruav.CrApplication
 import com.cr.cruav.R
 import com.cr.cruav.R
+import com.cr.data.CrUtil
+import com.cr.view.CrViewWheel
+import dji.v5.utils.common.ContextUtil
 import java.math.RoundingMode
 import java.math.RoundingMode
 import java.text.DecimalFormat
 import java.text.DecimalFormat
 import java.text.SimpleDateFormat
 import java.text.SimpleDateFormat
@@ -210,5 +215,20 @@ class CrUnitManager {
         fun getSmallestWidthDP(context: Context):Int{
         fun getSmallestWidthDP(context: Context):Int{
            return context.resources.configuration.smallestScreenWidthDp
            return context.resources.configuration.smallestScreenWidthDp
         }
         }
+
+        /**
+         * 设置选择器
+         * @param wheel CrViewWheel 选择器
+         * @param listener OnItemSelectedListener 监听
+         */
+        fun setWheel(wheel: CrViewWheel, listener: OnItemSelectedListener) {
+            wheel.setTextColorCenter(Color.YELLOW) // TODO: 4/17/21 设置选中项颜色
+            wheel.setItemsVisibleCount(5)
+            wheel.setTextSize(CrUtil.getDimens(R.dimen.sp_5))
+            wheel.setTypeface(CrUtil.getFont(ContextUtil.getContext()))
+            wheel.setCyclic(false) // TODO: 6/9/21 禁止循环
+            wheel.setOnItemSelectedListener(listener)
+            wheel.setDividerColor(CrColorManager.getColor(R.color.cadetblue))
+        }
     }
     }
 }
 }

+ 100 - 64
app/src/main/java/com/cr/cruav/AvMain.kt

@@ -1,11 +1,16 @@
 package com.cr.cruav
 package com.cr.cruav
 
 
 import android.annotation.SuppressLint
 import android.annotation.SuppressLint
+import android.app.ActivityManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
 import android.os.Bundle
 import android.os.Bundle
 import android.view.View
 import android.view.View
+import android.view.WindowManager
 import androidx.activity.viewModels
 import androidx.activity.viewModels
-
 import androidx.fragment.app.commit
 import androidx.fragment.app.commit
+import com.cr.common.CrTimerManager
 import com.cr.data.CrConfig
 import com.cr.data.CrConfig
 import com.cr.data.CrUtil
 import com.cr.data.CrUtil
 import com.cr.event.*
 import com.cr.event.*
@@ -18,71 +23,45 @@ import com.squareup.otto.Subscribe
 import dji.v5.common.error.IDJIError
 import dji.v5.common.error.IDJIError
 import dji.v5.common.register.DJISDKInitEvent
 import dji.v5.common.register.DJISDKInitEvent
 import dji.v5.manager.interfaces.SDKManagerCallback
 import dji.v5.manager.interfaces.SDKManagerCallback
-import dji.v5.utils.common.ToastUtils
 import kotlinx.android.synthetic.main.av_main.*
 import kotlinx.android.synthetic.main.av_main.*
 import kotlinx.android.synthetic.main.tools_top.*
 import kotlinx.android.synthetic.main.tools_top.*
+import java.util.*
+import kotlin.concurrent.scheduleAtFixedRate
 
 
 class AvMain : CrActivity(), View.OnClickListener {
 class AvMain : CrActivity(), View.OnClickListener {
-    // define: 2023/3/10 将App视图绑定到模型
-    private val msdkRegisterVm: CrMSDKRegisterVM by viewModels()
-
-    // define: 2023/3/10 将MSDK视图绑定到模型
-    private val msdkInfoVm: CrMSDKInfoVM by viewModels()
-
-    // define: 2023/3/10 将遥控器视图绑定到模型
-    private val remoteControlVm: CrRemoteControlVM by viewModels()
-
-    // define: 2023/3/10 将飞行器视图绑定到模型
-    private val flightControlVm: CrFlightControlVM by viewModels()
-
-    // define: 2023/3/14 将链路视图绑定到模型
-    private val linkVm: CrLinkVM by viewModels()
-
-    // define: 2023/8/2 绑定相机视图到模型
-    private val cameraVm:CrCameraVM by viewModels()
-
-    // define: 2023/3/13 顶部视图容器
-    private var fragmentTopInfo: FragmentTopInfo? = null
-
-    // define: 2023/3/13 图传容器
-    private var fragmentFPV: FragmentFPV? = null
-
-    // define: 2023/3/14 地图容器
-    private var fragmentMap: FragmentMap? = null
-
-    // define: 2023/3/14 图传窗口是不是小窗口
-    private var isFpvSmallWindow: Boolean = true
-
-    // define: 2023/4/11 初始化设置页面
-    private var fragmentSet: FragmentSet? = null
-
-    // define: 2023/4/14 图层控制页面
-    private var fragmentLayerControl: FragmentLayerControl? = null
-
-    // define: 2023/4/17 涂鸦页面
-    private var fragmentDoodle: FragmentDoodle? = null
-
-    // define: 2023/4/18 标志页面
-    private var fragmentMark: FragmentMark? = null
-
-    // define: 2023/4/21 工具页面
-    private var fragmentTools: FragmentTools? = null
-
-    // define: 2023/6/9 案件图斑编辑页面
-    private var fragmentCaseTools: FragmentCaseTools? = null
-
-    // define: 2023/6/15 案件上传页面
-    private var fragmentUploadCase: FragmentUploadCase? = null
-
-    // define: 2023/7/29 大疆动态信息
-    private var fragmentDynamicInfo:FragmentDynamicInfo?= null
-
+    // todo: 2023/9/14 视图模型绑定
+    private val msdkRegisterVm: CrMSDKRegisterVM by viewModels() // define: 2023/3/10 将App视图绑定到模型
+    private val msdkInfoVm: CrMSDKInfoVM by viewModels()  // define: 2023/3/10 将MSDK视图绑定到模型
+    private val remoteControlVm: CrRemoteControlVM by viewModels() // define: 2023/3/10 将遥控器视图绑定到模型
+    private val flightControlVm: CrFlightControlVM by viewModels() // define: 2023/3/10 将飞行器视图绑定到模型
+    private val liveStreamVm: CrLiveStreamVM by viewModels() // define: 2023/9/13 将直播视图绑定到模型
+    private val linkVm: CrLinkVM by viewModels() // define: 2023/3/14 将链路视图绑定到模型
+    private val cameraVm:CrCameraVM by viewModels() // define: 2023/8/2 绑定相机视图到模型
+
+    // todo: 2023/9/14 Fragment定义
+    private var fragmentTopInfo: FragmentTopInfo? = null // define: 2023/3/13 顶部视图容器
+    private var fragmentFPV: FragmentFPV? = null // define: 2023/3/13 图传容器
+    private var fragmentMap: FragmentMap? = null // define: 2023/3/14 地图容器
+    private var fragmentSet: FragmentSet? = null // define: 2023/4/11 初始化设置页面
+    private var fragmentLayerControl: FragmentLayerControl? = null // define: 2023/4/14 图层控制页面
+    private var fragmentDoodle: FragmentDoodle? = null // define: 2023/4/17 涂鸦页面
+    private var fragmentMark: FragmentMark? = null  // define: 2023/4/18 标志页面
+    private var fragmentTools: FragmentTools? = null  // define: 2023/4/21 工具页面
+    private var fragmentCaseTools: FragmentCaseTools? = null  // define: 2023/6/9 案件图斑编辑页面
+    private var fragmentUploadCase: FragmentUploadCase? = null // define: 2023/6/15 案件上传页面
+    private var fragmentDynamicInfo:FragmentDynamicInfo?= null  // define: 2023/7/29 大疆动态信息
+    private var fragmentLiveStream:FragmentLiveStream?= null  // define: 2023/9/13 直播页面
+
+    // todo: 2023/9/14 变量定义
+    private var isFpvSmallWindow: Boolean = true  // define: 2023/3/14 图传窗口是不是小窗口
 
 
     /**
     /**
      * 入口函数
      * 入口函数
      */
      */
     override fun onCreate(savedInstanceState: Bundle?) {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         super.onCreate(savedInstanceState)
+        // todo: 2023/9/13 开启屏幕常亮
+        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
         setContentView(R.layout.av_main);
         setContentView(R.layout.av_main);
         // todo: 2023/4/11 挂载控件
         // todo: 2023/4/11 挂载控件
         joinControls()
         joinControls()
@@ -110,6 +89,7 @@ class AvMain : CrActivity(), View.OnClickListener {
         tools_append_point_from_map.setOnClickListener(this)
         tools_append_point_from_map.setOnClickListener(this)
         tools_case_local.setOnClickListener(this)
         tools_case_local.setOnClickListener(this)
         tools_case_net.setOnClickListener(this)
         tools_case_net.setOnClickListener(this)
+        tools_live.setOnClickListener(this)
     }
     }
 
 
     /**
     /**
@@ -176,6 +156,12 @@ class AvMain : CrActivity(), View.OnClickListener {
             addFragment(it, R.id.av_frm_center_panel)
             addFragment(it, R.id.av_frm_center_panel)
             hideFragment(it)
             hideFragment(it)
         }
         }
+        // todo: 2023/9/13 初始化直播页面
+        fragmentLiveStream = FragmentLiveStream()
+        fragmentLiveStream?.let {
+            addFragment(it,R.id.av_frm_right_panel)
+            hideFragment(it)
+        }
     }
     }
 
 
     /**
     /**
@@ -205,7 +191,6 @@ class AvMain : CrActivity(), View.OnClickListener {
                     av_fragment_map.addView(fragmentMap!!.view)
                     av_fragment_map.addView(fragmentMap!!.view)
                     av_fragment_fpv.addView(fragmentFPV!!.view)
                     av_fragment_fpv.addView(fragmentFPV!!.view)
                 }
                 }
-
             }
             }
         }
         }
         supportFragmentManager.commit {
         supportFragmentManager.commit {
@@ -242,6 +227,8 @@ class AvMain : CrActivity(), View.OnClickListener {
                 linkVm.initListener()
                 linkVm.initListener()
                 // todo: 2023/8/2 相机监听初始化
                 // todo: 2023/8/2 相机监听初始化
                 cameraVm.initListener()
                 cameraVm.initListener()
+                // todo: 2023/9/13 订阅直播信息
+                initObserve()
             }
             }
 
 
             // todo: 2023/3/13 App注册失败回调
             // todo: 2023/3/13 App注册失败回调
@@ -249,22 +236,27 @@ class AvMain : CrActivity(), View.OnClickListener {
                 CrUtil.print("App注册失败" + error.toString())
                 CrUtil.print("App注册失败" + error.toString())
             }
             }
 
 
+            // todo: 2023/9/14 产品失连
             override fun onProductDisconnect(productId: Int) {
             override fun onProductDisconnect(productId: Int) {
 
 
             }
             }
 
 
+            // todo: 2023/9/14 产品连接
             override fun onProductConnect(productId: Int) {
             override fun onProductConnect(productId: Int) {
 
 
             }
             }
 
 
+            // todo: 2023/9/14 产品变化
             override fun onProductChanged(productId: Int) {
             override fun onProductChanged(productId: Int) {
 
 
             }
             }
 
 
+            // todo: 2023/9/14 产品变化过程
             override fun onInitProcess(event: DJISDKInitEvent?, totalProcess: Int) {
             override fun onInitProcess(event: DJISDKInitEvent?, totalProcess: Int) {
 
 
             }
             }
 
 
+            // todo: 2023/9/14 基础数据下载进程
             override fun onDatabaseDownloadProgress(current: Long, total: Long) {
             override fun onDatabaseDownloadProgress(current: Long, total: Long) {
 
 
             }
             }
@@ -272,6 +264,20 @@ class AvMain : CrActivity(), View.OnClickListener {
     }
     }
 
 
     /**
     /**
+     * 初始化订阅
+     */
+    private fun initObserve(){
+        mainHandler.post{
+            // todo: 2023/9/13 订阅直播信息
+            liveStreamVm.streamStatusInfo.observe(this){
+                it?.let {
+                    CrApplication.getEventBus().post(EventCommon(CrCommonAction.UPDATE_LIVE_INFO,it))
+                }
+            }
+        }
+    }
+
+    /**
      * 视图点击事件
      * 视图点击事件
      * @param view View
      * @param view View
      */
      */
@@ -312,12 +318,15 @@ class AvMain : CrActivity(), View.OnClickListener {
             }
             }
             R.id.tools_case_local->{
             R.id.tools_case_local->{
                 // todo: 2023/8/14 本地案件查询
                 // todo: 2023/8/14 本地案件查询
-                CrApplication.getEventBus().post(EventMapCapture(EventMapCapture.CaptureAction.CAPTURE_ACTION_START,"Case20230103",null))
             }
             }
             R.id.tools_case_net->{
             R.id.tools_case_net->{
                 // todo: 2023/8/14 网络案件查询
                 // todo: 2023/8/14 网络案件查询
 
 
             }
             }
+            R.id.tools_live->{
+                // todo: 2023/9/13 打开直播设置
+                showFragment(fragmentLiveStream!!)
+            }
         }
         }
     }
     }
 
 
@@ -503,6 +512,10 @@ class AvMain : CrActivity(), View.OnClickListener {
      */
      */
     override fun onDestroy() {
     override fun onDestroy() {
         CrApplication.getEventBus().unregister(this)
         CrApplication.getEventBus().unregister(this)
+        // todo: 2023/9/13 关闭屏幕常亮
+        window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+        // todo: 2023/9/15 释放MSDK
+        msdkRegisterVm.destroyMSDK()
         super.onDestroy()
         super.onDestroy()
     }
     }
 
 
@@ -514,6 +527,31 @@ class AvMain : CrActivity(), View.OnClickListener {
         init()
         init()
     }
     }
 
 
+    override fun onPause() {
+        super.onPause()
+        callExitSystem()
+    }
+
+    /**
+     * 重启系统
+     */
+    private fun callExitSystem() {
+        // TODO: 2023/7/25 获取包名
+        var packageName = CrApplication.getContext().packageName
+        // TODO: 2023/7/25 获取重启后页面的类名
+        var className = AvLogin::class.java.name
+        // TODO: 2023/7/25 创建一个Intent来重启App
+        var intent = Intent(Intent.ACTION_MAIN)
+        intent.addCategory(Intent.CATEGORY_LAUNCHER)
+        var cn = ComponentName(packageName,className)
+        intent.component = cn
+        // TODO: 2023/7/25 FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP即重启应用程序的标识
+        // TODO: 2023/7/25 FLAG_ACTIVITY_NEW_TASK告诉系统新的任务需要新的任务栈,FLAG_ACTIVITY_CLEAR_TOP告诉系统清空任务栈
+        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+        CrApplication.getContext().startActivity(intent);
+        // TODO: 2023/7/25 结束当前窗体
+        finish();
+    }
 
 
     /**
     /**
      * 订阅Fragment管理事件
      * 订阅Fragment管理事件
@@ -532,12 +570,10 @@ class AvMain : CrActivity(), View.OnClickListener {
      */
      */
     @Subscribe
     @Subscribe
     fun onVideoYUVSave(event:EventCommon<String>){
     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))
-            }
+        if(event.action == CrCommonAction.VIDEO_SAVE_YUV_FAILURE){
+            CrApplication.getEventBus().post(EventCommon<String>(CrCommonAction.SHOW_MESSAGE,event.param))
+        }else if(event.action == CrCommonAction.VIDEO_SAVE_YUV_SUCCESS){
+            CrApplication.getEventBus().post(EventCommon<String>(CrCommonAction.SHOW_MESSAGE,event.param))
         }
         }
     }
     }
 }
 }

+ 57 - 15
app/src/main/java/com/cr/cruav/CrActivity.kt

@@ -10,6 +10,8 @@ import androidx.fragment.app.FragmentActivity
 import androidx.fragment.app.FragmentManager
 import androidx.fragment.app.FragmentManager
 import androidx.fragment.app.FragmentTransaction
 import androidx.fragment.app.FragmentTransaction
 import com.cr.dialog.DialogNormal
 import com.cr.dialog.DialogNormal
+import com.cr.models.CompletionModel
+import com.cr.models.ICompletion
 
 
 /**
 /**
  * 操作系统:MAC系统
  * 操作系统:MAC系统
@@ -22,7 +24,7 @@ open class CrActivity : AppCompatActivity() {
     val MSG_INFO_SHOW_DIALOG:Int = 1
     val MSG_INFO_SHOW_DIALOG:Int = 1
     val MSG_ERROR_SHOW_DIALOG:Int =2
     val MSG_ERROR_SHOW_DIALOG:Int =2
     // define: 2023/3/10 创建主线程Looper
     // define: 2023/3/10 创建主线程Looper
-    private var mainHandler = object:Handler(Looper.getMainLooper()){
+    protected var mainHandler = object:Handler(Looper.getMainLooper()){
         // todo: 2023/4/10 接收消息
         // todo: 2023/4/10 接收消息
         override fun handleMessage(msg: Message) {
         override fun handleMessage(msg: Message) {
             if(msg.what == MSG_INFO_SHOW_DIALOG){
             if(msg.what == MSG_INFO_SHOW_DIALOG){
@@ -97,26 +99,66 @@ open class CrActivity : AppCompatActivity() {
         }
         }
     }
     }
 
 
+
+    /**
+     * 显示警告消息
+     * @param warning String 警告消息
+     */
+    protected fun showWarning(warning: String) {
+        mainHandler.post {
+            DialogNormal(CrApplication.getContext(), "警告", warning).show()
+        }
+    }
+
     /**
     /**
-     * 显示提示消息(用于多线程)
-     * @param message String 消息内容
+     * 显示错误信息
+     * @param error String 错误消息
      */
      */
-    fun showMessage(message:String){
-        var msg:Message = Message()
-        msg.what = MSG_INFO_SHOW_DIALOG
-        msg.obj = message
-        mainHandler.sendMessage(msg)
+    protected fun showError(error: String) {
+        mainHandler.post{
+            DialogNormal(CrApplication.getContext(), "错误", error).show()
+        }
     }
     }
 
 
     /**
     /**
-     * 显示错误消息(用于多线程)
-     * @param error String 错误内容
+     * 确认对话框
+     * @param message String 显示的消息内容
+     * @param actionButtonContent Array<String> 按钮文字数组 大小必须是2
+     * @param callback ICompletion<String> 回调 可以传null
      */
      */
-    fun showError(error:String){
-        var msg:Message = Message()
-        msg.what = MSG_INFO_SHOW_DIALOG
-        msg.obj = error
-        mainHandler.sendMessage(msg)
+    protected fun showConfirm(message:String,actionButtonContent:Array<String>,callback: ICompletion<String>?){
+        if(actionButtonContent.size != 2) return
+        mainHandler.post {
+            var dialog = DialogNormal(CrApplication.getContext(),"确认",message)
+            dialog.setButtonsText(actionButtonContent[0],actionButtonContent[1])
+            dialog.setListener(object:DialogNormal.DialogNormalListener{
+                // todo: 2023/8/15 确认操作
+                override fun completion() {
+                    callback?.let {
+                        it.onCompletion(CompletionModel(true,""))
+                    }
+                }
+
+                // todo: 2023/8/15 关闭操作
+                override fun close() {
+                    callback?.let {
+                        it.onCompletion(CompletionModel(false,""))
+                    }
+                }
+
+            })
+            dialog.show()
+        }
+    }
+
+    /**
+     * 显示提示信息
+     * @param information String 提示消息
+     */
+    protected fun showInformation(information: String) {
+        mainHandler.post {
+            DialogNormal(CrApplication.getContext(), "提示", information).show()
+        }
     }
     }
 
 
     /**
     /**

+ 12 - 1
app/src/main/java/com/cr/dialog/DialogInput.kt

@@ -136,7 +136,7 @@ class DialogInput : Dialog, View.OnClickListener {
      * @param strOne String 标签一内容
      * @param strOne String 标签一内容
      * @param strTow String 标签二内容
      * @param strTow String 标签二内容
      */
      */
-    fun setLabels(strOne: String,strTow:String) {
+    fun setLabels(strOne: String,strTow:String?) {
         if(strOne == null){
         if(strOne == null){
             panelOne?.visibility = View.GONE
             panelOne?.visibility = View.GONE
         }else{
         }else{
@@ -192,6 +192,16 @@ class DialogInput : Dialog, View.OnClickListener {
     }
     }
 
 
     /**
     /**
+     * 设置初始化显示内容
+     * @param onText String
+     * @param twoText String
+     */
+    fun setContent(onText:String,twoText:String){
+        txtOne?.setContent(onText)
+        txtTwo?.setContent(twoText)
+    }
+
+    /**
      * 设置
      * 设置
      * @param okVisible Boolean 确定按钮是否显示
      * @param okVisible Boolean 确定按钮是否显示
      * @param closeVisible Boolean 取消按钮是否显示
      * @param closeVisible Boolean 取消按钮是否显示
@@ -224,6 +234,7 @@ class DialogInput : Dialog, View.OnClickListener {
                     CrUtil.showToast("请输入内容!")
                     CrUtil.showToast("请输入内容!")
                     return
                     return
                 }
                 }
+                dismiss()
                 listener?.completion(txtOne!!.getContent(),txtTwo!!.getContent(),self!!)
                 listener?.completion(txtOne!!.getContent(),txtTwo!!.getContent(),self!!)
             }
             }
             R.id.dig_btn_cancel -> {
             R.id.dig_btn_cancel -> {

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

@@ -26,4 +26,14 @@ enum class CrCommonAction {
      * 显示消息
      * 显示消息
      */
      */
     SHOW_MESSAGE,
     SHOW_MESSAGE,
+
+    /**
+     * 更新直播信息
+     */
+    UPDATE_LIVE_INFO,
+
+    /**
+     * 直播信息状态
+     */
+    LIVE_INFO_STATE,
 }
 }

+ 11 - 0
app/src/main/java/com/cr/pages/CrFragment.kt

@@ -7,6 +7,7 @@ import android.view.View
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.Fragment
 import com.cr.common.CrKeyManager
 import com.cr.common.CrKeyManager
 import com.cr.cruav.CrApplication
 import com.cr.cruav.CrApplication
+import com.cr.data.CrUtil
 import com.cr.dialog.DialogLoadingUtil
 import com.cr.dialog.DialogLoadingUtil
 import com.cr.dialog.DialogNormal
 import com.cr.dialog.DialogNormal
 import com.cr.models.CompletionModel
 import com.cr.models.CompletionModel
@@ -141,6 +142,16 @@ open class CrFragment :Fragment(){
     }
     }
 
 
     /**
     /**
+     * 显示提示消息
+     * @param message String
+     */
+    protected fun showToast(message:String){
+        mainHandler.post {
+            CrUtil.showToast(message)
+        }
+    }
+
+    /**
      * 获取上下文
      * 获取上下文
      * @return Context?
      * @return Context?
      */
      */

+ 105 - 34
app/src/main/java/com/cr/pages/FragmentFPV.kt

@@ -11,8 +11,10 @@ import android.view.SurfaceHolder
 import android.view.SurfaceView
 import android.view.SurfaceView
 import android.view.View
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
 import android.widget.Button
 import android.widget.Button
 import android.widget.TextView
 import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.fragment.app.activityViewModels
 import androidx.fragment.app.activityViewModels
 import com.cr.common.CrUnitManager
 import com.cr.common.CrUnitManager
 import com.cr.cruav.CrApplication
 import com.cr.cruav.CrApplication
@@ -20,7 +22,10 @@ import com.cr.cruav.R
 import com.cr.data.CrUtil
 import com.cr.data.CrUtil
 import com.cr.event.CrCommonAction
 import com.cr.event.CrCommonAction
 import com.cr.event.EventCommon
 import com.cr.event.EventCommon
+import com.cr.models.CompletionModel
+import com.cr.models.ICompletion
 import com.cr.viewmodel.CrFlightControlVM
 import com.cr.viewmodel.CrFlightControlVM
+import com.cr.viewmodel.CrLiveStreamVM
 import com.cr.viewmodel.CrVideoChannelListener
 import com.cr.viewmodel.CrVideoChannelListener
 import com.cr.viewmodel.CrVideoChannelVM
 import com.cr.viewmodel.CrVideoChannelVM
 import com.squareup.otto.Subscribe
 import com.squareup.otto.Subscribe
@@ -44,7 +49,7 @@ import java.io.OutputStream
  * 创建日期:2023/3/10 08:50
  * 创建日期:2023/3/10 08:50
  * 描述:图传窗口
  * 描述:图传窗口
  */
  */
-class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
+class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback, OnGlobalLayoutListener {
     /**
     /**
      * FPV窗口监听
      * FPV窗口监听
      */
      */
@@ -59,10 +64,14 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
     // define: 2023/3/10 将飞行器视图绑定到模型
     // define: 2023/3/10 将飞行器视图绑定到模型
     private val flightControlVm: CrFlightControlVM by activityViewModels()
     private val flightControlVm: CrFlightControlVM by activityViewModels()
 
 
+    // define: 2023/9/12 绑定直播模型
+    private val liveStreamVm: CrLiveStreamVM by activityViewModels()
+
     // define: 2023/3/10 延迟初始化一个视频画布
     // define: 2023/3/10 延迟初始化一个视频画布
     private lateinit var surfaceView: SurfaceView
     private lateinit var surfaceView: SurfaceView
     private var btnTrigger: Button? = null
     private var btnTrigger: Button? = null
     private var lblInfo: TextView? = null // define: 2023/8/4 显示信息
     private var lblInfo: TextView? = null // define: 2023/8/4 显示信息
+    private var panelFpv: ConstraintLayout? = null // define: 2023/9/14 容器
 
 
     // define: 2023/3/11 视频通道
     // define: 2023/3/11 视频通道
     private var curVideoChannel: IVideoChannel? = null
     private var curVideoChannel: IVideoChannel? = null
@@ -77,6 +86,10 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
     private var heightChange: Boolean = false
     private var heightChange: Boolean = false
     private var fps: Int = -1
     private var fps: Int = -1
 
 
+    // todo: 2023/9/14 画布相关参数
+    private var surfaceViewWidth: Int = 0
+    private var surfaceViewHeight: Int = 0
+
     // todo: 2023/3/13 监听
     // todo: 2023/3/13 监听
     lateinit var listener: FPVListener
     lateinit var listener: FPVListener
 
 
@@ -101,6 +114,8 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
         surfaceView = view.findViewById(R.id.surface_fpv)
         surfaceView = view.findViewById(R.id.surface_fpv)
         btnTrigger = view.findViewById(R.id.fpv_trigger)
         btnTrigger = view.findViewById(R.id.fpv_trigger)
         btnTrigger?.setOnClickListener(clickListener)
         btnTrigger?.setOnClickListener(clickListener)
+        // todo: 2023/9/14 挂接容器
+        panelFpv = view.findViewById(R.id.fpv_panel)
         // todo: 2023/8/4 挂接信息显示组件
         // todo: 2023/8/4 挂接信息显示组件
         lblInfo = view.findViewById(R.id.fpv_info)
         lblInfo = view.findViewById(R.id.fpv_info)
         // todo: 2023/3/10 设置视图控件监听
         // todo: 2023/3/10 设置视图控件监听
@@ -111,6 +126,26 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
     }
     }
 
 
     /**
     /**
+     * 添加视图尺寸变化监听
+     */
+    private fun addSizeChangeForSurfaceView() {
+        surfaceView.viewTreeObserver!!.addOnGlobalLayoutListener(this)
+    }
+
+    /**
+     * 画布尺寸变化监听
+     */
+    override fun onGlobalLayout() {
+        if (surfaceViewWidth != surfaceView.width && surfaceViewHeight != surfaceView.height) {
+            surfaceViewWidth = surfaceView.width
+            surfaceViewHeight = surfaceView.height
+            CrUtil.print("宽度:${surfaceViewWidth} 高度:${surfaceViewHeight}")
+            // todo: 2023/9/14 移除监听 要不会多次调用
+            surfaceView.viewTreeObserver.removeOnGlobalLayoutListener(this)
+        }
+    }
+
+    /**
      * 视图创建完成后调用
      * 视图创建完成后调用
      * @param view View
      * @param view View
      * @param savedInstanceState Bundle?
      * @param savedInstanceState Bundle?
@@ -130,9 +165,22 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
                 // todo: 2023/3/13 切换视频通道
                 // todo: 2023/3/13 切换视频通道
                 videoChannelVM.triggerStreamSource()
                 videoChannelVM.triggerStreamSource()
             }
             }
-            R.id.fpv_layout -> {
+            R.id.fpv_panel -> {
                 // todo: 2023/3/13 切换窗口
                 // todo: 2023/3/13 切换窗口
-                listener?.triggerWindow()
+                listener.triggerWindow()
+//                // todo: 2023/9/14 添加画布尺寸变化监听
+//                // todo: 2023/9/14 主要是为了在画布变化时可以调整解码器解码的宽度和高度
+//                addSizeChangeForSurfaceView()
+                // todo: 2023/9/15 判断当前是否在直播 如果是 则先停止 再开启
+                if (liveStreamVm?.streamStatusInfo.value!!.isStreaming) {
+                    liveStreamVm.stopStream(object : ICompletion<String> {
+                        // todo: 2023/9/15 停止完成后回调
+                        override fun onCompletion(completion: CompletionModel<String>) {
+                            if (completion.isSuccess == true)
+                                liveStreamVm?.startStream(null)
+                        }
+                    })
+                }
             }
             }
         }
         }
     }
     }
@@ -145,7 +193,7 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
         videoChannelVM.videoChannelInfo.observe(viewLifecycleOwner) {
         videoChannelVM.videoChannelInfo.observe(viewLifecycleOwner) {
             it?.let {
             it?.let {
                 val videoStreamInfo =
                 val videoStreamInfo =
-                    "fps:[${it.fps}] width:[${videoWidth}] height:[${videoHeight}]"
+                    "帧率:[${it.fps}] 宽:[${videoWidth}] 高:[${videoHeight}]"
                 lblInfo?.text = videoStreamInfo
                 lblInfo?.text = videoStreamInfo
             }
             }
         }
         }
@@ -189,14 +237,27 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
      * 设置视频通道到画布
      * 设置视频通道到画布
      */
      */
     private fun setChannelToSurface() {
     private fun setChannelToSurface() {
+        // todo: 2023/9/15 释放解码器资源
+        videoDecoder?.let {
+            videoDecoder?.removeDecoderStateChangeListener(decoderStateChangeListener)
+            videoDecoder?.onPause()
+            videoDecoder?.destroy()
+            videoDecoder = null
+        }
+        // todo: 2023/9/15 重新判断初始化
         if (videoDecoder == null) {
         if (videoDecoder == null) {
             curVideoChannel?.let {
             curVideoChannel?.let {
                 videoDecoder = VideoDecoder(
                 videoDecoder = VideoDecoder(
                     this@FragmentFPV.context,
                     this@FragmentFPV.context,
                     it.videoChannelType,
                     it.videoChannelType,
                     DecoderOutputMode.SURFACE_MODE,
                     DecoderOutputMode.SURFACE_MODE,
-                    surfaceView.holder
+                    surfaceView.holder,
+                    surfaceView.width,
+                    surfaceView.height,
+                    true
                 )
                 )
+                // todo: 2023/9/12 设置直播视频通道
+//                liveStreamVm.setVideoChannel(it.videoChannelType)
                 // todo: 2023/3/10 添加解码状态监听
                 // todo: 2023/3/10 添加解码状态监听
                 videoDecoder?.addDecoderStateChangeListener(decoderStateChangeListener)
                 videoDecoder?.addDecoderStateChangeListener(decoderStateChangeListener)
             }
             }
@@ -206,7 +267,7 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
     }
     }
 
 
     /**
     /**
-     * 编码状态监听
+     * 解码器状态监听
      */
      */
     private val decoderStateChangeListener =
     private val decoderStateChangeListener =
         DecoderStateChangeListener { oldState, newState ->
         DecoderStateChangeListener { oldState, newState ->
@@ -281,7 +342,7 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
     /**
     /**
      * 释放资源
      * 释放资源
      */
      */
-    private fun releaseResource(){
+    private fun releaseResource() {
         // todo: 2023/8/7 解除监听
         // todo: 2023/8/7 解除监听
         curVideoChannel?.removeStreamDataListener(streamDataListener)
         curVideoChannel?.removeStreamDataListener(streamDataListener)
         videoDecoder?.removeDecoderStateChangeListener(decoderStateChangeListener)
         videoDecoder?.removeDecoderStateChangeListener(decoderStateChangeListener)
@@ -375,7 +436,7 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
                     // todo: 2023/8/7 执行一次保存 就恢复图传
                     // todo: 2023/8/7 执行一次保存 就恢复图传
                     handlerYUV(true)
                     handlerYUV(true)
                     // todo: 2023/8/7 保存图片
                     // todo: 2023/8/7 保存图片
-                    saveYuvData(mediaFormat,data,width,height)
+                    saveYuvData(mediaFormat, data, width, height)
                 }
                 }
             }
             }
         }
         }
@@ -390,32 +451,34 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
     private fun saveYuvData(mediaFormat: MediaFormat?, data: ByteArray?, width: Int, height: Int) {
     private fun saveYuvData(mediaFormat: MediaFormat?, data: ByteArray?, width: Int, height: Int) {
         data?.let {
         data?.let {
             mediaFormat?.let {
             mediaFormat?.let {
-                when(it.getInteger(MediaFormat.KEY_COLOR_FORMAT)){
-                    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar->{
-                        newSaveYuvDataToJPEG420P(data,width,height)
+                when (it.getInteger(MediaFormat.KEY_COLOR_FORMAT)) {
+                    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar -> {
+                        newSaveYuvDataToJPEG420P(data, width, height)
                     }
                     }
-                    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar->{
-                        newSaveYuvDataToJPEG(data,width,height)
+                    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar -> {
+                        newSaveYuvDataToJPEG(data, width, height)
                     }
                     }
                 }
                 }
-            }?:sendFailureMessage("数据格式异常")
-        }?:sendFailureMessage("无图像数据")
+            } ?: sendFailureMessage("数据格式异常")
+        } ?: sendFailureMessage("无图像数据")
     }
     }
 
 
     /**
     /**
      * 发送错误消息事件
      * 发送错误消息事件
      * @param error String 错误消息
      * @param error String 错误消息
      */
      */
-    private fun sendFailureMessage(error:String){
-        CrApplication.getEventBus().post(EventCommon<String>(CrCommonAction.VIDEO_SAVE_YUV_FAILURE,error))
+    private fun sendFailureMessage(error: String) {
+        CrApplication.getEventBus()
+            .post(EventCommon<String>(CrCommonAction.VIDEO_SAVE_YUV_FAILURE, error))
     }
     }
 
 
     /**
     /**
      * 发送YUA数据保存成功消息
      * 发送YUA数据保存成功消息
      * @param fileName String 保存的文件名称
      * @param fileName String 保存的文件名称
      */
      */
-    private fun sendSaveYUVSuccessMessage(fileName:String){
-        CrApplication.getEventBus().post(EventCommon<String>(CrCommonAction.VIDEO_SAVE_YUV_SUCCESS,fileName))
+    private fun sendSaveYUVSuccessMessage(fileName: String) {
+        CrApplication.getEventBus()
+            .post(EventCommon<String>(CrCommonAction.VIDEO_SAVE_YUV_SUCCESS, fileName))
     }
     }
 
 
     /**
     /**
@@ -475,45 +538,53 @@ class FragmentFPV : CrAnimationFragment(), SurfaceHolder.Callback {
             height
             height
         )
         )
     }
     }
+
     /**
     /**
      * 保存图片
      * 保存图片
      * @param buf ByteArray 数据
      * @param buf ByteArray 数据
      * @param width Int 宽度
      * @param width Int 宽度
      * @param height Int 高度
      * @param height Int 高度
      */
      */
-    private fun screenShot(buf:ByteArray,width:Int,height:Int){
+    private fun screenShot(buf: ByteArray, width: Int, height: Int) {
         // todo: 2023/8/7 获取图片
         // todo: 2023/8/7 获取图片
-        val yuvImage = YuvImage(buf,ImageFormat.NV21,width,height,null)
+        val yuvImage = YuvImage(buf, ImageFormat.NV21, width, height, null)
         // todo: 2023/8/7 保存文件
         // todo: 2023/8/7 保存文件
-        val outputFile:OutputStream
+        val outputFile: OutputStream
         var fileName = "uav_${CrUnitManager.toSystemDate()}.jpg"
         var fileName = "uav_${CrUnitManager.toSystemDate()}.jpg"
         var path = "${CrUtil.IMAGE_PATH}${fileName}"
         var path = "${CrUtil.IMAGE_PATH}${fileName}"
-        outputFile = try{
+        outputFile = try {
             FileOutputStream(File(path))
             FileOutputStream(File(path))
-        }catch (e:FileNotFoundException){
+        } catch (e: FileNotFoundException) {
             sendFailureMessage("异常:${e.message}")
             sendFailureMessage("异常:${e.message}")
             return
             return
         }
         }
         // todo: 2023/8/7 压缩保存图片
         // todo: 2023/8/7 压缩保存图片
-        yuvImage.compressToJpeg(Rect(0,0,width,height),100,outputFile)
-        try{
+        yuvImage.compressToJpeg(Rect(0, 0, width, height), 100, outputFile)
+        try {
             outputFile.close()
             outputFile.close()
             // todo: 2023/8/7 发送正确消息
             // todo: 2023/8/7 发送正确消息
             sendSaveYUVSuccessMessage(fileName)
             sendSaveYUVSuccessMessage(fileName)
-        }catch (e:IOException){
+        } catch (e: IOException) {
             sendFailureMessage("IO错误:${e.message}")
             sendFailureMessage("IO错误:${e.message}")
         }
         }
     }
     }
 
 
+    /**
+     * 设置画布尺寸变化
+     */
+    fun setSurfaceViewSizeChange() {
+        var width = surfaceView.width
+        var height = surfaceView.height
+        CrUtil.print("宽度:${width} 高度:${height}")
+    }
+
     // todo: 2023/8/7 订阅事件
     // todo: 2023/8/7 订阅事件
     @Subscribe
     @Subscribe
-    fun onChanged(event:EventCommon<String>){
-        event?.let {
-            when(it.action){
-                CrCommonAction.VIDEO_SAVE_YUV->{
-                    // todo: 2023/8/7 切换到YUV模式
-                    handlerYUV(false)
-                }
+    fun onChanged(event: EventCommon<String>) {
+        when (event.action) {
+            CrCommonAction.VIDEO_SAVE_YUV -> {
+                // todo: 2023/8/7 切换到YUV模式
+                handlerYUV(false)
             }
             }
         }
         }
     }
     }

+ 1 - 1
app/src/main/java/com/cr/pages/FragmentLayerControl.kt

@@ -56,7 +56,7 @@ class FragmentLayerControl :CrNavigationFragment(){
      * 初始化页面
      * 初始化页面
      */
      */
     override fun initPage() {
     override fun initPage() {
-        var adapter = LayerAdapter(context!!,LayerManager.getInstance().getLayers()!!)
+        var adapter = LayerAdapter(CrApplication.getContext(),LayerManager.getInstance().getLayers()!!)
         listView?.adapter = adapter
         listView?.adapter = adapter
     }
     }
 
 

+ 451 - 0
app/src/main/java/com/cr/pages/FragmentLiveStream.kt

@@ -0,0 +1,451 @@
+package com.cr.pages
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.fragment.app.activityViewModels
+import com.bigkoo.pickerview.adapter.ArrayWheelAdapter
+import com.contrarywind.listener.OnItemSelectedListener
+import com.cr.common.CrColorManager
+import com.cr.common.CrLiveManager
+import com.cr.common.CrSaveManager
+import com.cr.common.CrUnitManager
+import com.cr.cruav.CrApplication
+import com.cr.cruav.R
+import com.cr.data.CrUtil
+import com.cr.dialog.DialogInput
+import com.cr.event.BarAction
+import com.cr.event.CrCommonAction
+import com.cr.event.EventCommon
+import com.cr.event.EventFragmentBarAction
+import com.cr.models.CompletionModel
+import com.cr.models.ICompletion
+import com.cr.view.CrViewWheel
+import com.cr.viewmodel.CrLiveStreamVM
+import dji.v5.common.video.channel.VideoChannelState
+
+/**
+ * 操作系统:MAC系统
+ * 创建者:王成
+ * 创建日期:2023/9/12 13:50
+ * 描述:直播页面
+ */
+class FragmentLiveStream : CrNavigationFragment(), View.OnClickListener {
+    // todo: 2023/9/12 控件定义
+    private var wheelLiveQuality: CrViewWheel? = null  // define: 2023/9/12 直播品质选择器
+    private var wheelLiveUrl: CrViewWheel? = null  // define: 2023/9/12 直播地址选择器
+    private var wheelLiveChannelType: CrViewWheel? = null // define: 2023/9/12 直播通道选择器
+    private var btnActionAppendUrl: LinearLayout? = null // define: 2023/9/12 添加直播地址按钮
+    private var btnActionQuality:LinearLayout?=null // define: 2023/9/14 视频质量
+    private var btnActionChannelType:LinearLayout?= null // define: 2023/9/14 设置视频通道
+    private var lblInfo: TextView? = null  // define: 2023/9/13 直播动态信息
+    private var btnStartLive: Button? = null // define: 2023/9/13 直播开始
+    private var btnStopLive: Button? = null // define: 2023/9/13 停止直播
+
+
+    // todo: 2023/9/12 定义变量
+    private var adapterWheelLiveQuality: ArrayWheelAdapter<String>? = null // define: 2023/9/12 适配器
+    private var adapterWheelLiveUrl: ArrayWheelAdapter<String>? = null  // define: 2023/9/12 适配器
+    private var adapterWheelChannelType: ArrayWheelAdapter<String>? = null // define: 2023/9/12 适配器
+    private var itemsLiveQuality: MutableList<String>? = null  // define: 2023/9/12 直播视频质量数据集
+    private var itemsLiveUrl: MutableList<String>? = null  // define: 2023/9/12 直播地址数据集
+    private var itemsLiveChannelType: MutableList<String>? = null  // define: 2023/9/12 直播通道选择集
+
+    private var indexLiveQuality:Int = -1  // define: 2023/9/13 直播质量当前索引
+    private var indexLiveChannelType:Int = -1  // define: 2023/9/13 直播通道当前索引
+    private var indexLiveUrl:Int = -1  // define: 2023/9/13 直播地址当前索引
+
+    // todo: 2023/9/13 动态显示的直播信息
+    private var isStreaming: String = "否"  // define: 2023/9/13 是否正在直播
+    private var fps: Int = 0   // define: 2023/9/13 帧率
+    private var vbps: Int = 0  // define: 2023/9/13 码率
+    private var resolutionWidth: Int = 0  // define: 2023/9/13 分辨率宽
+    private var resolutionHeight: Int = 0  // define: 2023/9/13 分辨率高
+    private var packetLoss: Int = 0  // define: 2023/9/13 丢包率
+    private var rtt: Int = 0  // define: 2023/9/13 延迟时间
+    private var error: String = "" // define: 2023/9/13 错误信息
+
+
+    // define: 2023/9/12 绑定直播模型
+    private val liveStreamVm: CrLiveStreamVM by activityViewModels()
+
+    /**
+     * 初始化
+     * @param inflater LayoutInflater
+     * @param container ViewGroup?
+     * @param savedInstanceState Bundle?
+     * @return View?
+     */
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        // todo: 2023/9/13 设置动画方向
+        setAnimationDirection(AnimationDirection.RIGHT)
+        self = this
+        mainView = inflater.inflate(R.layout.frag_live_stream, null)
+        // todo: 2023/9/13 设置导航栏
+        setBar(R.id.nv)
+        // todo: 2023/9/12 挂载控件
+        joinControls()
+        // todo: 2023/9/12 初始化页面
+        initPage()
+        return mainView
+    }
+
+    /**
+     * 挂载控件
+     */
+    override fun joinControls() {
+        super.joinControls()
+        mainView?.let {
+            // todo: 2023/9/12 挂接直播地址选择器
+            wheelLiveUrl = it.findViewById(R.id.wheel_live_url)
+            CrUnitManager.setWheel(wheelLiveUrl!!, wheelLiveUrlListener)
+            // todo: 2023/9/12 挂接直播视频质量选择器
+            wheelLiveQuality = it.findViewById(R.id.wheel_live_quality)
+            CrUnitManager.setWheel(wheelLiveQuality!!, wheelLiveQualityListener)
+            // todo: 2023/9/12 挂接直播视频通道选择器
+            wheelLiveChannelType = it.findViewById(R.id.wheel_live_channel_type)
+            CrUnitManager.setWheel(wheelLiveChannelType!!, wheelLiveChannelTypeListener)
+            // todo: 2023/9/12 挂载添加直播地址按钮
+            btnActionAppendUrl = it.findViewById(R.id.btn_append_url)
+            btnActionAppendUrl?.setOnClickListener(this)
+            // todo: 2023/9/13 挂接直播动态信息
+            lblInfo = it.findViewById(R.id.live_info)
+            // todo: 2023/9/13 挂接直播操作按钮
+            btnStartLive = it.findViewById(R.id.btn_start_live)
+            btnStartLive?.setOnClickListener(this)
+            btnStopLive = it.findViewById(R.id.btn_stop_live)
+            btnStopLive?.setOnClickListener(this)
+            btnActionQuality = it.findViewById(R.id.btn_set_quality)
+            btnActionQuality?.setOnClickListener(this)
+            btnActionChannelType = it.findViewById(R.id.btn_set_channel_type)
+            btnActionChannelType?.setOnClickListener(this)
+        }
+    }
+
+    /**
+     * 初始化页面
+     */
+    override fun initPage() {
+        super.initPage()
+        // todo: 2023/9/12 初始化直播视频质量选择器
+        itemsLiveQuality = mutableListOf()
+        adapterWheelLiveQuality = ArrayWheelAdapter(itemsLiveQuality)
+        wheelLiveQuality?.adapter = adapterWheelLiveQuality
+        syncWheelLiveQuality()
+
+        // todo: 2023/9/13 初始化直播地址选择器
+        itemsLiveUrl = mutableListOf()
+        adapterWheelLiveUrl = ArrayWheelAdapter(itemsLiveUrl)
+        wheelLiveUrl?.adapter = adapterWheelLiveUrl
+        syncWheelUrl()
+
+        // todo: 2023/9/13 初始化直播通道选择器
+        itemsLiveChannelType = mutableListOf()
+        adapterWheelChannelType = ArrayWheelAdapter(itemsLiveChannelType)
+        wheelLiveChannelType?.adapter = adapterWheelChannelType
+        syncWheelChannelType()
+
+        // todo: 2023/9/13 订阅
+        initObserve()
+
+        // todo: 2023/9/13 初始化直播信息
+        clearLiveStreamInfo()
+    }
+
+    /**
+     * 订阅
+     */
+    private fun initObserve() {
+        // todo: 2023/9/13 注册动态信息监听
+        liveStreamVm.streamStatusInfo.observe(requireActivity()) {
+            it?.let {
+                isStreaming = if (it.isStreaming) "是" else "否"
+                fps = it.fps
+                vbps = it.vbps
+                resolutionWidth = it.videoWidth
+                resolutionHeight = it.videoHeight
+                packetLoss = it.packetLoss
+                rtt = it.rtt
+                activeStreamInfo()
+            }
+        }
+        // todo: 2023/9/13 注册错误监听
+        liveStreamVm.streamErrorInfo.observe(requireActivity()) {
+            it?.let {
+                error = it.description()
+                activeStreamInfo()
+            }
+        }
+    }
+
+    /**
+     * 同步视频质量
+     */
+    private fun syncWheelLiveQuality() {
+        itemsLiveQuality?.clear()
+        var qualityTitleList = CrLiveManager.getInstance().getQualityList()
+        for (title in qualityTitleList) {
+            itemsLiveQuality?.add(title)
+        }
+        wheelLiveQuality?.setSelectItem(itemsLiveQuality!![0])
+        wheelLiveQuality?.onItemSelected()
+    }
+
+    /**
+     * 同步码流通道
+     */
+    private fun syncWheelChannelType() {
+        itemsLiveChannelType?.clear()
+        var channelTypeTitleList = CrLiveManager.getInstance().getChannelTypeList()
+        for (title in channelTypeTitleList) {
+            itemsLiveChannelType?.add(title)
+        }
+        wheelLiveChannelType?.setSelectItem(itemsLiveChannelType!![0])
+        wheelLiveChannelType?.onItemSelected()
+    }
+
+    /**
+     * 同步直播地址
+     */
+    private fun syncWheelUrl() {
+        itemsLiveUrl?.clear()
+        var urlList = CrSaveManager.getInstance().getLiveUrlList()
+        for (title in urlList) {
+            itemsLiveUrl?.add(title)
+        }
+        wheelLiveUrl?.setSelectItem(itemsLiveUrl!![itemsLiveUrl!!.size - 1])
+        wheelLiveUrl?.onItemSelected()
+    }
+
+    /**
+     * 直播URL地址选择器监听
+     */
+    private val wheelLiveUrlListener = OnItemSelectedListener { index ->
+        indexLiveUrl = index
+    }
+
+    /**
+     * 直播画面品质选择器监听
+     */
+    private val wheelLiveQualityListener = OnItemSelectedListener { index ->
+        indexLiveQuality = index
+    }
+
+    /**
+     * 直播视频通道选择器监听
+     */
+    private val wheelLiveChannelTypeListener = OnItemSelectedListener { index ->
+        indexLiveChannelType = index
+    }
+
+    /**
+     * 显示追加直播地址对话框
+     */
+    private fun showAppendLiveUrlDialog() {
+        var dialog = DialogInput(CrApplication.getContext())
+        dialog.setTitle("输入直播地址")
+        dialog.setLabels("直播地址", null)
+        dialog.setButtonsText("设置", "关闭")
+        dialog.setHints("输入直播地址rtmp://...", "")
+        dialog.setContent("rtmp://", "")
+        dialog.setListener(dialogInputListener)
+        dialog.show()
+    }
+
+    /**
+     * 添加直播地址对话框监听
+     */
+    private val dialogInputListener = object : DialogInput.DialogInputListener {
+        // todo: 2023/9/12 完成
+        override fun completion(valueOne: String, valueTwo: String, self: DialogInput) {
+            CrSaveManager.getInstance().saveLiveUrl(valueOne)
+            syncWheelUrl()
+        }
+
+        // todo: 2023/9/12 关闭
+        override fun close() {
+
+        }
+    }
+
+    /**
+     * 恢复初始状态
+     */
+    private fun clearLiveStreamInfo() {
+        fps = -1
+        vbps = -1
+        isStreaming = "否"
+        resolutionWidth = -1
+        resolutionHeight = -1
+        packetLoss = -1
+        rtt = -1
+        error = ""
+        lblInfo?.setTextColor(CrColorManager.getColor(R.color.live_normal))
+        updateLiveStreamInfo()
+    }
+
+    /**
+     * 激活直播信息
+     */
+    private fun activeStreamInfo() {
+        lblInfo?.setTextColor(CrColorManager.getColor(R.color.live_active))
+        updateLiveStreamInfo()
+    }
+
+    /**
+     * 更新信息展示
+     */
+    private fun updateLiveStreamInfo() {
+        val liveStreamInfo = "是否直播: $isStreaming \n" +
+                "帧率: ${fps}fps \n" +
+                "码率: ${vbps}Kbps \n" +
+                "分辨率: $resolutionWidth * $resolutionHeight \n" +
+                "丢包率: ${packetLoss}% \n" +
+                "延迟: ${rtt}ms \n" +
+                "错误: $error"
+        lblInfo?.text = liveStreamInfo
+    }
+
+    /**
+     * 开启直播
+     */
+    private fun startLive() {
+        // todo: 2023/9/13 添加监听
+        liveStreamVm.addStreamStatusListener()
+        // todo: 2023/9/13 获取相关信息
+        wheelLiveQuality?.setSelectItem(
+            CrLiveManager.getInstance().getQualityTitle(liveStreamVm.getLiveStreamQuality())
+        )
+        wheelLiveChannelType?.setSelectItem(
+            CrLiveManager.getInstance().getChannelTypeTitle(liveStreamVm.getVideoChannel())
+        )
+    }
+
+    // todo: 2023/9/14 操作
+    /**
+     * 开始直播
+     */
+    private fun actionStartLive(){
+        // todo: 2023/9/13 判断是否选择了直播地址
+        if(indexLiveUrl == -1){
+            showWarning("请选择直播地址后重试!")
+            return
+        }
+        // todo: 2023/9/13 判断视频通道是否已开启
+        if(!liveStreamVm.channelTypeIsOpen()){
+            showWarning("请开启视频通道后重试!")
+            return
+        }
+        // todo: 2023/9/13 设置直播地址
+        liveStreamVm.setRTMStreamSettings(itemsLiveUrl!![indexLiveUrl])
+        // todo: 2023/9/13 开始直播
+        liveStreamVm.startStream(object : ICompletion<String> {
+            override fun onCompletion(completion: CompletionModel<String>) {
+                if (completion.isSuccess == true) {
+                    showToast("直播开启成功!")
+                    startLive()
+                    // todo: 2023/9/13 发送隐藏直播信息展示组件
+                    CrApplication.getEventBus().post(EventCommon(CrCommonAction.LIVE_INFO_STATE,true))
+                } else {
+                    showError(completion.result!!)
+                }
+            }
+        })
+    }
+
+    /**
+     * 停止直播
+     */
+    private fun actionStopLive(){
+        liveStreamVm.stopStream(object : ICompletion<String> {
+            override fun onCompletion(completion: CompletionModel<String>) {
+                if (completion.isSuccess == true) {
+                    showToast("直播已停止")
+                } else {
+                    showError(completion.result!!)
+                }
+                // todo: 2023/9/13 初始化信息 移除监听
+                clearLiveStreamInfo()
+                liveStreamVm.removeStreamStatusListener()
+                // todo: 2023/9/13 发送隐藏直播信息展示组件
+                CrApplication.getEventBus().post(EventCommon(CrCommonAction.LIVE_INFO_STATE,false))
+            }
+        })
+    }
+
+    /**
+     * 设置视频质量
+     */
+    private fun actionSetQuality(){
+        // todo: 2023/9/14 判断是否正在直播
+        if(!liveStreamVm.streamStatusInfo?.value!!.isStreaming){
+            showWarning("未直播,无法设置!")
+        }else{
+            var quality = CrLiveManager.getInstance().getQuality(itemsLiveQuality!![indexLiveQuality])
+            liveStreamVm.setLiveStreamQuality(quality)
+        }
+    }
+
+    /**
+     * 设置直播通道
+     */
+    private fun actionSetChannelType(){
+        // todo: 2023/9/14 判断是否正在直播
+        if(!liveStreamVm.streamStatusInfo?.value!!.isStreaming){
+            showWarning("未直播,无法设置!")
+        }else{
+            // todo: 2023/9/14 判断视频流是否开启
+            var channelType = CrLiveManager.getInstance().getChannelType(itemsLiveChannelType!![indexLiveChannelType])
+            if(liveStreamVm.getChannelStatus(channelType) != VideoChannelState.ON){
+                showWarning("视频通道未开启,无法设置!")
+            }else{
+                liveStreamVm.setVideoChannel(channelType)
+            }
+        }
+    }
+
+
+    /**
+     * 覆写点击事件
+     * @param view View
+     */
+    override fun onClick(view: View?) {
+        when (view?.id) {
+            R.id.btn_append_url -> {
+                // todo: 2023/9/12 添加直播地址
+                showAppendLiveUrlDialog()
+            }
+            R.id.btn_start_live -> {
+                // todo: 2023/9/14 开始直播
+                actionStartLive()
+            }
+            R.id.btn_stop_live -> {
+                // todo: 2023/9/14 停止直播
+               actionStopLive()
+            }
+            R.id.btn_set_quality->{
+                // todo: 2023/9/14 设置直播画质
+                actionSetQuality()
+            }
+            R.id.btn_set_channel_type->{
+                // todo: 2023/9/14 设置视频通道
+                actionSetChannelType()
+            }
+        }
+    }
+
+    /**
+     * 覆写关闭方法
+     */
+    override fun dismiss() {
+        CrApplication.getEventBus().post(EventFragmentBarAction(self!!, BarAction.ACTION_DISMISS))
+    }
+}

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

@@ -108,27 +108,27 @@ class FragmentMap : CrAnimationFragment() {
 
 
     // todo: 2023/4/14 字段相关常量
     // todo: 2023/4/14 字段相关常量
     companion object {
     companion object {
-        val FIELD_NET_CASE_POLYGON_AJH = "AJH"   //  define: 2023/4/13 网络案件的案件编号
-        val FIELD_CASE_POLYGON_ANJID = "ANJID"   //  define: 2023/4/13 案件ID
-        val FIELD_CASE_POLYGON_MJ = "MJ"   //  define: 2023/4/13 面积
-        val FIELD_CASE_POLYGON_BZ = "BZ"   //  define: 2023/4/13 违建面的类型 0--代表未匹配 1--代表已匹配
-        val FIELD_CASE_POLYGON_BZ_VALUE_NO = "0"   //  define: 2023/4/13 未匹配
-        val FIELD_CASE_POLYGON_BZ_VALUE_YES = "1"   //  define: 2023/4/13 已匹配
-        val FIELD_CASE_NAME = "NAME"   //  define: 2023/4/13 名称字段
-        val FIELD_CASE_IMAGES = "IMAGES"   //  define: 2023/4/13 图片集合字段
-        val FIELD_CASE_RQ = "RQ"   //  define: 2023/4/13 日期字段
-        val FIELD_CASE_LAT = "LAT"   //  define: 2023/4/13 纬度字段
-        val FIELD_CASE_LNG = "LNG"   //  define: 2023/4/13 经度字段
-        val FIELD_CASE_ISUP = "ISUP"   //  define: 2023/4/13 是否上传
-        val FIELD_CASE_ALT = "ALT"  //  define: 2023/4/13 高度字段
-        val FIELD_CASE_ANG = "ANG"   //  define: 2023/4/13 角度字段
-        val FIELD_CASE_TYPE = "TYPE"   //  define: 2023/4/13 类型字段
-        val FIELD_CASE_ISDOWN = "ISDOWN"   //  define: 2023/4/13 是否下载
-
-        val MEDIA_TYPE_WJ_NO = "0"  // define: 2023/6/12 未上传的案件点
-        val MEDIA_TYPE_WJ_YES = "1" // define: 2023/6/12 已上传的案件点
-        val MEDIA_TYPE_REPEAT_NO = "4" // define: 2023/6/12 未上传的复飞点
-        val MEDIA_TYPE_REPEAT_YES = "5" // define: 2023/6/12 已上传的复飞点
+        const val FIELD_NET_CASE_POLYGON_AJH = "AJH"   //  define: 2023/4/13 网络案件的案件编号
+        const val FIELD_CASE_POLYGON_ANJID = "ANJID"   //  define: 2023/4/13 案件ID
+        const val FIELD_CASE_POLYGON_MJ = "MJ"   //  define: 2023/4/13 面积
+        const val FIELD_CASE_POLYGON_BZ = "BZ"   //  define: 2023/4/13 违建面的类型 0--代表未匹配 1--代表已匹配
+        const val FIELD_CASE_POLYGON_BZ_VALUE_NO = "0"   //  define: 2023/4/13 未匹配
+        const val FIELD_CASE_POLYGON_BZ_VALUE_YES = "1"   //  define: 2023/4/13 已匹配
+        const val FIELD_CASE_NAME = "NAME"   //  define: 2023/4/13 名称字段
+        const val FIELD_CASE_IMAGES = "IMAGES"   //  define: 2023/4/13 图片集合字段
+        const val FIELD_CASE_RQ = "RQ"   //  define: 2023/4/13 日期字段
+        const val FIELD_CASE_LAT = "LAT"   //  define: 2023/4/13 纬度字段
+        const val FIELD_CASE_LNG = "LNG"   //  define: 2023/4/13 经度字段
+        const val FIELD_CASE_ISUP = "ISUP"   //  define: 2023/4/13 是否上传
+        const val FIELD_CASE_ALT = "ALT"  //  define: 2023/4/13 高度字段
+        const val FIELD_CASE_ANG = "ANG"   //  define: 2023/4/13 角度字段
+        const val FIELD_CASE_TYPE = "TYPE"   //  define: 2023/4/13 类型字段
+        const val FIELD_CASE_ISDOWN = "ISDOWN"   //  define: 2023/4/13 是否下载
+
+        const val MEDIA_TYPE_WJ_NO = "0"  // define: 2023/6/12 未上传的案件点
+        const val MEDIA_TYPE_WJ_YES = "1" // define: 2023/6/12 已上传的案件点
+        const val MEDIA_TYPE_REPEAT_NO = "4" // define: 2023/6/12 未上传的复飞点
+        const val MEDIA_TYPE_REPEAT_YES = "5" // define: 2023/6/12 已上传的复飞点
     }
     }
 
 
     // todo: 2023/4/13 标志相关
     // todo: 2023/4/13 标志相关
@@ -239,9 +239,10 @@ class FragmentMap : CrAnimationFragment() {
             mapTouch = MapTouch(CrApplication.getContext(), itView)
             mapTouch = MapTouch(CrApplication.getContext(), itView)
             mapTouch?.setListener(touchListener)
             mapTouch?.setListener(touchListener)
             itView.onTouchListener = mapTouch
             itView.onTouchListener = mapTouch
+            // todo: 2023/4/13 地图初始化
+            mapInit()
         }
         }
-        // todo: 2023/4/13 地图初始化
-        mapInit()
+
     }
     }
 
 
     /**
     /**
@@ -377,8 +378,8 @@ class FragmentMap : CrAnimationFragment() {
      * 地图初始化
      * 地图初始化
      */
      */
     private fun mapInit() {
     private fun mapInit() {
-        // todo: 2023/4/13 加载在线底图
-        addOnlineRasterLayer()
+        // todo: 2023/4/13 加载在线底图 9月13日 注释 主要原因是加上后打不开地图
+        //addOnlineRasterLayer()
         // todo: 2023/4/13 添加切片图层
         // todo: 2023/4/13 添加切片图层
         addTiledToMap()
         addTiledToMap()
         // todo: 2023/4/13 添加基础矢量地图
         // todo: 2023/4/13 添加基础矢量地图

+ 4 - 4
app/src/main/java/com/cr/pages/FragmentSet.kt

@@ -35,10 +35,10 @@ class FragmentSet : CrNavigationFragment() {
     private var adapter:CrPageAdapter? = null
     private var adapter:CrPageAdapter? = null
 
 
     // define: 2023/4/11 定义页面
     // define: 2023/4/11 定义页面
-    private var pageMain:FragmentSetMain? = null
-    private var pageSetIpAndCom:FragmentSetIpAndCom? = null
-    private var pageDownloadData:FragmentSetDataDownload? = null
-    private var pageSimulator:FragmentSimulator?= null
+    private var pageMain:FragmentSetMain? = null   // define: 2023/9/12 主页面
+    private var pageSetIpAndCom:FragmentSetIpAndCom? = null    // define: 2023/9/12 设置Ip和端口页面
+    private var pageDownloadData:FragmentSetDataDownload? = null   // define: 2023/9/12 下载数据页面
+    private var pageSimulator:FragmentSimulator?= null   // define: 2023/9/12 模拟器页面
 
 
     // define: 2023/4/11 定义控件
     // define: 2023/4/11 定义控件
     private var viewPager: ViewPager2?=null
     private var viewPager: ViewPager2?=null

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

@@ -66,20 +66,20 @@ open class FragmentTopInfo : CrFragment() {
     )
     )
 
 
     // todo: 2023/8/17 定义控件
     // todo: 2023/8/17 定义控件
-    private var lblSDKVersion:TextView?=null
-    private var lblGPSCount:TextView?= null
-    private var lblRCConnection:TextView?=null
-    private var lblProductName:TextView?=null
-    private var lblIMUState:TextView?=null
-    private var lblLongitude:TextView?=null
-    private var lblLatitude:TextView?=null
-    private var lblSignal:TextView?= null
-    private var imgSignal:ImageView?=null
-    private var lblRCBattery:TextView?=null
-    private var imgRCBattery:ImageView?=null
-    private var lblFCBattery:TextView?=null
-    private var imgFCBattery:ImageView?=null
-    private var lblCompassState:TextView?=null
+    private var lblSDKVersion:TextView?=null  // define: 2023/9/14 SDK版本信息
+    private var lblGPSCount:TextView?= null   // define: 2023/9/14 GPS卫星数量
+    private var lblRCConnection:TextView?=null  // define: 2023/9/14 遥控器连接状态
+    private var lblProductName:TextView?=null  // define: 2023/9/14 连接产品名称
+    private var lblIMUState:TextView?=null   // define: 2023/9/14 IMU状态
+    private var lblLongitude:TextView?=null   // define: 2023/9/14 当前经度
+    private var lblLatitude:TextView?=null   // define: 2023/9/14 当前纬度
+    private var lblSignal:TextView?= null  // define: 2023/9/14 信号质量
+    private var imgSignal:ImageView?=null  // define: 2023/9/14 信号质量图
+    private var lblRCBattery:TextView?=null  // define: 2023/9/14 遥控器电池电量
+    private var imgRCBattery:ImageView?=null   // define: 2023/9/14 遥控器电池电量图
+    private var lblFCBattery:TextView?=null   // define: 2023/9/14 飞行器电池电量
+    private var imgFCBattery:ImageView?=null   // define: 2023/9/14 飞行器电池电量符号
+    private var lblCompassState:TextView?=null  // define: 2023/9/14 指南针状态
 
 
 
 
     /**
     /**

+ 0 - 1
app/src/main/java/com/cr/pages/FragmentUploadAction.kt

@@ -391,7 +391,6 @@ class FragmentUploadAction : CrNavigationFragment(), View.OnClickListener {
                     DialogLoadingUtil.dismiss()
                     DialogLoadingUtil.dismiss()
                     showInformation(message)
                     showInformation(message)
                 }
                 }
-
             })
             })
     }
     }
 
 

+ 242 - 0
app/src/main/java/com/cr/viewmodel/CrLiveStreamVM.kt

@@ -0,0 +1,242 @@
+package com.cr.viewmodel
+
+import androidx.lifecycle.MutableLiveData
+import com.cr.common.CrLiveManager
+import com.cr.common.CrLiveStreamStateInfo
+import com.cr.data.CrUtil
+import com.cr.models.CompletionModel
+import com.cr.models.ICompletion
+import dji.v5.common.callback.CommonCallbacks
+import dji.v5.common.error.IDJIError
+import dji.v5.common.video.channel.VideoChannelState
+import dji.v5.common.video.channel.VideoChannelType
+import dji.v5.manager.datacenter.MediaDataCenter
+import dji.v5.manager.datacenter.livestream.*
+import dji.v5.manager.datacenter.livestream.settings.RtmpSettings
+import dji.v5.manager.datacenter.video.VideoStreamManager
+import dji.v5.manager.interfaces.ILiveStreamManager
+
+/**
+ * 操作系统:MAC系统
+ * 创建者:王成
+ * 创建日期:2023/9/11 17:25
+ * 描述:直播模型
+ */
+class CrLiveStreamVM : CrViewModel(){
+    // todo: 2023/9/12 直播视频流状态信息
+    var streamStatusInfo = MutableLiveData<CrLiveStreamStateInfo>()
+
+    // todo: 2023/9/12 错误信息
+    var streamErrorInfo = MutableLiveData<IDJIError>()
+
+    // todo: 2023/9/12 监听
+    private var liveStreamStatusListener:LiveStreamStatusListener? = null
+
+    // todo: 2023/9/12 视频直播管理器
+    private val liveStreamManager:ILiveStreamManager = MediaDataCenter.getInstance().liveStreamManager
+
+    /**
+     * 初始化
+     */
+    init {
+        streamStatusInfo.postValue(CrLiveStreamStateInfo(false))
+        // todo: 2023/9/12 重置初始化赋值
+        reset()
+        // todo: 2023/9/12 初始化监听
+        liveStreamStatusListener = object:LiveStreamStatusListener{
+            // todo: 2023/9/12 状态监听
+            override fun onLiveStreamStatusUpdate(status: LiveStreamStatus?) {
+                status?.let {
+                    streamStatusInfo.value?.isStreaming = status.isStreaming
+                    streamStatusInfo.value?.fps = status.fps
+                    streamStatusInfo.value?.vbps = status.vbps
+                    streamStatusInfo.value?.rtt = status.rtt
+                    streamStatusInfo.value?.packetLoss = status.packetLoss
+                    streamStatusInfo.value?.packetCacheLength = status.packetCacheLen
+                    // todo: 2023/9/14 获取视频流质量
+                    var resolution = CrLiveManager.getInstance().getQualityToVideoResolution(getLiveStreamQuality())
+                    streamStatusInfo.value?.videoWidth = resolution[0]
+                    streamStatusInfo.value?.videoHeight = resolution[1]
+                    refresh(streamStatusInfo)
+                }
+            }
+
+            // todo: 2023/9/12 错误监听
+            override fun onError(error: IDJIError?) {
+                error?.let {
+                    streamErrorInfo.postValue(it)
+                }
+            }
+        }
+    }
+
+    /**
+     * 重写reset方法
+     */
+    override fun reset() {
+        streamStatusInfo.value?.let {
+            it.isStreaming = false
+            it.fps = 0
+            it.vbps = 0
+            it.packetLoss = 0
+            it.packetCacheLength = 0
+            it.rtt = 0
+            refresh(streamStatusInfo)
+        }
+    }
+
+    /**
+     * 销毁后调用
+     */
+    override fun onCleared() {
+        super.onCleared()
+        removeStreamStatusListener()
+    }
+
+    /**
+     * 添加直播流状态监听
+     */
+    fun addStreamStatusListener(){
+        liveStreamManager.addLiveStreamStatusListener(liveStreamStatusListener)
+    }
+
+    /**
+     * 移除直播流状态监听
+     */
+    fun removeStreamStatusListener(){
+        liveStreamManager.removeLiveStreamStatusListener(liveStreamStatusListener)
+    }
+
+    /**
+     * 设置直播管理器
+     * @param liveStreamSettings LiveStreamSettings
+     */
+    private fun setLiveStreamSettings(liveStreamSettings:LiveStreamSettings){
+        liveStreamManager.liveStreamSettings = liveStreamSettings
+    }
+
+    /**
+     * 开始直播
+     * @param callback ICompletion<String>
+     */
+    fun startStream(callback:ICompletion<String>?){
+        liveStreamManager.startStream(object:CommonCallbacks.CompletionCallback{
+            // todo: 2023/9/12 成功
+            override fun onSuccess() {
+                callback.let { it?.onCompletion(CompletionModel(true,"")) }
+            }
+
+            // todo: 2023/9/12 失败
+            override fun onFailure(error: IDJIError) {
+                callback.let { it?.onCompletion(CompletionModel(false,error.description())) }
+            }
+
+        })
+    }
+
+    /**
+     * 停止直播
+     * @param callback ICompletion<String>
+     */
+    fun stopStream(callback: ICompletion<String>?){
+        liveStreamManager.stopStream(object:CommonCallbacks.CompletionCallback{
+            // todo: 2023/9/12 成功
+            override fun onSuccess() {
+                callback.let { it?.onCompletion(CompletionModel(true,"")) }
+            }
+
+            // todo: 2023/9/12 失败
+            override fun onFailure(error: IDJIError) {
+                callback.let { it?.onCompletion(CompletionModel(false,error.description())) }
+            }
+        })
+    }
+
+
+    /**
+     * 设置直播流
+     */
+    fun setRTMStreamSettings(rtmpURL:String){
+        // todo: 2023/9/12 创建配置
+        val liveStreamRtmpSettings = LiveStreamSettings.Builder()
+            .setLiveStreamType(LiveStreamType.RTMP)
+            .setRtmpSettings(RtmpSettings.Builder().setUrl(rtmpURL).build())
+            .build()
+        // todo: 2023/9/12 设置配置
+        setLiveStreamSettings(liveStreamRtmpSettings)
+    }
+
+    /**
+     * 设置直播视频通道
+     * @param videoChannel VideoChannelType
+     */
+    fun setVideoChannel(videoChannel: VideoChannelType){
+        liveStreamManager.videoChannelType = videoChannel
+    }
+
+    /**
+     * 获取直播视频通道
+     * @return VideoChannelType
+     */
+    fun getVideoChannel():VideoChannelType{
+        return liveStreamManager.videoChannelType
+    }
+
+    /**
+     * 设置直播视频质量
+     * @param liveStreamQuality StreamQuality
+     */
+    fun setLiveStreamQuality(liveStreamQuality:StreamQuality){
+        liveStreamManager.liveStreamQuality = liveStreamQuality
+    }
+
+    /**
+     * 获取直播视频质量
+     * @return StreamQuality
+     */
+    fun getLiveStreamQuality():StreamQuality{
+        return liveStreamManager.liveStreamQuality
+    }
+
+    /**
+     * 设置直播视频码率模式
+     * @param bitRateMode LiveVideoBitrateMode
+     */
+    fun setLiveVideoBitRateMode(bitRateMode: LiveVideoBitrateMode){
+        liveStreamManager.liveVideoBitrateMode = bitRateMode
+    }
+
+    /**
+     * 获取直播视频码率模式
+     * @return LiveVideoBitrateMode
+     */
+    fun getLiveVideoBitReteMode():LiveVideoBitrateMode{
+        return liveStreamManager?.liveVideoBitrateMode
+    }
+
+    /**
+     * 设置直播视频码率
+     * @param bitrate Int
+     */
+    fun setLiveVideoBitRate(bitrate: Int) {
+        liveStreamManager.liveVideoBitrate = bitrate
+    }
+
+    /**
+     * 判断直播选择的视频通道是否开启
+     * @return Boolean
+     */
+    fun channelTypeIsOpen():Boolean{
+         return getChannelStatus(liveStreamManager.videoChannelType) == VideoChannelState.ON
+    }
+
+    /**
+     * 获取通道状态
+     * @param channel VideoChannelType
+     * @return VideoChannelState
+     */
+    fun getChannelStatus(channel: VideoChannelType): VideoChannelState {
+        return VideoStreamManager.getInstance().getAvailableVideoChannel(channel)
+            ?.let { it.videoChannelStatus } ?: let { VideoChannelState.CLOSE }
+    }
+}

+ 14 - 2
app/src/main/java/com/cr/viewmodel/CrMSDKRegisterVM.kt

@@ -4,6 +4,8 @@ import android.content.Context
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.MutableLiveData
 import com.cr.common.CrMSDKRegister
 import com.cr.common.CrMSDKRegister
 import com.cr.cruav.R
 import com.cr.cruav.R
+import com.cr.data.CrUtil
+import com.cr.data.DEFAULT_STR
 import com.cr.data.REGISTER_FAILED
 import com.cr.data.REGISTER_FAILED
 import com.cr.models.CrMSDKInfoModel
 import com.cr.models.CrMSDKInfoModel
 import dji.v5.common.error.IDJIError
 import dji.v5.common.error.IDJIError
@@ -31,7 +33,7 @@ class CrMSDKRegisterVM:CrViewModel() {
      * 初始化
      * 初始化
      */
      */
     init {
     init {
-        registerInfo.value = CrMSDKRegister(REGISTER_FAILED)
+        registerInfo.value = CrMSDKRegister(DEFAULT_STR)
         registerInfo.value?.isRegister = false
         registerInfo.value?.isRegister = false
     }
     }
 
 
@@ -52,7 +54,7 @@ class CrMSDKRegisterVM:CrViewModel() {
         SDKManager.getInstance().init(context,object :SDKManagerCallback{
         SDKManager.getInstance().init(context,object :SDKManagerCallback{
             // TODO: 注册成功 
             // TODO: 注册成功 
             override fun onRegisterSuccess() {
             override fun onRegisterSuccess() {
-                registerInfo.value?.registerInfo = msdkInfoModel.getSDKVersion();//StringUtils.getResStr(ContextUtil.getContext(), R.string.registered)
+                registerInfo.value?.registerInfo = msdkInfoModel.getSDKVersion()
                 registerInfo.value?.isRegister = true;
                 registerInfo.value?.isRegister = true;
                 refreshRegisterInfo()
                 refreshRegisterInfo()
                 callback.onRegisterSuccess()
                 callback.onRegisterSuccess()
@@ -97,4 +99,14 @@ class CrMSDKRegisterVM:CrViewModel() {
 
 
         })
         })
     }
     }
+
+    /**
+     * 释放注册资源
+     */
+    fun destroyMSDK(){
+        // todo: 2023/9/15 此方法很重要
+        // todo: 2023/9/15 通过代码方式重启系统或无人机连接时会导致重新加载
+        // todo: 2023/9/15 调用此方法可保证DJI将重新注册
+        SDKManager.getInstance().destroy()
+    }
 }
 }

+ 4 - 0
app/src/main/java/com/cr/widget/CrLinearLayoutWidget.kt

@@ -1,6 +1,8 @@
 package com.cr.widget
 package com.cr.widget
 
 
 import android.content.Context
 import android.content.Context
+import android.os.Handler
+import android.os.Looper
 import android.util.AttributeSet
 import android.util.AttributeSet
 import android.view.View
 import android.view.View
 import android.widget.LinearLayout
 import android.widget.LinearLayout
@@ -16,6 +18,8 @@ open class CrLinearLayoutWidget@JvmOverloads constructor(
     attrs: AttributeSet?=null,
     attrs: AttributeSet?=null,
     defStyleAttr: Int = 0
     defStyleAttr: Int = 0
 ):LinearLayout(context,attrs,defStyleAttr) {
 ):LinearLayout(context,attrs,defStyleAttr) {
+    // define: 2023/9/14 主线程
+    protected var mainHandler = Handler(Looper.getMainLooper())
     // define: 2023/4/14 主视图
     // define: 2023/4/14 主视图
     protected var mainView:View ?= null
     protected var mainView:View ?= null
     /**
     /**

+ 140 - 0
app/src/main/java/com/cr/widget/CrLiveInfoWidget.kt

@@ -0,0 +1,140 @@
+package com.cr.widget
+
+import android.content.Context
+import android.opengl.Visibility
+import android.util.AttributeSet
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.cr.common.CrLiveStreamStateInfo
+import com.cr.common.CrTimerManager
+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.squareup.otto.Subscribe
+
+/**
+ * 操作系统:MAC系统
+ * 创建者:王成
+ * 创建日期:2023/9/13 14:37
+ * 描述:直播信息组件
+ */
+class CrLiveInfoWidget @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet?=null,
+    defStyleAttr: Int = 0
+):CrLinearLayoutWidget(context,attrs,defStyleAttr){
+    // todo: 2023/9/13 组件定义
+    private var liveInfoFps: TextView?= null  // define: 2023/9/13 帧率
+    private var liveInfoVbps: TextView?= null // define: 2023/9/13 码率
+    private var liveInfoResolution: TextView?=null // define: 2023/9/13 分辨率
+    private var liveInfoPacket: TextView?=null // define: 2023/9/13 丢包率
+    private var liveInfoRtt: TextView?=null // define: 2023/9/13 延迟
+    private var liveTimer:TextView?= null // define: 2023/9/14 直播计时
+    private var liveInfoPanel:LinearLayout?= null // define: 2023/9/13 直播信息容器
+    /**
+     * 初始化
+     */
+    init {
+        // todo: 2023/9/13 创建视图
+        initView(R.layout.wg_live)
+        // todo: 2023/9/13 挂载控件
+        joinControls()
+    }
+
+    /**
+     * 覆写挂载控件
+     */
+    override fun joinControls() {
+        liveInfoFps = findViewById(R.id.live_info_fps)
+        liveInfoVbps = findViewById(R.id.live_info_vbps)
+        liveInfoResolution = findViewById(R.id.live_info_resolution)
+        liveInfoPacket = findViewById(R.id.live_info_packet)
+        liveInfoRtt = findViewById(R.id.live_info_rtt)
+        liveTimer = findViewById(R.id.live_info_timer)
+        liveInfoPanel = findViewById(R.id.live_info_panel)
+        liveInfoPanel?.visibility = GONE
+    }
+
+    /**
+     * 生命周期
+     * 该方法当View及其子View从XML文件中加载完成后触发调用
+     */
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+    }
+
+    /**
+     * 生命周期
+     * 当前View或其祖先的可见性改变时被调用
+     * @param changedView View
+     * @param visibility Int
+     */
+    override fun onVisibilityChanged(changedView: View, visibility: Int) {
+        super.onVisibilityChanged(changedView, visibility)
+    }
+
+    /**
+     * 生命周期
+     * 当View被附着到一个窗口时触发
+     */
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        // todo: 2023/9/13 注册订阅
+        CrApplication.getEventBus().register(this)
+    }
+
+    /**
+     * 案件生命周期
+     * 当View离开附着的窗口时触发
+     * Activity调用onDestroy方法时View就会离开窗口
+     */
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        // todo: 2023/9/13 解除订阅
+        CrApplication.getEventBus().unregister(this)
+    }
+
+    /**
+     * 更新直播信息
+     * @param event EventCommon<CrLiveStreamStateInfo>
+     */
+    @Subscribe
+    fun onUpdate(event:EventCommon<CrLiveStreamStateInfo>){
+        if(event.action == CrCommonAction.UPDATE_LIVE_INFO){
+            liveInfoFps?.text = "${event.param?.fps}f"
+            liveInfoVbps?.text = "${event.param?.vbps}k"
+            liveInfoResolution?.text = "${event.param?.videoWidth}*${event.param?.videoHeight}"
+            liveInfoPacket?.text = "${event.param?.packetLoss}%"
+            liveInfoRtt?.text = "${event.param?.rtt}ms"
+        }
+    }
+
+    /**
+     * 订阅显示或隐藏
+     * @param event EventCommon<Boolean>
+     */
+    @Subscribe
+    fun onShowStatus(event:EventCommon<Boolean>){
+        if(event.action == CrCommonAction.LIVE_INFO_STATE){
+            if(event.param == true){
+                liveInfoPanel?.visibility = VISIBLE
+                // todo: 2023/9/14 设置计时器
+                CrTimerManager.getInstance().startTimer(1,object:CrTimerManager.ITimerCountListener{
+                    // todo: 2023/9/14 计时回调
+                    override fun onTimer(count: Long, timeValue: String) {
+                        mainHandler.post{
+                            liveTimer?.text = timeValue
+                        }
+                    }
+                })
+            }else{
+                liveInfoPanel?.visibility = GONE
+                // todo: 2023/9/14 关闭计时器
+                CrTimerManager.getInstance().stopTimer()
+            }
+        }
+    }
+}

+ 2 - 2
app/src/main/res/drawable/btn_tools_normal.xml

@@ -1,14 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android">
 <shape xmlns:android="http://schemas.android.com/apk/res/android">
     <!--填充色-->
     <!--填充色-->
-    <solid android:color="@color/tools_button_back" />
+    <solid android:color="#00FF0000" />
     <!--描边-->
     <!--描边-->
     <stroke
     <stroke
         android:width="0dp"
         android:width="0dp"
         android:color="#df4249" />
         android:color="#df4249" />
     <!-- 圆角 -->
     <!-- 圆角 -->
     <corners
     <corners
-        android:radius="@dimen/cr_100_dp"/>
+        android:radius="0dp"/>
     <padding
     <padding
         android:left="0dp"
         android:left="0dp"
         android:right="0dp"
         android:right="0dp"

+ 1 - 1
app/src/main/res/drawable/btn_tools_select.xml

@@ -8,7 +8,7 @@
         android:color="#736864" />
         android:color="#736864" />
     <!-- 圆角 -->
     <!-- 圆角 -->
     <corners
     <corners
-        android:radius="@dimen/cr_100_dp"/>
+        android:radius="0dp"/>
     <padding
     <padding
         android:left="0dp"
         android:left="0dp"
         android:right="0dp"
         android:right="0dp"

BIN
app/src/main/res/drawable/ico_fps.png


BIN
app/src/main/res/drawable/ico_packet.png


BIN
app/src/main/res/drawable/ico_resolution.png


BIN
app/src/main/res/drawable/ico_rtt.png


BIN
app/src/main/res/drawable/ico_timer.png


BIN
app/src/main/res/drawable/ico_vbps.png


+ 15 - 3
app/src/main/res/layout/av_main.xml

@@ -26,8 +26,8 @@
         android:background="@color/cardview_shadow_start_color"/>
         android:background="@color/cardview_shadow_start_color"/>
     <!--FPV容器-->
     <!--FPV容器-->
     <FrameLayout
     <FrameLayout
-        android:layout_width="@dimen/cr_180_dp"
-        android:layout_height="@dimen/cr_160_dp"
+        android:layout_width="@dimen/cr_200_dp"
+        android:layout_height="@dimen/cr_140_dp"
         android:id="@+id/av_fragment_fpv"
         android:id="@+id/av_fragment_fpv"
         app:layout_constraintBottom_toTopOf="@id/av_main_panel_bottom"
         app:layout_constraintBottom_toTopOf="@id/av_main_panel_bottom"
         app:layout_constraintRight_toRightOf="parent"
         app:layout_constraintRight_toRightOf="parent"
@@ -96,12 +96,24 @@
     <LinearLayout
     <LinearLayout
         android:layout_width="wrap_content"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginRight="@dimen/common_padding"
+        android:layout_marginEnd="@dimen/common_padding"
         android:layout_marginTop="@dimen/common_padding"
         android:layout_marginTop="@dimen/common_padding"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintTop_toBottomOf="@id/av_fragment_top">
         app:layout_constraintTop_toBottomOf="@id/av_fragment_top">
         <include layout="@layout/tools_top"/>
         <include layout="@layout/tools_top"/>
     </LinearLayout>
     </LinearLayout>
+    <!--顶部直播信息条-->
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/cr_34_dp"
+        android:layout_marginStart="@dimen/common_padding"
+        android:layout_marginTop="@dimen/common_padding"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/av_fragment_top">
+        <com.cr.widget.CrLiveInfoWidget
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+    </LinearLayout>
     <!--右侧弹窗容器-->
     <!--右侧弹窗容器-->
     <FrameLayout
     <FrameLayout
         style="@style/common_match_panel_v"
         style="@style/common_match_panel_v"

+ 3 - 3
app/src/main/res/layout/dig_input.xml

@@ -6,7 +6,7 @@
     android:layout_height="match_parent"
     android:layout_height="match_parent"
     android:gravity="center">
     android:gravity="center">
     <LinearLayout
     <LinearLayout
-        android:layout_width="@dimen/cr_180_dp"
+        android:layout_width="@dimen/cr_240_dp"
         android:layout_height="wrap_content"
         android:layout_height="wrap_content"
         android:background="@drawable/shape_back_dialog_normal"
         android:background="@drawable/shape_back_dialog_normal"
         android:orientation="vertical"
         android:orientation="vertical"
@@ -77,13 +77,13 @@
                 android:background="@drawable/btn_red_selector"
                 android:background="@drawable/btn_red_selector"
                 android:text="@string/dig_normal_btn_ok"
                 android:text="@string/dig_normal_btn_ok"
                 android:id="@+id/dig_btn_ok"
                 android:id="@+id/dig_btn_ok"
-                android:layout_width="@dimen/cr_40_dp"/>
+                android:layout_width="@dimen/cr_60_dp"/>
             <Button
             <Button
                 style="@style/btn_default"
                 style="@style/btn_default"
                 android:background="@drawable/btn_blue_selector"
                 android:background="@drawable/btn_blue_selector"
                 android:text="@string/dig_normal_btn_close"
                 android:text="@string/dig_normal_btn_close"
                 android:id="@+id/dig_btn_cancel"
                 android:id="@+id/dig_btn_cancel"
-                android:layout_width="@dimen/cr_40_dp"/>
+                android:layout_width="@dimen/cr_60_dp"/>
         </LinearLayout>
         </LinearLayout>
     </LinearLayout>
     </LinearLayout>
 </LinearLayout>
 </LinearLayout>

+ 13 - 10
app/src/main/res/layout/frag_fpv.xml

@@ -1,31 +1,34 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:orientation="vertical"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:layout_height="match_parent"
-    android:id="@+id/fpv_layout"
+    android:id="@+id/fpv_panel"
     android:background="@drawable/shape_circle"
     android:background="@drawable/shape_circle"
     android:clipToOutline="true">
     android:clipToOutline="true">
     <SurfaceView
     <SurfaceView
         android:layout_width="match_parent"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_height="match_parent"
-        android:id="@+id/surface_fpv"/>
+        android:id="@+id/surface_fpv"
+        tools:ignore="MissingConstraints" />
+    <!--FPV视频信息-->
     <TextView
     <TextView
+        android:id="@+id/fpv_info"
         android:layout_width="match_parent"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="****"
+        android:layout_height="@dimen/cr_24_dp"
+        android:background="@color/fpv_video_info_back"
         android:gravity="center"
         android:gravity="center"
-        android:id="@+id/fpv_info"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:textColor="@color/blue"
-        android:textSize="@dimen/sp_14"/>
+        android:text="@string/fpv_video_info"
+        android:textColor="@color/white"
+        android:textSize="@dimen/sp_12"
+        app:layout_constraintBottom_toBottomOf="parent" />
     <Button
     <Button
         android:layout_width="wrap_content"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_height="wrap_content"
         android:visibility="gone"
         android:visibility="gone"
-        android:text="切换通道"
+        android:text="@string/fpv_action_trigger_channel_type"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintTop_toTopOf="parent"
         android:id="@+id/fpv_trigger"/>
         android:id="@+id/fpv_trigger"/>

+ 136 - 0
app/src/main/res/layout/frag_live_stream.xml

@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/shape_back_fragment">
+    <com.cr.widget.CrNavigationBarWidget
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:crTitle="@string/nv_title_live"
+        app:isGotoBack="false"
+        app:isDismiss="true"
+        android:id="@+id/nv"
+        android:layout_weight="0"/>
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/cr_10_dp"
+        android:layout_marginTop="@dimen/cr_5_dp"
+        android:layout_marginBottom="@dimen/cr_5_dp">
+        <Button
+            style="@style/btn_default"
+            android:text="@string/live_action_start"
+            android:layout_weight="1"
+            android:id="@+id/btn_start_live"/>
+        <Button
+            style="@style/btn_default"
+            android:text="@string/live_action_stop"
+            android:layout_weight="1"
+            android:background="@drawable/btn_set_selector"
+            android:id="@+id/btn_stop_live"/>
+    </LinearLayout>
+    <View style="@style/view_split_h1"/>
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+            <RelativeLayout
+                style="@style/live_head_main">
+                <TextView
+                    style="@style/live_head_title"
+                    android:text="@string/live_title_url"/>
+                <LinearLayout
+                    style="@style/live_head_button_panel"
+                    android:id="@+id/btn_append_url">
+                    <com.cr.view.CrICON
+                        style="@style/live_head_button_icon"
+                        app:cr_font="@string/ico_add"/>
+                    <TextView
+                        style="@style/live_head_button_label"
+                        android:text="@string/live_action_append"/>
+                </LinearLayout>
+            </RelativeLayout>
+            <View style="@style/view_split_h1"/>
+            <LinearLayout
+                style="@style/live_wheel_panel">
+                <com.cr.view.CrViewWheel
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:id="@+id/wheel_live_url"/>
+            </LinearLayout>
+            <View style="@style/view_split_h1"/>
+            <RelativeLayout
+                style="@style/live_head_main">
+                <TextView
+                    style="@style/live_head_title"
+                    android:text="@string/live_title_quality"/>
+                <LinearLayout
+                    style="@style/live_head_button_panel"
+                    android:id="@+id/btn_set_quality">
+                    <com.cr.view.CrICON
+                        style="@style/live_head_button_icon"
+                        app:cr_font="@string/ico_set"/>
+                    <TextView
+                        style="@style/live_head_button_label"
+                        android:text="@string/live_action_set"/>
+                </LinearLayout>
+            </RelativeLayout>
+            <View style="@style/view_split_h1"/>
+            <LinearLayout
+                style="@style/live_wheel_panel">
+                <com.cr.view.CrViewWheel
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:id="@+id/wheel_live_quality"/>
+            </LinearLayout>
+            <View style="@style/view_split_h1"/>
+            <RelativeLayout
+                style="@style/live_head_main">
+                <TextView
+                    style="@style/live_head_title"
+                    android:text="@string/live_title_channel_type"/>
+                <LinearLayout
+                    style="@style/live_head_button_panel"
+                    android:id="@+id/btn_set_channel_type">
+                    <com.cr.view.CrICON
+                        style="@style/live_head_button_icon"
+                        app:cr_font="@string/ico_set"/>
+                    <TextView
+                        style="@style/live_head_button_label"
+                        android:text="@string/live_action_set"/>
+                </LinearLayout>
+            </RelativeLayout>
+            <View style="@style/view_split_h1"/>
+            <LinearLayout
+                style="@style/live_wheel_panel">
+                <com.cr.view.CrViewWheel
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:id="@+id/wheel_live_channel_type"/>
+            </LinearLayout>
+            <View style="@style/view_split_h1"/>
+            <RelativeLayout
+                style="@style/live_head_main">
+                <TextView
+                    style="@style/live_head_title"
+                    android:text="@string/live_title_info"/>
+            </RelativeLayout>
+            <View style="@style/view_split_h1"/>
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/cr_100_dp"
+                android:layout_marginLeft="@dimen/cr_10_dp"
+                android:layout_marginRight="@dimen/cr_10_dp"
+                android:layout_marginBottom="@dimen/cr_10_dp"
+                android:textColor="@color/white"
+                android:textSize="@dimen/sp_10"
+                android:id="@+id/live_info"
+                android:lineSpacingExtra="@dimen/cr_3_dp"/>
+        </LinearLayout>
+    </ScrollView>
+</LinearLayout>

+ 20 - 1
app/src/main/res/layout/tools_top.xml

@@ -2,7 +2,8 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="horizontal"
     android:orientation="horizontal"
     android:layout_width="wrap_content"
     android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
+    android:layout_height="wrap_content"
+    android:background="@drawable/shape_circle">
     <LinearLayout
     <LinearLayout
         style="@style/tools_top_panel"
         style="@style/tools_top_panel"
         android:id="@+id/tools_append_point_from_uav">
         android:id="@+id/tools_append_point_from_uav">
@@ -10,6 +11,7 @@
             style="@style/tools_image"
             style="@style/tools_image"
             android:src="@drawable/ico_location"/>
             android:src="@drawable/ico_location"/>
     </LinearLayout>
     </LinearLayout>
+    <View style="@style/view_split_v1"/>
     <LinearLayout
     <LinearLayout
         style="@style/tools_top_panel"
         style="@style/tools_top_panel"
         android:id="@+id/tools_append_point_from_map">
         android:id="@+id/tools_append_point_from_map">
@@ -17,6 +19,7 @@
             style="@style/tools_image"
             style="@style/tools_image"
             android:src="@drawable/ico_airstate"/>
             android:src="@drawable/ico_airstate"/>
     </LinearLayout>
     </LinearLayout>
+    <View style="@style/view_split_v1"/>
     <LinearLayout
     <LinearLayout
         style="@style/tools_top_panel"
         style="@style/tools_top_panel"
         android:id="@+id/tools_case_tools">
         android:id="@+id/tools_case_tools">
@@ -24,6 +27,7 @@
             style="@style/tools_image"
             style="@style/tools_image"
             android:src="@drawable/ico_case_tools"/>
             android:src="@drawable/ico_case_tools"/>
     </LinearLayout>
     </LinearLayout>
+    <View style="@style/view_split_v1"/>
     <LinearLayout
     <LinearLayout
         style="@style/tools_top_panel"
         style="@style/tools_top_panel"
         android:id="@+id/tools_case_local">
         android:id="@+id/tools_case_local">
@@ -31,6 +35,7 @@
             style="@style/tools_image"
             style="@style/tools_image"
             android:src="@drawable/ico_identify_localcase"/>
             android:src="@drawable/ico_identify_localcase"/>
     </LinearLayout>
     </LinearLayout>
+    <View style="@style/view_split_v1"/>
     <LinearLayout
     <LinearLayout
         style="@style/tools_top_panel"
         style="@style/tools_top_panel"
         android:id="@+id/tools_case_net">
         android:id="@+id/tools_case_net">
@@ -38,6 +43,7 @@
             style="@style/tools_image"
             style="@style/tools_image"
             android:src="@drawable/ico_identify_netcase"/>
             android:src="@drawable/ico_identify_netcase"/>
     </LinearLayout>
     </LinearLayout>
+    <View style="@style/view_split_v1"/>
     <LinearLayout
     <LinearLayout
         style="@style/tools_top_panel"
         style="@style/tools_top_panel"
         android:id="@+id/tools_wx">
         android:id="@+id/tools_wx">
@@ -45,6 +51,7 @@
             style="@style/tools_image"
             style="@style/tools_image"
             android:src="@drawable/tools_weixin_close"/>
             android:src="@drawable/tools_weixin_close"/>
     </LinearLayout>
     </LinearLayout>
+    <View style="@style/view_split_v1"/>
     <LinearLayout
     <LinearLayout
         style="@style/tools_top_panel"
         style="@style/tools_top_panel"
         android:id="@+id/tools_tools">
         android:id="@+id/tools_tools">
@@ -52,6 +59,7 @@
             style="@style/tools_image"
             style="@style/tools_image"
             android:src="@drawable/tools_tools"/>
             android:src="@drawable/tools_tools"/>
     </LinearLayout>
     </LinearLayout>
+    <View style="@style/view_split_v1"/>
     <LinearLayout
     <LinearLayout
         style="@style/tools_top_panel"
         style="@style/tools_top_panel"
         android:id="@+id/tools_doodle">
         android:id="@+id/tools_doodle">
@@ -59,6 +67,7 @@
             style="@style/tools_image"
             style="@style/tools_image"
             android:src="@drawable/tools_ty"/>
             android:src="@drawable/tools_ty"/>
     </LinearLayout>
     </LinearLayout>
+    <View style="@style/view_split_v1"/>
     <LinearLayout
     <LinearLayout
         style="@style/tools_top_panel"
         style="@style/tools_top_panel"
         android:id="@+id/tools_mark">
         android:id="@+id/tools_mark">
@@ -66,6 +75,7 @@
             style="@style/tools_image"
             style="@style/tools_image"
             android:src="@drawable/tools_startico"/>
             android:src="@drawable/tools_startico"/>
     </LinearLayout>
     </LinearLayout>
+    <View style="@style/view_split_v1"/>
     <LinearLayout
     <LinearLayout
         style="@style/tools_top_panel"
         style="@style/tools_top_panel"
         android:id="@+id/tools_layer">
         android:id="@+id/tools_layer">
@@ -73,6 +83,15 @@
             style="@style/tools_image"
             style="@style/tools_image"
             android:src="@drawable/tools_layer"/>
             android:src="@drawable/tools_layer"/>
     </LinearLayout>
     </LinearLayout>
+    <View style="@style/view_split_v1"/>
+    <LinearLayout
+        style="@style/tools_top_panel"
+        android:id="@+id/tools_live">
+        <ImageView
+            style="@style/tools_image"
+            android:src="@drawable/tools_camera_startvideo"/>
+    </LinearLayout>
+    <View style="@style/view_split_v1"/>
     <LinearLayout
     <LinearLayout
         style="@style/tools_top_panel"
         style="@style/tools_top_panel"
         android:id="@+id/tools_set">
         android:id="@+id/tools_set">

+ 102 - 0
app/src/main/res/layout/wg_live.xml

@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/shape_circle">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/cr_34_dp"
+        android:id="@+id/live_info_panel">
+        <!--帧率-->
+        <LinearLayout
+            style="@style/dji_live_component">
+            <LinearLayout
+                style="@style/dji_top_component_lbl_panel">
+                <TextView
+                    style="@style/dji_top_text_title_style"
+                    android:text="@string/title_live_fps"/>
+                <TextView
+                    style="@style/dji_top_text_value_style"
+                    android:text="@string/default_value"
+                    android:id="@+id/live_info_fps"/>
+            </LinearLayout>
+        </LinearLayout>
+        <View style="@style/view_split_v1"/>
+        <!--码率-->
+        <LinearLayout
+            style="@style/dji_live_component">
+            <LinearLayout
+                style="@style/dji_top_component_lbl_panel">
+                <TextView
+                    style="@style/dji_top_text_title_style"
+                    android:text="@string/title_live_vbps"/>
+                <TextView
+                    style="@style/dji_top_text_value_style"
+                    android:text="@string/default_value"
+                    android:id="@+id/live_info_vbps"/>
+            </LinearLayout>
+        </LinearLayout>
+        <View style="@style/view_split_v1"/>
+        <!--分辨率-->
+        <LinearLayout
+            style="@style/dji_live_component"
+            android:layout_width="@dimen/cr_70_dp">
+            <LinearLayout
+                style="@style/dji_top_component_lbl_panel">
+                <TextView
+                    style="@style/dji_top_text_title_style"
+                    android:text="@string/title_live_resolution"/>
+                <TextView
+                    style="@style/dji_top_text_value_style"
+                    android:text="@string/default_value"
+                    android:id="@+id/live_info_resolution"/>
+            </LinearLayout>
+        </LinearLayout>
+        <View style="@style/view_split_v1"/>
+        <!--丢包率-->
+        <LinearLayout
+            style="@style/dji_live_component">
+            <LinearLayout
+                style="@style/dji_top_component_lbl_panel">
+                <TextView
+                    style="@style/dji_top_text_title_style"
+                    android:text="@string/title_live_rtt"/>
+                <TextView
+                    style="@style/dji_top_text_value_style"
+                    android:text="@string/default_value"
+                    android:id="@+id/live_info_packet"/>
+            </LinearLayout>
+        </LinearLayout>
+        <View style="@style/view_split_v1"/>
+        <!--延迟-->
+        <LinearLayout
+            style="@style/dji_live_component">
+            <LinearLayout
+                style="@style/dji_top_component_lbl_panel">
+                <TextView
+                    style="@style/dji_top_text_title_style"
+                    android:text="@string/title_live_packet"/>
+                <TextView
+                    style="@style/dji_top_text_value_style"
+                    android:text="@string/default_value"
+                    android:id="@+id/live_info_rtt"/>
+            </LinearLayout>
+        </LinearLayout>
+        <View style="@style/view_split_v1"/>
+        <!--计时-->
+        <LinearLayout
+            style="@style/dji_live_component"
+            android:layout_width="@dimen/cr_60_dp">
+            <LinearLayout
+                style="@style/dji_top_component_lbl_panel">
+                <TextView
+                    style="@style/dji_top_text_title_style"
+                    android:text="@string/title_live_timer"/>
+                <TextView
+                    style="@style/dji_top_text_value_style"
+                    android:text="@string/value_camera_video"
+                    android:id="@+id/live_info_timer"/>
+            </LinearLayout>
+        </LinearLayout>
+    </LinearLayout>
+</LinearLayout>

+ 9 - 0
app/src/main/res/values/colors.xml

@@ -31,6 +31,15 @@
 
 
     <!--速度视图-->
     <!--速度视图-->
     <color name="progress">#c2f571</color>
     <color name="progress">#c2f571</color>
+    <!--工具容器样式-->
+    <color name="tools_panel">#D00a3474</color>
+
+    <!--直播信息颜色-->
+    <color name="live_normal">#5F9EA0</color>
+    <color name="live_active">#F8F732</color>
+
+    <!--FPV颜色-->
+    <color name="fpv_video_info_back">#A02F4F4F</color>
 
 
     <!--常用颜色-->
     <!--常用颜色-->
     <color name="white">#FFFFFF</color>
     <color name="white">#FFFFFF</color>

+ 2 - 0
app/src/main/res/values/dimens.xml

@@ -462,6 +462,8 @@
     <dimen name="cr_n8_dp">-8dp</dimen>
     <dimen name="cr_n8_dp">-8dp</dimen>
 
 
     <!-- 字体大小 -->
     <!-- 字体大小 -->
+    <dimen name="sp_4">4sp</dimen>
+    <dimen name="sp_5">5sp</dimen>
     <dimen name="sp_6">6sp</dimen>
     <dimen name="sp_6">6sp</dimen>
     <dimen name="sp_7">7sp</dimen>
     <dimen name="sp_7">7sp</dimen>
     <dimen name="sp_8">8sp</dimen>
     <dimen name="sp_8">8sp</dimen>

+ 26 - 3
app/src/main/res/values/strings.xml

@@ -21,6 +21,12 @@
     <string name="title_altitude" translatable="false">相对航高</string>
     <string name="title_altitude" translatable="false">相对航高</string>
     <string name="title_go_home_distance" translatable="false">返航距离</string>
     <string name="title_go_home_distance" translatable="false">返航距离</string>
     <string name="title_go_home_altitude" translatable="false">返航高度</string>
     <string name="title_go_home_altitude" translatable="false">返航高度</string>
+    <string name="title_live_fps">帧率</string>
+    <string name="title_live_vbps">码率</string>
+    <string name="title_live_rtt">丢包</string>
+    <string name="title_live_resolution">分辨率</string>
+    <string name="title_live_packet">延迟</string>
+    <string name="title_live_timer">计时</string>
     <!--相机-->
     <!--相机-->
     <string name="title_camera_iso" translatable="false">ISO</string>
     <string name="title_camera_iso" translatable="false">ISO</string>
     <string name="title_camera_speed" translatable="false">快门速度</string>
     <string name="title_camera_speed" translatable="false">快门速度</string>
@@ -62,8 +68,8 @@
     <string name="login_btn_login">登录</string>
     <string name="login_btn_login">登录</string>
     <string name="login_btn_set">设置</string>
     <string name="login_btn_set">设置</string>
     <string name="login_save_userandpass">记住用户名和密码</string>
     <string name="login_save_userandpass">记住用户名和密码</string>
-    <string name="switch_on_text"></string>
-    <string name="switch_off_text"></string>
+    <string name="switch_on_text"></string>
+    <string name="switch_off_text"></string>
 
 
     <!--设置IP地址和端口页面-->
     <!--设置IP地址和端口页面-->
     <string name="frm_ic_hint_ip">Ip 000.000.000.000</string>
     <string name="frm_ic_hint_ip">Ip 000.000.000.000</string>
@@ -84,7 +90,8 @@
     <string name="nv_title_image_editor">图片编辑</string>
     <string name="nv_title_image_editor">图片编辑</string>
     <string name="nv_title_set_ip_and_com">设置IP和COM</string>
     <string name="nv_title_set_ip_and_com">设置IP和COM</string>
     <string name="nv_title_dji_initialize">飞行器自检</string>
     <string name="nv_title_dji_initialize">飞行器自检</string>
-    <string name="nv_title_simulator">UAV模拟器</string>
+    <string name="nv_title_simulator">飞行模拟器</string>
+    <string name="nv_title_live">直播相关</string>
 
 
     <!--对话框-->
     <!--对话框-->
     <string name="dig_normal_btn_ok">确定</string>
     <string name="dig_normal_btn_ok">确定</string>
@@ -195,6 +202,20 @@
     <string name="up_attribute_case_type_async">同步案件类型</string>
     <string name="up_attribute_case_type_async">同步案件类型</string>
     <string name="up_action_submit">上传</string>
     <string name="up_action_submit">上传</string>
 
 
+    <!--直播相关-->
+    <string name="live_title_quality">直播画质</string>
+    <string name="live_title_channel_type">直播通道</string>
+    <string name="live_title_url">直播地址</string>
+    <string name="live_action_start">开启直播</string>
+    <string name="live_action_stop">停止直播</string>
+    <string name="live_action_append">添加地址</string>
+    <string name="live_action_set">设置</string>
+    <string name="live_title_info">直播动态信息</string>
+
+    <!--FPV相关-->
+    <string name="fpv_action_trigger_channel_type">切换视频通道</string>
+    <string name="fpv_video_info">帧率:30 宽:1280 高:960</string>
+
     <!--字体-->
     <!--字体-->
     <string name="ico_nv_left" translatable="false">&#xe60b;</string>
     <string name="ico_nv_left" translatable="false">&#xe60b;</string>
     <string name="ico_nv_right" translatable="false">&#xe81a;</string>
     <string name="ico_nv_right" translatable="false">&#xe81a;</string>
@@ -214,5 +235,7 @@
     <string name="ico_move">&#xe8c9;</string>
     <string name="ico_move">&#xe8c9;</string>
     <string name="ico_recover">&#xe672;</string>
     <string name="ico_recover">&#xe672;</string>
     <string name="ico_reset">&#xe6cb;</string>
     <string name="ico_reset">&#xe6cb;</string>
+    <string name="ico_add">&#xeb8c;</string>
+    <string name="ico_set">&#xeb90;</string>
 
 
 </resources>
 </resources>

+ 59 - 5
app/src/main/res/values/themes.xml

@@ -7,7 +7,7 @@
         <item name="colorPrimaryVariant">@color/purple_700</item>
         <item name="colorPrimaryVariant">@color/purple_700</item>
         <item name="colorOnPrimary">@color/white</item>
         <item name="colorOnPrimary">@color/white</item>
         <!-- Status bar color. -->
         <!-- Status bar color. -->
-<!--        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>-->
+        <!--        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>-->
         <!-- Customize your theme here. -->
         <!-- Customize your theme here. -->
         <item name="android:fontFamily">@font/adr</item>
         <item name="android:fontFamily">@font/adr</item>
     </style>
     </style>
@@ -69,6 +69,11 @@
         <item name="android:gravity">center_vertical|center_horizontal</item>
         <item name="android:gravity">center_vertical|center_horizontal</item>
     </style>
     </style>
 
 
+    <!--大疆直播视图容器-->
+    <style name="dji_live_component" parent="dji_top_component">
+        <item name="android:layout_width">@dimen/cr_40_dp</item>
+    </style>
+
     <!--大疆顶部视图-组件-图片样式-->
     <!--大疆顶部视图-组件-图片样式-->
     <style name="dji_top_component_img">
     <style name="dji_top_component_img">
         <item name="android:layout_width">@dimen/cr_30_dp</item>
         <item name="android:layout_width">@dimen/cr_30_dp</item>
@@ -225,6 +230,7 @@
         <item name="android:layout_marginRight">@dimen/common_margin</item>
         <item name="android:layout_marginRight">@dimen/common_margin</item>
         <item name="android:textColor">@color/yellow</item>
         <item name="android:textColor">@color/yellow</item>
     </style>
     </style>
+
     <style name="lbl_value_common">
     <style name="lbl_value_common">
         <item name="android:layout_width">wrap_content</item>
         <item name="android:layout_width">wrap_content</item>
         <item name="android:layout_height">wrap_content</item>
         <item name="android:layout_height">wrap_content</item>
@@ -259,13 +265,13 @@
         <item name="android:layout_height">@dimen/cr_34_dp</item>
         <item name="android:layout_height">@dimen/cr_34_dp</item>
         <item name="android:padding">@dimen/cr_8_dp</item>
         <item name="android:padding">@dimen/cr_8_dp</item>
         <item name="android:background">@drawable/btn_tools_selector</item>
         <item name="android:background">@drawable/btn_tools_selector</item>
-        <item name="android:layout_marginTop">@dimen/common_padding</item>
         <item name="android:gravity">center</item>
         <item name="android:gravity">center</item>
     </style>
     </style>
     <!--工具外框样式-->
     <!--工具外框样式-->
     <style name="tools_top_panel" parent="tools_left_panel">
     <style name="tools_top_panel" parent="tools_left_panel">
-        <item name="android:layout_marginLeft">@dimen/cr_8_dp</item>
+        <item name="android:layout_marginLeft">0dp</item>
     </style>
     </style>
+
     <!--工具图片样式-->
     <!--工具图片样式-->
     <style name="tools_image">
     <style name="tools_image">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_width">match_parent</item>
@@ -284,12 +290,14 @@
         <item name="android:gravity">center</item>
         <item name="android:gravity">center</item>
         <item name="android:orientation">vertical</item>
         <item name="android:orientation">vertical</item>
     </style>
     </style>
+
     <style name="button_image">
     <style name="button_image">
         <item name="android:layout_width">@dimen/cr_20_dp</item>
         <item name="android:layout_width">@dimen/cr_20_dp</item>
         <item name="android:layout_height">@dimen/cr_20_dp</item>
         <item name="android:layout_height">@dimen/cr_20_dp</item>
         <item name="android:scaleType">fitCenter</item>
         <item name="android:scaleType">fitCenter</item>
         <item name="background">@drawable/shape_image_circle</item>
         <item name="background">@drawable/shape_image_circle</item>
     </style>
     </style>
+
     <style name="button_title">
     <style name="button_title">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">wrap_content</item>
         <item name="android:layout_height">wrap_content</item>
@@ -389,6 +397,7 @@
         <item name="android:textSize">@dimen/sp_10</item>
         <item name="android:textSize">@dimen/sp_10</item>
         <item name="android:layout_weight">1</item>
         <item name="android:layout_weight">1</item>
     </style>
     </style>
+
     <style name="lbl_value_common_match" parent="lbl_value_common">
     <style name="lbl_value_common_match" parent="lbl_value_common">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">match_parent</item>
         <item name="android:layout_height">match_parent</item>
@@ -401,6 +410,7 @@
         <item name="android:layout_marginBottom">@dimen/cr_5_dp</item>
         <item name="android:layout_marginBottom">@dimen/cr_5_dp</item>
         <item name="android:layout_marginTop">@dimen/cr_5_dp</item>
         <item name="android:layout_marginTop">@dimen/cr_5_dp</item>
     </style>
     </style>
+
     <style name="Theme">Theme.NoTitleBar.Fullscreen</style>
     <style name="Theme">Theme.NoTitleBar.Fullscreen</style>
 
 
     <!-- ConstraintLayout 布局中通用内容 上下占满 -->
     <!-- ConstraintLayout 布局中通用内容 上下占满 -->
@@ -412,6 +422,50 @@
         <item name="android:orientation">vertical</item>
         <item name="android:orientation">vertical</item>
     </style>
     </style>
 
 
+    <!--直播页面布局-->
+    <!--标题头标题文字样式-->
+    <style name="live_head_title" parent="lbl_title_common">
+        <item name="android:textColor">@color/white</item>
+        <item name="android:layout_centerVertical">true</item>
+        <item name="android:layout_marginLeft">@dimen/cr_10_dp</item>
+    </style>
+    <!--标题头按钮内文字样式-->
+    <style name="live_head_button_label" parent="lbl_title_common">
+        <item name="android:textSize">@dimen/sp_12</item>
+    </style>
+    <!--标题头样式-->
+    <style name="live_head_main">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">@dimen/cr_24_dp</item>
+    </style>
+    <!--标题头按钮容器样式-->
+    <style name="live_head_button_panel">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">match_parent</item>
+        <item name="android:layout_alignParentRight">true</item>
+        <item name="android:layout_marginRight">@dimen/common_margin</item>
+        <item name="android:gravity">center</item>
+        <item name="android:clickable">true</item>
+        <item name="android:background">@drawable/btn_imgbtn_selector</item>
+        <item name="android:padding">@dimen/common_padding</item>
+    </style>
+    <!--标题头按钮内图标样式-->
+    <style name="live_head_button_icon">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:text">@string/default_a</item>
+        <item name="android:textColor">@color/white</item>
+        <item name="android:textSize">@dimen/sp_14</item>
+        <item name="android:layout_marginRight">@dimen/common_margin</item>
+    </style>
+    <!--选择器容器样式-->
+    <style name="live_wheel_panel">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">@dimen/cr_80_dp</item>
+        <item name="android:layout_marginLeft">@dimen/cr_20_dp</item>
+        <item name="android:layout_marginRight">@dimen/cr_20_dp</item>
+    </style>
+
     <!--自定义文本框属性-->
     <!--自定义文本框属性-->
     <declare-styleable name="ViewEditTextProperty">
     <declare-styleable name="ViewEditTextProperty">
         <attr name="crHint" />
         <attr name="crHint" />
@@ -439,8 +493,8 @@
 
 
     <!--速度控件属性-->
     <!--速度控件属性-->
     <declare-styleable name="CrSpeedWidget">
     <declare-styleable name="CrSpeedWidget">
-        <attr name="speedTitle" format="string"/>
-        <attr name="speedProgress" format="integer"/>
+        <attr name="speedTitle" format="string" />
+        <attr name="speedProgress" format="integer" />
     </declare-styleable>
     </declare-styleable>
 
 
     <!--图标控件属性-->
     <!--图标控件属性-->