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);
    });
  }
相关推荐
2301_7891695412 分钟前
react crash course 2024(9) proxying
前端·javascript·react.js
计算机学姐21 分钟前
基于nodejs+vue的超市管理系统
前端·javascript·vue.js·vscode·前端框架·node.js·ecmascript
Z_B_L23 分钟前
three.js----快速上手,如何用vue在web页面中导入 gltf/glb , fbx , obj 模型
开发语言·前端·javascript
谢尔登24 分钟前
webpack 和 vite 区别
前端·webpack·node.js
重生之我在20年代敲代码1 小时前
HTML讲解(三)通用部分
前端·笔记·html
_.Switch1 小时前
Python Web 开发中的DevOps 实践与自动化运维
运维·开发语言·前端·python·架构·serverless·devops
轻口味1 小时前
Android SharedPreference详解
android·开发语言
计算机学姐1 小时前
基于nodejs+vue的宠物医院管理系统
前端·javascript·vue.js·mysql·npm·node.js·sass
余生H2 小时前
前端大模型入门:使用Transformers.js手搓纯网页版RAG(二)- qwen1.5-0.5B - 纯前端不调接口
前端·javascript·人工智能·大语言模型·rag·端侧大模型·webml
你会发光哎u2 小时前
深入理解包管理工具
开发语言·前端·javascript·node.js