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);
    });
  }
相关推荐
锋风Fengfeng25 分钟前
安卓15预置第三方apk时签名报错问题解决
android
吃杠碰小鸡25 分钟前
lodash常用函数
前端·javascript
emoji11111134 分钟前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼37 分钟前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_7482500342 分钟前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O1 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235951 小时前
web复习(三)
前端
User_undefined1 小时前
uniapp Native.js原生arr插件服务发送广播到uniapp页面中
android·javascript·uni-app
AiFlutter1 小时前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
麦兜*1 小时前
轮播图带详情插件、uniApp插件
前端·javascript·uni-app·vue