wifi直连遥控照相系统重构

4 篇文章 0 订阅
3 篇文章 0 订阅
本文介绍了使用Kotlin重构旧的WiFi直连遥控照相系统,增加了功能并优化了架构。系统采用MVVM架构,利用协程处理模块间通信和并发,通过StateFlow和Channel进行数据传递。重构后,系统支持前置镜头、镜头切换和缩放功能。
摘要由CSDN通过智能技术生成

回顾

2018年刚开始接触android时,用两部手机实现了wifi直连遥控照相(请参考:用wifi直连(p2p)实现遥控照相
),当时对于android系统和java语言了解甚少,做出来的东西比较幼稚,现在回头看,有些方法比较笨,存在不少问题。例如:为了实现双工通信在系统中用了两个端口,收发各用一个。实际上一个端口也完全能实现双工通信。
近期有一些空闲时间,把老系统重构了。

kotlin语言

在android开发中使用 kotlin语言已经是大势所趋,况且 kotlin语言的优势(便捷、抽象力、多态)也是java不能比拟的。所以此次重构用 kotlin语言重写了程序。虽然在android studio平台上可以把全篇java转换成kotlin,但是有些转换并不成功,或者转换后的代码明显不合理,需要手工重写代码。上手kotlin以后,基本上没有了再使用java来开发android应用的意愿。

功能改进

  • 增加前置镜头,原来只能使用后置镜头。
  • 在远程控制功能中增加了前后置镜头切换、放大(Zoom In)、缩小(Zoom Out),保留了拍照功能。

架构

时过境迁,这次采用Android Studio中常用的MVVM架构,UI层代码和逻辑层代码可以有效分离,调试和修改比较方便。
本系统涉及WIFI连接、socket通信、照相机、视频图像编解码,每一种处理都比较复杂,为了避免ViewModel臃肿,各个业务逻辑分别放在不同的模块中,通过ViewModel调用这些模块。
相机端模块构成图
相机端模块构成图
控制器端模块构成图
控制器端模块构成图
控制器端的两个UI界面ConnectFragment和MainFragment通过SharedViewModel共享数据。

协程应用

使用kotlin协程可以轻松应对各种异步逻辑需求,主要应用于模块之间通信和并发处理。

模块之间通信

以前开发过程中,相当多时间和精力消耗在模块之间通信上。模块之间通信本质上是异步处理逻辑,正是协程擅长的领域,这次用到以下两种手段,感觉很方便,代码也简洁许多。当然与协程相关的数据传递方法不止这两种。

StateFlow

为了传递消息,定义了一个消息类Msg。成员type表示消息的类型,obj表示消息实体(可以为null)。

class Msg(val type: String, val obj: Any? )

在需要发送消息的模块中,定义StateFlow的实例,StateFlow属于热数据流(无论是否有订阅者,都会执行发出数据流的操作),使用场景与LiveData相似。对于可变消息的发布需要使用MutableStateFlow。
下面是相机端MainViewModel中的代码。

private val _msg = MutableStateFlow(Msg("", null))
val msgStateFlow: StateFlow<Msg> = _msg
private fun buildMsg(msg: Msg) {
    _msg.value = msg
    }

需要发出消息时,调用buildMsg()即可。例如:

buildMsg(Msg("msgStateFlow", "图片保存成功! 保存路径:$savedPath 耗时:$time"))

相机端MainActivity是消息的订阅者,接收数据的collect方法是一个挂起函数,只能在协程域中运行,因此在MainActivity的UI进程中需要启动协程,以异步执行方式接收MainViewModel发出的消息。

lifecycleScope.launch {
    viewModel.msgStateFlow.collect {
        when (it.type) {
             // 根据消息的类型分别处理
        }
    }
}

这里也可以使用通常的CoroutineScope创建协程,但是lifecycleScope是与生命周期绑定的协程域,当LifecycleOwner也就是MainActivity销毁时自动取消,无需人工干预。对于ViewModel也有与生命周期绑定的协程域,它就是viewModelScope。

Channel

对于视频图像数据的传递,感觉用Channel更方便一些。Channel是一种热数据通道,内部有一个并发安全的队列,可以指定缓存,用于实现不同协程间的通信。
定义Channel的实例。

private val imageDataChannel = Channel<ImageData>(Channel.CONFLATED)

imageDataChannel的缓存定义为CONFLATED,意思是如果缓存溢出,丢弃旧数据,加入新数据,不挂起。

在需要发送数据的模块的协程域中,使用send方法发出数据。

CoroutineScope(Job()).launch {imageDataChannel.send(imageData)}

在接收数据模块的协程域中,使用receive方法取得数据。

coroutineScope.async(Dispatchers.IO) {
	val objectOutputStream = ObjectOutputStream(bufferedSink.outputStream())
	while (isActive) {
		val imageData = imageDataChannel.receive()
		// 发送imageData数据
        objectOutputStream.writeObject(imageData)
        bufferedSink.flush()
	}
}

对于接收到的imageData,此处使用okio的BufferedSink转为socket数据流输出。

并发处理

相机端和控制器端都使用socket双向通信,以前使用不同线程实现收发功能,用kotlin协程来实现则便利很多。
以相机端的通信模块为例,代码如下

