flutter扫码推流简单应用 - SRS流媒体服务器

需求场景

  1. 用户打开APP,点击扫码按钮后去识别二维码,然后根据二维码的相关信息将移动端的摄像头推送到SRS服务器中
  2. 用户打开wechat,点击扫一扫去识别二维码,然后默认打开已有的APP并进行推流

技术选型

基于公司原有技术栈,APP技术选型上采用的是Flutter2

ini 复制代码
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 2.8.1, on Microsoft Windows [Version 10.0.22631.3447],
    locale zh-CN)
[√] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
[√] Android Studio (version 2021.3)
[√] Connected device (1 available)

功能拆分

  • SRS推流
  • 二维码识别
  • URLScheme协议调用应用

SRS推流

dart 复制代码
// 创建peerConnection
_pc = await webrtc.createPeerConnection({'sdpSemantics': "unified-plan"});
// 添加audio信使,若无则在createOffer对应的字段上填false
_pc!.addTransceiver(
  kind: webrtc.RTCRtpMediaType.RTCRtpMediaTypeAudio,
  init: webrtc.RTCRtpTransceiverInit(
      direction: webrtc.TransceiverDirection.SendOnly),
);
// 添加video信使,若无则在createOffer对应的字段上填false
_pc!.addTransceiver(
  kind: webrtc.RTCRtpMediaType.RTCRtpMediaTypeVideo,
  init: webrtc.RTCRtpTransceiverInit(
      direction: webrtc.TransceiverDirection.SendOnly),
);
// 添加轨道,注意如果没有video/audio信使,则不能将对应的track添加到peerConnection当中
for (var element in stream.getTracks()) {
  await _pc!.addTrack(element, stream);
}
// 创建SDP
webrtc.RTCSessionDescription offer = await _pc!.createOffer({
  'mandatory': {'OfferToSendAudio': true, 'OfferToSendVideo': true},
});
// 将自身SDP添加进来
await _pc!.setLocalDescription(offer);
// 向srs服务器获取对方的SDP
HttpClientRequest req = await client.postUrl(Uri.parse(apiUrl));
...
// SRS服务器应答后获得o['sdp']
webrtc.RTCSessionDescription answer =
  webrtc.RTCSessionDescription(o['sdp'], 'answer');
// 将对方的SDP添加进来则建立完整的peerConnection对方能接收到对应的video/audio
await _pc!.setRemoteDescription(answer);

二维码识别

基于简单的调研发现flutter一般使用的qr_code_scanner或者barcode_scan2 由于【qr_code_scanner: ^0.7.0】配置有些要求跟本身项目的android配置有点区别

所以采用了【barcode_scan2: ^4.2.4】

dart 复制代码
import 'package:barcode_scan2/barcode_scan2.dart';
  /// 扫码 - 【点击扫码按钮】
  Future _scan() async {
    Completer completer = Completer<String>();
    try {
      final result = await BarcodeScanner.scan(
        options: ScanOptions(
          strings: {
            'cancel': "取消",
          },
          restrictFormat: selectedFormats,
          useCamera: -1,
          autoEnableFlash: true,
          android: const AndroidOptions(
            aspectTolerance: 0.50,
            useAutoFocus: true,
          ),
        ),
      );
      if (result.rawContent != "") {
        completer.complete(result.rawContent);
      } 
    } on PlatformException catch (e) {
      if (e.code == BarcodeScanner.cameraAccessDenied) {
        completer.completeError('缺少摄像头权限');
      } else {
        completer.completeError('Unknown error: $e');
      }
    }
    return completer.future;
  }

这里有个小问题,会出现APP的应用名称在红色圈的位置,想要去除掉需要

在依赖缓存中找到barcode_scan2文件【在项目根目录的.flutter-plugins文件中找到依赖的文件路径】 直接在其文件内修改android/src/main/AndroidManifest.xml中的activity添加android:label=""

xml 复制代码
<activity android:name="de.mintware.barcode_scan.BarcodeScannerActivity" android:label="" />

URLScheme协议调用应用

很多时候会看到一个场景,用户扫了二维码后跳转到某个网页,网页实际上由两个功能,第一拉起APP也就是前往第三方应用,第二给用户提供下载APP的链接。


拉起APP用到的就是URLScheme

android设置URLScheme

在flutter根目录下的android/app/src/main/AndroidManifest.xml中activity中

ini 复制代码
<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="yoururlscheme" android:host="host1" />
</intent-filter>

ios设置URLScheme

在ios/Runner/Info.plist

xml 复制代码
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.example.app</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>yoururlscheme://host1</string>
    </array>
  </dict>
