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);
    });
  }
相关推荐
XiaoLeisj33 分钟前
Android Kotlin 全链路系统化指南:从基础语法、类型系统与面向对象,到函数式编程、集合操作、协程并发与 Flow 响应式数据流实战
android·开发语言·kotlin·协程
ywf12151 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭2 小时前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf8 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特8 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷8 小时前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian9 小时前
前端node常用配置
前端
华洛9 小时前
利好打工人,openclaw不是企业提效工具,而是个人助理
前端·javascript·产品经理
xkxnq9 小时前
第六阶段:Vue生态高级整合与优化(第93天)Element Plus进阶:自定义主题(变量覆盖)+ 全局配置与组件按需加载优化
前端·javascript·vue.js
A黄俊辉A10 小时前
vue css中 :global的使用
前端·javascript·vue.js