美文网首页android开发专题Hybrid开发
h5与原生的大文件交互解决策略

h5与原生的大文件交互解决策略

作者: Hsicen | 来源:发表于2019-11-26 16:01 被阅读0次

    起因

    最近项目有个新的需求,就是社区要实现本地的视频录制,预览,上传然后社区播放的功能,由于之前项目重构的时候,为了节省时间 整个社区的功能都交由h5开发,现在要添加本地视频,然后预览播放有两种实现方式:

    • 第一种方式是视频的选择,预览,都是直接跳转原生界面进行操作,h5只负责界面的展示
    • 第二种方式是视频的展示,预览和播放都由h5统一做,原生只负责视频的选择然后把数据传递给h5

    我个人是倾向于第一种实现方式的,因为h5本身存在许多兼容性问题,再加上Android本身被各个国产厂商魔改,而且也有h5也有性能问题,h5播放视频也会有卡顿,全屏,手机硬件交互等等问题,总之我是很不推荐第二种方案的。但但但是呢,我们的产品说微博,微信h5播放视频不是挺好的么,视频播放那么流畅,也没有什么卡顿,兼容性问题呢。这我还能说什么呢,产品都坚持了,而且两周一个迭代周期也不允许我有什么可多说的了,只好按照产品的意愿做了撒,况且视频播放器那一套流程,各种状态的控制也挺复杂的,这些问题都交由h5开发的同学去考虑吧。

    方案调研

    在确定了由h5进行视频的展示,预览,播放后,就开始寻找我们如何把视频数据传递给h5,按照项目里面已有的h5与原生的交互方案,使用JSBridge进行数据交互,但是数据有大小限制,视频动不动就几十M,即使将视频压缩,也还是不能完全保证能够把数据传递过去;然后就是使用webview的input_file机制,调起系统的文件选择器,选择文件,但是缺点是每次只能选择一个文件,而且界面UI不能自定义,这个方案也被放弃了;然后又尝试了把文件转化为二进制流传递给h5,但是发现h5根本无法识别,这种方案又失败了;最后在网上找到一种似乎可行的方案,那就是在本地启动一个本地服务器,然后定义一个文件请求接口,h5调起原生的视频选择控件选择一个视频后,我们把文件路径和本地服务器地址传给h5,然后h5点击预览时就通过这个接口把文件路径传给我,本地服务器在收到请求后,根据文件路径找到文件,然后把文件流返给h5,h5在拿到文件流后进行视频的播放。这种方案看来可行,而且也有成功的案例,比如局域网投屏和文件传输都是用的这种方案。

    Demo实现

    在确定了使用本地服务器实现文件传输方案后,就要着手实现了。由于之前没有做过而且项目时间比较紧,就在全球最大的同性交友网站上找寻找看有没有比较优秀的开源库,最后找到了几个比较好的三方库,经过比较选择了AndServer,根据说明文档就开始开发。首先,通过gradle方式引进项目

    dependencies {
        .........
        implementation 'com.yanzhenjie.andserver:api:2.0.5'
        annotationProcessor 'com.yanzhenjie.andserver:processor:2.0.5'
    }
    

    然后对项目进行简单的封装,也方便后续进行方案的替换

    先实现一个Service,对服务进行启动和停止的控制

    class CoreService : JobService() {
    
        private val mServer by lazy {
            AndServer.serverBuilder(this@CoreService)
                .inetAddress(NetUtils.localIPAddress)
                .port(ServerManager.SERVER_PORT)
                .timeout(10, TimeUnit.SECONDS)
                .listener(object : Server.ServerListener {
                    override fun onException(e: Exception?) {
                        ServerManager.onServerError(this@CoreService, e?.message ?: "未知错误")
                    }
    
                    override fun onStarted() {
                        val hostAddress = NetUtils.localIPAddress?.hostAddress ?: "127.0.0.1"
                        ServerManager.onServerStart(this@CoreService, hostAddress)
                    }
    
                    override fun onStopped() {
                        ServerManager.onServerStop(this@CoreService)
                    }
                }).build()
        }
    
        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
            startServer()
            return START_STICKY
        }
    
        override fun onDestroy() {
            stopServer()
            super.onDestroy()
        }
    
        private fun startServer() {
            if (mServer.isRunning) {
                val hostAddress = NetUtils.localIPAddress?.hostAddress ?: "127.0.0.1"
                ServerManager.onServerStart(this, hostAddress)
            } else mServer.startup()
        }
    
        private fun stopServer() {
            mServer.shutdown()
        }
    
        override fun onStopJob(params: JobParameters?): Boolean {
            return false
        }
    
        override fun onStartJob(params: JobParameters?): Boolean {
            startServer()
            return true
        }
    }
    

    Service静态注册

    <service
        android:name=".server.CoreService"
        android:permission="android.permission.BIND_JOB_SERVICE"
        android:enabled="true"
        android:exported="true" />
    

    NetUtils是一个工具类,主要是获取本地的IP地址

    object NetUtils {
    
        /*** Get local Ip address.*/
        val localIPAddress: InetAddress?
            get() {
                var enumeration: Enumeration<NetworkInterface>? = null
                try {
                    enumeration = NetworkInterface.getNetworkInterfaces()
                } catch (e: SocketException) {
                    e.printStackTrace()
                }
    
                if (enumeration != null) {
                    while (enumeration.hasMoreElements()) {
                        val nif = enumeration.nextElement()
                        val inetAddresses = nif.inetAddresses
                        if (inetAddresses != null) {
                            while (inetAddresses.hasMoreElements()) {
                                val inetAddress = inetAddresses.nextElement()
                                if (inetAddress.toString().contains("127.0.0.1")) {
                                    return inetAddress
                                }
                            }
                        }
                    }
                }
                return null
            }
    }
    

    这里说一下我为什么要绑定127.0.0.1的本地地址,因为我们项目中有缓存机制,选择的视频在切换网络过后还要求能够预览,如果绑定动态分配的ip地址,那么在切换网络过后,可能会造成ip地址发生变化,之前h5选择的资源文件会无法预览,如果我们绑定127.0.0.1,无论是切换网络还是完全关闭网络,都可以访问到之前选择的文件资源(只要本地文件没有删除),并且只有在本应用内才能够访问到,安全性提高。

    接下来就是ServerManager,AndServer的全局控制类

    class ServerManager constructor(
        private val mContext: Context, private val mListener: ServerChangeListener
    ) : BroadcastReceiver() {
        private val mService: Intent = Intent(mContext, CoreService::class.java)
    
        fun register() {
            runCatching {
                val filter = IntentFilter(ACTION)
                mContext.registerReceiver(this, filter)
            }.onFailure {
                it.handle()
            }
        }
    
        fun unRegister() {
            runCatching {
                mContext.unregisterReceiver(this)
            }.onFailure {
                it.handle()
            }
        }
    
        fun startServer() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                val builder = JobInfo.Builder(JOB_ID, ComponentName(mContext, CoreService::class.java))
                    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY).build()
                val jobScheduler = mContext.getSystemService(JobScheduler::class.java) as JobScheduler
                jobScheduler.schedule(builder)
            } else {
                mContext.startService(mService)
            }
        }
    
        fun stopServer() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                val jobScheduler = mContext.getSystemService(JobScheduler::class.java) as JobScheduler
                jobScheduler.cancel(JOB_ID)
            } else {
                mContext.stopService(mService)
            }
        }
    
        override fun onReceive(context: Context?, intent: Intent?) {
            val action = intent?.action ?: return
            if (ACTION == action) {
                when (intent.getIntExtra(CMD_KEY, 0)) {
                    CMD_VALUE_START -> {
                        val ip = intent.getStringExtra(MESSAGE_KEY)
                        mListener.onServerStart(ip)
                    }
    
                    CMD_VALUE_ERROR -> {
                        val errMsg = intent.getStringExtra(MESSAGE_KEY)
                        mListener.onServerError(errMsg)
                    }
    
                    CMD_VALUE_STOP -> mListener.onServerStop()
                }
            }
        }
    
        companion object {
            const val JOB_ID = 100000
    
            const val SERVER_PORT = 8088
    
            private const val ACTION = "com.andserver.receiver"
            private const val CMD_KEY = "CMD_KEY"
            private const val MESSAGE_KEY = "MESSAGE_KEY"
    
            private const val CMD_VALUE_START = 1
            private const val CMD_VALUE_ERROR = 2
            private const val CMD_VALUE_STOP = 4
    
            fun onServerStart(context: Context, hostAddress: String) {
                sendBroadcast(context, CMD_VALUE_START, hostAddress)
            }
    
            fun onServerError(context: Context, error: String) {
                sendBroadcast(context, CMD_VALUE_ERROR, error)
            }
    
            fun onServerStop(context: Context) {
                sendBroadcast(context, CMD_VALUE_STOP)
            }
    
            private fun sendBroadcast(context: Context, cmd: Int) {
                sendBroadcast(context, cmd, null)
            }
    
            private fun sendBroadcast(context: Context, cmd: Int, message: String?) {
                val broadcast = Intent(ACTION)
                broadcast.putExtra(CMD_KEY, cmd)
                broadcast.putExtra(MESSAGE_KEY, message)
                context.sendBroadcast(broadcast)
            }
        }
    }
    

    本地服务状态监听接口类定义

    interface ServerChangeListener {
    
        fun onServerStart(ipAddress: String)
    
        fun onServerStop()
    
        fun onServerError(msg: String)
    }
    

    最后是文件请求Controler,处理文件请求响应

    @RestController
    class TransferFileController {
        @GetMapping("/file")
        fun browseFile(@RequestParam(name = "fileId") fileId: String, response: HttpResponse) {
            val mFile = File(fileId)
            val mBody = FileBody(mFile)
            response.setBody(mBody)
        }
    }
    

    这里需要注意的一点是,如果你的文件路名含有&特殊字符,那么fileId请求参数就会被自动截断。遇到这种情况,一般有两种处理方式,第一种是使用post请求,把参数放在body中;第二种方式是我们在把文件路径传递给h5之前,我们先进行编码,将特殊字符替换掉,然后我们在收到请求后,去寻找本地文件之前对编码的路径进行还原

    最后再对启动入口进行一次封装,对外提供启动服务的入口

    object ServerUtil {
        private var serverManager: ServerManager? = null
        var baseUrl = ""  //服务基地址
        var fileUrl = ""  //文件服务地址
    
        fun initWebServer(activity: Activity?) {
            if (serverManager == null) {
                serverManager = ServerManager(
                    activity ?: GlobalContextProvider.getGlobalContext(),
                    object : ServerChangeListener {
                        override fun onServerStart(ipAddress: String) {
                            Logger.d("hsc  启动微服务:  $ipAddress")
                            baseUrl = "http://$ipAddress:${ServerManager.SERVER_PORT}"
                            fileUrl = "http://$ipAddress:${ServerManager.SERVER_PORT}/file"
                        }
    
                        override fun onServerStop() = Logger.d("停止微服务")
                        override fun onServerError(msg: String) = Logger.d("微服务启动报错:$msg")
                    })
            }
            serverManager?.register()
            serverManager?.startServer()
        }
    
        fun unRegWebServer() {
            serverManager?.stopServer()
            serverManager?.unRegister()
            serverManager = null
        }
    }
    

    然后在你需要使用服务之前,调用initWebServer()启动服务就可以了,需要注意一点的就是,你可能需要全局监听网络变化,网络变化后,你需要重启一次服务才能够访问到资源

    相关文章

      网友评论

        本文标题:h5与原生的大文件交互解决策略

        本文链接:https://www.haomeiwen.com/subject/izxywctx.html