</array>

解析URLScheme

依赖:【uni_links: ^0.5.1】

scss 复制代码
  Future _initUniLinks() async {
    // 处理初始URI
    final initialUri = await getInitialUri();
    if (initialUri != null) {
      handleUri(initialUri);
    }

    // 监听后续的URI
    linkStream.listen((String? url) {
      if (url == null) {
        return;
      }
      Uri uri = Uri.parse(url);
      handleUri(uri);
    }, onError: (Object error) {
      print('Error receiving URI data: $error');
    });
  }

特别注意的是,由于APP扫码推流和微信扫一扫进行扫码后拉起推流用的实际上是同一个二维码,所以对于二维码的地址需要特别的设计

python 复制代码
import qrcode
from urllib.parse import quote,unquote

url = "http://192.166.167.54:80/push/" # http地址,微信扫一扫访问的地址
url += "?"
url += f"push_url={quote('rtmp://srs.zxtttdacsed.com:1935/live/livecode177_300001')}" # 推流地址【重点】
url += f"&push_api_url={quote('http://srs.zxtttdacsed.com:1985/rtc/v1/publish/')}" # 推流sdp交换地址【重点】
url += f"&jump_url={quote('yourschemeurl://host1')}" # APP注册的scheme地址
url += f"&download_url={quote('http://192.166.167.54:80/live/app.apk')}" # 下载地址
qrcode.make(url).save('test/test.png')

最后生成的地址如下:

http://192.166.167.54:80/push/?push_url=rtmp%3A//srs.zxtttdacsed.com%3A1935/live/livecode177_300001&push_api_url=http%3A//srs.zxtttdacsed.com%3A1985/rtc/v1/publish/&jump_url=yourschemeurl%3A//host1&download_url=http%3A//192.166.167.54%3A80/live/app.apk 这样解决了微信扫一扫能跳转到网页的问题 网页也需要进行特别的处理

js 复制代码
var href = window.location.href;
href = href.split('?');
var params = {};
href[1].split('&').map(item=>{
  var arr = item.split('=');
  params[arr[0]] = decodeURIComponent(arr[1]);
})
window.onload = function () {
    var isSuccessOpenApp = false;
    // 如果2秒后应用还没有被打开,那么假定用户没有安装应用,可以添加跳转下载链接地址
    setTimeout(()=>{
        if (!isSuccessOpenApp){
            window.location.href = params['download_url'];
        }
    });
    // 尝试打开应用
    document.querySelector('a').addEventListener('click', function(event) {
        event.preventDefault();
        // 在这里执行自定义的跳转逻辑,拼接成跟源二维码类似的结构方便flutter端进行解析
        window.location.href = params['jump_url'] + '?' + 'push_url=' + params['push_url'] + '&push_api_url=' + params['push_api_url'];
        isSuccessOpenApp = true;
    });
  }

这样拉起来的APP接收到的参数也会类似于原二维码的参数flutter端只需要解决获取URI参数中的push_url和push_api_url

dart 复制代码
  void handleUri(Uri uri) {
    // 解析uri中的参数
    final parameters = uri.queryParameters;
    // 使用参数
    print('${uri.scheme}, ${uri.host}, ${uri.path}, ${uri.queryParameters}');
    if (uri.queryParameters['push_url'] == null) {
      ToastUtils.show('缺少推流地址', color: Colors.redAccent);
      return;
    }
    if (uri.queryParameters['push_api_url'] == null) {
      ToastUtils.show('缺少网关地址', color: Colors.redAccent);
      return;
    }
    _pushUrl = Uri.decodeComponent(uri.queryParameters['push_url']!);
    _push_api_url = Uri.decodeComponent(uri.queryParameters['push_api_url']!);
    _push(_pushUrl, _push_api_url).then((value) {
      setState(() {
        isPublish = true;
      });
      ToastUtils.show("推流成功");
    }).catchError((e) {
      print(e);
      ToastUtils.show('推流失败', color: Colors.redAccent);
    });
  }
相关推荐
乐闻x2 分钟前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚3 分钟前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
Amd79419 分钟前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You27 分钟前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生39 分钟前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互
baiduopenmap1 小时前
百度世界2024精选公开课:基于地图智能体的导航出行AI应用创新实践
前端·人工智能·百度地图
loooseFish1 小时前
小程序webview我爱死你了 小程序webview和H5通讯
前端
菜牙买菜1 小时前
让安卓也能玩出Element-Plus的表格效果
前端
请叫我欧皇i1 小时前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
533_1 小时前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js