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);
    });
  }
相关推荐
秦jh_12 分钟前
【Linux】多线程(概念,控制)
linux·运维·前端
帅次16 分钟前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
蜗牛快跑21325 分钟前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy26 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
枯骨成佛1 小时前
Android中Crash Debug技巧
android
涔溪1 小时前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与2 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun2 小时前
CSS样式实现3D效果
前端·css·3d