最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Flutte开发全景相机【实时预览】

    正文概述 掘金(李威尔)   2021-01-22   561

    一、少啰嗦,先看效果

    画质比较渣,看的清就好

    Flutte开发全景相机【实时预览】

    手机是通过wifi连接的全景相机,可以捕获到相机的实时预览,并且在手机中呈全景展示,移动相机的时候,画面会实时变化,在手机上拖动的时候,可以展示不同的方向的画面。

    二、相机API

    1、操作介绍

    相机是通过wifi功能连接手机,是把相机做为一共wifi热点让手机连接,所有请求相机的接口,可以直接请求http://192.168.1.1

    相机的请求主要操作一共分为2类,Commands/ExecuteCommands/Status,2个接口都属于POST类型,正如接口的命名,一个是进行操作,一个是查看操作的状态,我们可以直接发送POST请求来操作相机。比如对相机进行设置:

    // 用大家熟悉的ajax举例
    $.ajax(
      type: 'POST',
      url:'http://192.168.1.1/osc/commands/execute',
      data: {
        "name": "camera.setOptions",
        "parameters": {
          "options": {
            "exposureProgram":1,
            "iso":800,
            "shutterSpeed":0.002
          }
        }
      }
    )
    
    • name:你要执行的操作
    • parameters: 执行操作的参数

    2、getLivePreview

    此功能主要使用了OSC的camera.getLivePreview接口,根据官方文档解释,此API在SC型号相机中只能在拍摄模式下使用,而且当触发了拍照功能或者更改拍摄模式,此接口都会停止

    参数
    ParametersnoneOutputBinary data of live view (MotionJPEG)

    可以看到,这个接口不需要填入任何的参数,直接调用而返回一个live view stream,这是一个实时的MJPEG数据流,你要问我这个非常像JPEG的是什么东西,咱先按下不表,稍后来补充一下。

    三、Flutter实现

    1、需要引入的包

    实现此功能主要使用了2个插件,第一个是用来做接口请求,第二个用来做图片全景预览功能

    • http: 0.12.2
    • panorama: 0.3.1

    你要问我为什么不用Dio这个非常强大的请求工具,那是因为此接口返回的是一个live stream数据,需要一个持久连接的方法,http提供了client用来做持久连接,其中的send方法可以返回一个StreamedResponse,而在其他的请求库中没有找到,希望有懂的各位指点一下。

    除了上面2个插件,还有3个官方的包也是必不可少的。

    import 'dart:async'; // 异步操作
    import 'dart:typed_data'; // 使用里面的Uint8List
    import 'dart:convert'; // 转换JSON
    

    2、声明2个Stream

    既然请求的是一个数据流,就需要先声明一个StreamSubscription来监听这个数据,好进行控制。再声明一个StreamController将数据绘制到页面上

    StreamSubscription vidoestream;
    StreamController _streamController;
    

    3、进行请求

    该引入的都引入,改声明的都声明就可以开始请求和处理数据了。可以看到这个请求方法一共分为上下2部分,上半部分是用来做请求,下半部分用来将数据处理成图片

    一、请求

    • 我们首先需要一个client的实例,才能调用send方法,
    • 而这个方法需要传一个基于BaseRequest实例的参数,再声明一个final req = http.Request();
    • Request又需要传2个参数(String method, Uri uri)
    • 所以在声明一个final uri = Uri.http('192.168.1.1', '/osc/commands/execute')
    • 最后使用client.send发送请求
    • 加了一个timeout是做超时处理
    void liveStream() async {
        // 一、请求
        final client = http.Client();
        final uri = Uri.http('192.168.1.1', '/osc/commands/execute');
        final params = {"name": "camera.getLivePreview"};
        final req = http.Request('post', uri);
        req.body = json.encode(params);
        final res = await client.send(req).timeout(Duration(seconds: 5));
        
        
        // 二、数据转换
        const _trigger = 0xFF;
        const _soi = 0xD8;
        const _eoi = 0xD9;
        //1、声明一个空的整型List
        List<int> chunks = <int>[];
        //2、订阅请求返回的数据流
        vidoestream = res.stream.listen((List<int> data) async {
          if(chunks.isEmpty) { // 判断当前的chunks,是否有数据
            final startIndex = data.indexOf(_trigger); // 判断jpeg数据的开头标识, 将第一chunk插入进chunks
            if(startIndex >=0 && startIndex+1 < data.length && data[startIndex +1] == _soi) {
              final slicedData = data.sublist(startIndex, data.length);
              //3、插入
              chunks.addAll(slicedData);
            }
          } else {
            final startIndex = data.lastIndexOf(_trigger); // 判断结束标识,插入最后一个chunk,表示一帧的数据完成
            if( startIndex + 1 < data.length && data[startIndex + 1] == _eoi ) {
              final slicedData = data.sublist(0, startIndex + 2);
              //3、插入
              chunks.addAll(slicedData);
              //4 转换为图像后并add进stream
              final imageMemory = MemoryImage(Uint8List.fromList(chunks));
              await precacheImage(imageMemory, context);
              _streamController.add(imageMemory);
              //5 清空这一帧的数据
              chunks = <int>[];
            } else { // 既不是开头,也不是结尾,中间的chunk直接插入
              //3、插入
              chunks.addAll(data);
            }
          }
        });
    }
    

    二、转换数据

    对于不了解MJPEG的来说,这里可以说是最蒙圈的地方。

    上面的注释中第3点有3个,就当成一个,我们来看这5个地方

    • 1、List chunks = [];
    • 2、res.stream.listen
    • 3、chunks.addAll
    • 4、_streamController.add(imageMemory)
    • 5、chunks = []

    这里其实比较容易理解,首先声明一个List<int>用来存放图像的编码数据,在listen(监听)这个res.stream中数据,将每一帧的数据处理后用addall方法塞入chunk里面,这一帧的数据获取到后,我们就将数据转换为imageMemory,并插入进_streamController,当数据一直更新的时候,我们就可以进行实时预览。

    三、插入页面

    这里就比较简单,直接使用一个StreamBuilder的控件,里面在使用Panorama包裹住,功能就实现了。

    StreamBuilder(
      stream: _streamController.stream,
      builder: (context, db) {
        if(db.hasData) {
          return Panorama(
            child: Image(image: db.data),
          );
        }
        return Text('没数据');
      },
    ),
    
    好的,完结撒花。
    

    等等,哪有那么容易的,还有2个非常重要的问题:

    • _soi_eoi是什么东东?
    • 为什么chunks.addAll要使用3次?

    我们继续往下看

    四、核心原理了解

    为了解决问题,只花了这2天看了看相关资料,就带大家了解一下,而不敢说讲解?

    1、MJPEG

    先看一下官方解释,我们可以得知,我们回去的数据每帧都是一张JPEG图的数据,所以我们只需将数据转为图片就好,那么问题来了,我们如何将数据转为图片?

    再来看一下JPEG的官方解释,又发现使用JFIF(Jpeg File Interchange Format)来作为标准,通过这个标准,可以获悉数据里面哪些是标记码,再对数据进行处理

    再再看一下我们获取的数据是长什么样, Flutte开发全景相机【实时预览】

    JFIF主要标记码:

    标记码数值描述
    SOI(start of image)FFD8图像开始EOI(end of image)FFD9图像结束

    目前我们只需编码里面图像开发和结束的位置就好,就可以找到ff d8中前一个字节的索引,和ff d9后一个字节的索引,再将中间的数据截取出来,就是我们需要图形的数据

    List<int> chunks = <int>[];
    const _trigger = 0xFF;
    const _soi = 0xD8;
    const _eoi = 0xD9;
    int startIndex = -1;
    int endIndex = -1;
    // data是我们请求的数据流
    if(data[i] == _trigger && data[startIndex + 1] == _soi ) {
      startIndex = i
    }
    if(data[i] == _trigger && data[startIndex + 1] == _eoi ) {
      endIndex = i
    }
    chunks = data.sublist(startIndex, endIndex)
    
    好的,那么我们又完成了这个功能。才怪嘞。
    

    将这个数据塞入MemoryImage,将程序运行起来,在页面上显示,却发现每次的图像都是残缺的,而且控制台每次都报Invalid Image的错误。

    通过print(startIndex)或者print(endIndex)会发现打印多次设定的初始值-1,出现一次正确的索引位置后,再打印多次-1,这样一直循环下去。说明每一帧的数据都不是完整的,被分成了多块,还需要将这些数据块合并起来才能得到一帧完整的图像。

    2、Transfer-Encoding: chunked

    通过查找资料了解了一般的mjpeg-streaming实现,还有flutter插件市场里http.dart的源码,发现在服务端和客户端都没有对数据进行过多的处理,那问题就是传输的过程中,我们通过打印http响应的报文,可以发现响应头是这样子。

    keyvalue
    ConnectioncloseX-Content-Type-OptionsnosniffContent-Typemultipart/x-mixed-replace; boundary="---osclivepreview---"Transfer-Encodingchunked

    不了解http协议的话,就很容易忽视掉这里,其实的Transfer-Encoding: chunked翻译成中文,可以理解为分块传输编码,意思就是说传输大容量数据时,通过把数据分割成多块,能够让页面逐步显示页面。这种把实体主体分块的功能称为分块传输编码。

    这样就能理解了,为什么打印传输的数据时,隔几次打印一次SOI(图像开发标识)EOI(图像结束标识),那么只需要将数据进行拼接就好,回到前面的数据处理方法那里,再来看一下这个方法,可以说是完全理解了。

    const _trigger = 0xFF; // 标识
    const _soi = 0xD8; //图像开始
    const _eoi = 0xD9; //图像结束
    List<int> chunks = <int>[]; // 来保存每一帧的数据
    vidoestream = res.stream.listen((List<int> data) async {
      //判断当前的chunks,是否有数据
      if(chunks.isEmpty) { 
        // 找到开头标识
        final startIndex = data.indexOf(_trigger); 
        if(startIndex >=0 && startIndex+1 < data.length && data[startIndex +1] == _soi) {
          // 从开始标识,到最后是有用的数据
          final slicedData = data.sublist(startIndex, data.length);
          chunks.addAll(slicedData);
        }
      } else {
        // 找到结束标识,
        final startIndex = data.lastIndexOf(_trigger);
        if( startIndex + 1 < data.length && data[startIndex + 1] == _eoi ) {
          // 从最开头,到结束标识是有用的数据
          final slicedData = data.sublist(0, startIndex + 2);
          // 插入,这一帧的数据就完成
          chunks.addAll(slicedData);
          final imageMemory = MemoryImage(Uint8List.fromList(chunks));
          await precacheImage(imageMemory, context);
          _streamController.add(imageMemory);
          chunks = <int>[];
        } else { 
          // 既不是开头,也不是结尾,就是中间的数据,都有用,插入
          chunks.addAll(data);
        }
      }
    });
    

    5、总结

    在这个功能实现上,是耽误时间最久的,这一块涉及到不少知识,比如flutter中StreamController的使用,如何请求一个mjpeg stream图像编码技术,以及http的分块编码传输,而基础知识不牢固,都是要现找资料学习。好在经过这次开发,也学习了不少东西,了解到基础知识的重要性。这个功能实现,就在筹备这篇文章,可能还有很多不足的地方,欢迎大家指正。


    起源地下载网 » Flutte开发全景相机【实时预览】

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元