需求场景
- 用户打开APP,点击扫码按钮后去识别二维码,然后根据二维码的相关信息将移动端的摄像头推送到SRS服务器中
- 用户打开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配置有点区别
- ext.kotlin_version = '1.5.10'
- classpath 'com.android.tools.build:gradle:4.2.0
- distributionUrl=services.gradle.org/distributio...
- minSdkVersion 20
所以采用了【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);
});
}