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