class ServerRunnable(private val imageDataChannel: Channel<ImageData>) : Runnable {
    private val tag = this.javaClass.simpleName
    private val serverSocket = ServerSocket(Value.Int.serverSocketPort)
    private val serverJob by lazy { Job() }

    private val _msg = MutableStateFlow(Msg("", null))
    val msgStateFlow: StateFlow<Msg> = _msg
    private fun buildMsg(msg: Msg) {
        _msg.value = msg
    }

    override fun run() {
        Log.i(tag, "run")
        runBlocking(serverJob) {
            Log.i(tag, "serverJob start")
            while (isActive) {
                val clientSocket = getClientSocket() ?: break
                buildMsg(Msg(Value.Message.ConnectStatus, true))        //向ViewModel发出客户端已连接消息
                clientSocket.use { socket ->
                    val bufferedSource: BufferedSource = socket.source().buffer()
                    val bufferedSink: BufferedSink = socket.sink().buffer()
                    //输入协程
                    val inputJob = inputJobAsync(this, bufferedSource)
                    //输出协程
                    val outputJob = outputJobAsync(this, bufferedSink)
                    // if any job ended, other job should be cancelled.
                    if (outputJob.await() || inputJob.await()) {
                        outputJob.cancel()
                        inputJob.cancel()
                        buildMsg(Msg(Value.Message.ConnectStatus, false))        //向ViewModel发出客户端连接中断消息
                    }
                }
            }
        }
        Log.i(tag, "serverJob end")
    }

    private fun getClientSocket(): Socket? {
        return try {
            serverSocket.accept()   //这是阻塞方法,接收到客户端请求后进入后续处理
        } catch (e: IOException) {
            Log.e(tag, e.message.toString())
            null
        }
    }

    private fun outputJobAsync(coroutineScope: CoroutineScope, bufferedSink: BufferedSink) : Deferred<Boolean> {
        return coroutineScope.async(Dispatchers.IO) {
            Log.i(tag, "outputJob start")
            try {
                val objectOutputStream = ObjectOutputStream(bufferedSink.outputStream())
                while (isActive) {
                    // 接收来自MediaCodecCallback编码后的imageData数据
                    val imageData = imageDataChannel.receive()                  //这是阻塞方法
                    // 发送imageData数据
                    objectOutputStream.writeObject(imageData)
                    bufferedSink.flush()
//                    Log.i(tag, "imageData wrote")
                }
                objectOutputStream.close()
            } catch (socketException: SocketException) {
                Log.e(tag, "writeImageData SocketException")
            } catch (e: Exception) {
                e.printStackTrace()
            }
            Log.i(tag, "outputJob end")
            true
        }
    }

    private fun inputJobAsync(coroutineScope: CoroutineScope, bufferedSource: BufferedSource) : Deferred<Boolean> {
        return coroutineScope.async(Dispatchers.IO) {
            Log.i(tag, "inputJob start")
            try {
                while (isActive) {
                    Log.i(tag, "inputJobAsync coroutineScope isActive")
                    val receivedMsg = bufferedSource.readUtf8Line()
                    msgHandle(receivedMsg?: Value.Message.Blank)
                }
            }  catch (socketException: SocketException) {
                Log.e(tag, "readClientMsg SocketException")
            } catch (e: Exception) {
                e.printStackTrace()
            }
            Log.i(tag, "inputJob end")
            true
        }
    }

    private fun msgHandle(receivedMsg: String) {
        Log.i(tag, "msgHandle client message: $receivedMsg")
        when (receivedMsg) {
            Value.Message.ClientInterruptRequest -> {      //客户端中断请求
                //clientSocket should be closed and recreated
            }
           else -> {
               buildMsg(Msg(Value.Message.ClientMessage, receivedMsg))     //forward client message up
           }

        }
    }

    fun stopService() {
        if (!serverSocket.isClosed) serverSocket.close()
        CoroutineScope(Job()).launch {
            if (serverJob.isActive) serverJob.cancel()
        }
    }
}

尽管协程可以处理异步任务,但是在主线程中直接用协程处理IO也会报错的,所以必须在子线程中启动协程。
在子线程中用runBlocking启动一个阻塞当前线程的名为serverJob的协程,一旦serverJob.cancel()实施,serverJob状态不再isActive,而且阻塞方法serverSocket.accept()会发生异常,双管齐下,serverJob就被取消了。
对于输入输出IO处理,程序中使用async函数制造了两个Deferred的实例:inputJob和outputJob,它们本质上是具有返回值的Job,可以让调用者得到inputJob和outputJob的执行结果:成功或失败。outputJob.await()和inputJob.await()会阻塞当前协程,直至inputJob和outputJob执行完毕成功返回,或者发生异常返回。在本程序中无论是否发生异常,inputJob和outputJob均返回ture。这部分的异常处理留待以后完善。
outputJob发送的是图像数据,inputJob接收的是以换行符System.lineSeparator()结尾的文字信息。
控制器端的通信模块与相机端相似,只是接收发送的内容不同,inputJob接收的是图像数据,outputJob发送的是文字数据。

后记

关于wifi p2p直连、照相机的使用、视频编解码,与以前的系统并没有大的不同,因此不再赘述。如果有兴趣请参考源代码
欢迎指摘和讨论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值