博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Flutter下实现低延迟的跨平台RTSP/RTMP播放
阅读量:5993 次
发布时间:2019-06-20

本文共 20600 字,大约阅读时间需要 68 分钟。

为什么要用Flutter?

Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter可以与现有的代码一起工作。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。

Flutter有哪些与众不同

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

1. Beautiful - Flutter 允许你控制屏幕上的每一寸像素,这让「设计」不用再对「实现」妥协;

2. Fast - 一个应用不卡顿的标准是什么,你可能会说 16ms 抑或是 60fps,这对桌面端应用或者移动端应用来说已足够,但当面对广阔的 AR/VR 领域,60fps 仍然会成为使人脑产生眩晕的瓶颈,而 Flutter 的目标远不止 60fps;借助 Dart 支持的 AOT 编译以及 Skia 的绘制,Flutter 可以运行的很快;

3. Productive - 前端开发可能已经习惯的开发中 hot reload 模式,但这一特性在移动开发中还算是个新鲜事。Flutter 提供有状态的 hot reload 开发模式,并允许一套 codebase 运行于多端;其他的,再比如开发采用 JIT 编译与发布的 AOT 编译,都使得开发者在开发应用时可以更加高效;

4. Open - Dart / Skia / Flutter (Framework),这些都是开源的,Flutter 与 Dart 团队也对包括 Web 在内的多种技术持开放态度,只要是优秀的他们都愿意借鉴吸收。而在生态建设上,Flutter 回应 GitHub Issue 的速度更是让人惊叹,因为是真的快(closed 状态的 issue 平均解决时间为 0.29天);

除了支持APICloud, Unity3d, React Native外,为什么要做Flutter下的RTSP/RTMP播放器

首先,Flutter则是依靠Flutter Engine虚拟机在iOS和Android上运行,开发人员可以通过Flutter框架和API在内部进行交互。Flutter Engine使用C/C++编写,具有低延迟输入和高帧速率的特点,不像Unity3d一样,我们是回调YUV/RGB数据,在Unity3d里面绘制,Flutter直接调用native SDK,效率更高。

其次,客户和开发者驱动,Flutter发展至今,目前还没有个像样的RTSP或RTMP播放器,一个播放器,不是说,有个界面,有个开始、停止按钮就可以了,一个好用的直播播放器,对功能和性能属性要求很高,特别是稳定性和低延迟这块,不谦虚的说,可能是首款功能强大、真正好用的Flutter RTSP/RTMP直播播放SDK

Android和iOS手机上RTSP/RTMP播放效果:

1. 视频播放效果:

2. 界面截图:

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

上接口:

////  smartplayer.dart//  smartplayer////  GitHub: https://github.com/daniulive/SmarterStreaming//  website: https://www.daniulive.com////  Created by daniulive on 2019/02/25.//  Copyright © 2014~2019 daniulive. All rights reserved.//import 'dart:async';import 'dart:convert';import 'package:flutter/services.dart';class EVENTID {  static const EVENT_DANIULIVE_COMMON_SDK = 0x00000000;  static const EVENT_DANIULIVE_PLAYER_SDK = 0x01000000;  static const EVENT_DANIULIVE_PUBLISHER_SDK = 0x02000000;  static const EVENT_DANIULIVE_ERC_PLAYER_STARTED =      EVENT_DANIULIVE_PLAYER_SDK | 0x1;  static const EVENT_DANIULIVE_ERC_PLAYER_CONNECTING =      EVENT_DANIULIVE_PLAYER_SDK | 0x2;  static const EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED =      EVENT_DANIULIVE_PLAYER_SDK | 0x3;  static const EVENT_DANIULIVE_ERC_PLAYER_CONNECTED =      EVENT_DANIULIVE_PLAYER_SDK | 0x4;  static const EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED =      EVENT_DANIULIVE_PLAYER_SDK | 0x5;  static const EVENT_DANIULIVE_ERC_PLAYER_STOP =      EVENT_DANIULIVE_PLAYER_SDK | 0x6;  static const EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO =      EVENT_DANIULIVE_PLAYER_SDK | 0x7;  static const EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED =      EVENT_DANIULIVE_PLAYER_SDK | 0x8;  static const EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL =      EVENT_DANIULIVE_PLAYER_SDK | 0x9;  static const EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE =      EVENT_DANIULIVE_PLAYER_SDK | 0xA;  static const EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE =      EVENT_DANIULIVE_PLAYER_SDK | 0x21; /*录像写入新文件*/  static const EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED =      EVENT_DANIULIVE_PLAYER_SDK | 0x22; /*一个录像文件完成*/  static const EVENT_DANIULIVE_ERC_PLAYER_START_BUFFERING =      EVENT_DANIULIVE_PLAYER_SDK | 0x81;  static const EVENT_DANIULIVE_ERC_PLAYER_BUFFERING =      EVENT_DANIULIVE_PLAYER_SDK | 0x82;  static const EVENT_DANIULIVE_ERC_PLAYER_STOP_BUFFERING =      EVENT_DANIULIVE_PLAYER_SDK | 0x83;  static const EVENT_DANIULIVE_ERC_PLAYER_DOWNLOAD_SPEED =      EVENT_DANIULIVE_PLAYER_SDK | 0x91;}typedef SmartEventCallback = void Function(int, String, String, String);class SmartPlayerController {  MethodChannel _channel;  EventChannel _eventChannel;  SmartEventCallback _eventCallback;  void init(int id) {    _channel = MethodChannel('smartplayer_plugin_$id');    _eventChannel = EventChannel('smartplayer_event_$id');    _eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);  }  void setEventCallback(SmartEventCallback callback) {    _eventCallback = callback;  }  void _onEvent(Object event) {    if (event != null) {      Map valueMap = json.decode(event);      String param = valueMap['param'];      onSmartEvent(param);    }  }  void _onError(Object error) {    // print('error:'+ error);  }  Future
_smartPlayerCall(String funcName) async { var ret = await _channel.invokeMethod(funcName); return ret; } Future
_smartPlayerCallInt(String funcName, int param) async { var ret = await _channel.invokeMethod(funcName, { 'intParam': param, }); return ret; } Future
_smartPlayerCallIntInt( String funcName, int param1, int param2) async { var ret = await _channel.invokeMethod(funcName, { 'intParam': param1, 'intParam2': param2, }); return ret; } Future
_smartPlayerCallString(String funcName, String param) async { var ret = await _channel.invokeMethod(funcName, { 'strParam': param, }); return ret; } /// 设置解码方式 false 软解码 true 硬解码 默认为false /// ///
Future
setVideoDecoderMode(int isHwDecoder) async { return _smartPlayerCallInt('setVideoDecoderMode', isHwDecoder); } ///
/// 设置音频输出模式: if 0: 自动选择; if with 1: audiotrack模式, 此接口仅限于Android平台使用 /// ///
Future
setAudioOutputType(int useAudiotrack) async { return _smartPlayerCallInt('setAudioOutputType', useAudiotrack); } ///
/// 设置播放端缓存大小, 默认200毫秒 /// ///
Future
setBuffer(int buffer) async { return _smartPlayerCallInt('setBuffer', buffer); } ///
/// 接口可实时调用:设置是否实时静音,1:静音; 0: 取消静音 /// ///
Future
setMute(int isMute) async { return _smartPlayerCallInt('setMute', isMute); } ///
/// 设置RTSP TCP模式, 1: TCP; 0: UDP /// ///
Future
setRTSPTcpMode(int isUsingTcp) async { return _smartPlayerCallInt('setRTSPTcpMode', isUsingTcp); } ///
/// 设置RTSP超时时间, timeout单位为秒,必须大于0 /// ///
Future
setRTSPTimeout(int timeout) async { return _smartPlayerCallInt('setRTSPTimeout', timeout); } ///
/// 设置RTSP TCP/UDP自动切换 /// 对于RTSP来说,有些可能支持rtp over udp方式,有些可能支持使用rtp over tcp方式. /// 为了方便使用,有些场景下可以开启自动尝试切换开关, 打开后如果udp无法播放,sdk会自动尝试tcp, 如果tcp方式播放不了,sdk会自动尝试udp. /// ///
Future
setRTSPAutoSwitchTcpUdp(int is_auto_switch_tcp_udp) async { return _smartPlayerCallInt('setRTSPAutoSwitchTcpUdp', is_auto_switch_tcp_udp); } ///
/// 设置快速启动该模式, /// ///
Future
setFastStartup(int isFastStartup) async { return _smartPlayerCallInt('setFastStartup', isFastStartup); } ///
/// 设置超低延迟模式 false不开启 true开启 默认false /// ///
Future
setPlayerLowLatencyMode(int mode) async { return _smartPlayerCallInt('setPlayerLowLatencyMode', mode); } ///
/// 设置视频垂直反转 /// ///
Future
setFlipVertical(int is_flip) async { return _smartPlayerCallInt('setFlipVertical', is_flip); } ///
/// 设置视频水平反转 /// ///
Future
setFlipHorizontal(int is_flip) async { return _smartPlayerCallInt('setFlipHorizontal', is_flip); } ///
/// 设置顺时针旋转, 注意除了0度之外, 其他角度都会额外消耗性能 /// degress: 当前支持 0度,90度, 180度, 270度 旋转 /// ///
Future
setRotation(int degress) async { return _smartPlayerCallInt('setRotation', degress); } ///
/// 设置是否回调下载速度 /// is_report: if 1: 上报下载速度, 0: 不上报. /// report_interval: 上报间隔,以秒为单位,>0. /// ///
///
Future
setReportDownloadSpeed( int isReport, int reportInterval) async { return _smartPlayerCallIntInt( 'setReportDownloadSpeed', isReport, reportInterval); } ///
/// Set playback orientation(设置播放方向),此接口仅适用于Android平台 /// ///
/// surOrg: current orientation, PORTRAIT 1, LANDSCAPE with 2 Future
setOrientation(int surOrg) async { return _smartPlayerCallInt('setOrientation', surOrg); } ///
/// 设置是否需要在播放或录像过程中快照 /// ///
Future
setSaveImageFlag(int isSaveImage) async { return _smartPlayerCallInt('setSaveImageFlag', isSaveImage); } ///
/// 播放或录像过程中快照 /// ///
Future
saveCurImage(String imageName) async { return _smartPlayerCallString('saveCurImage', imageName); } ///
/// 播放或录像过程中,快速切换url /// ///
Future
switchPlaybackUrl(String uri) async { return _smartPlayerCallString('switchPlaybackUrl', uri); } ///
/// 创建录像存储路径 /// ///
Future
createFileDirectory(String path) async { return _smartPlayerCallString('createFileDirectory', path); } ///
/// 设置录像存储路径 /// ///
Future
setRecorderDirectory(String path) async { return _smartPlayerCallString('setRecorderDirectory', path); } ///
/// 设置单个录像文件大小 /// ///
Future
setRecorderFileMaxSize(int size) async { return _smartPlayerCallInt('setRecorderFileMaxSize', size); } ///
/// 设置录像时音频转AAC编码的开关 /// aac比较通用,sdk增加其他音频编码(比如speex, pcmu, pcma等)转aac的功能. /// ///
/// is_transcode: 设置为1的话,如果音频编码不是aac,则转成aac,如果是aac,则不做转换. 设置为0的话,则不做任何转换. 默认是0. Future
setRecorderAudioTranscodeAAC(int is_transcode) async { return _smartPlayerCallInt('setRecorderAudioTranscodeAAC', is_transcode); } ///
/// 设置播放路径 /// Future
setUrl(String url) async { return _smartPlayerCallString('setUrl', url); } ///
/// 开始播放 /// Future
startPlay() async { return _smartPlayerCall('startPlay'); } ///
/// 停止播放 /// Future
stopPlay() async { return _smartPlayerCall('stopPlay'); } ///
/// 开始录像 /// Future
startRecorder() async { return _smartPlayerCall('startRecorder'); } ///
/// 停止录像 /// Future
stopRecorder() async { return _smartPlayerCall('stopRecorder'); } ///
/// 关闭播放 /// Future
dispose() async { return await _channel.invokeMethod('dispose'); } void onSmartEvent(String param) { if (!param.contains(",")) { print("[onNTSmartEvent] android传递参数错误"); return; } List
strs = param.split(','); String code = strs[1]; String param1 = strs[2]; String param2 = strs[3]; String param3 = strs[4]; String param4 = strs[5]; int evCode = int.parse(code); var p1, p2, p3; switch (evCode) { case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_STARTED: print("开始。。"); break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTING: print("连接中。。"); break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED: print("连接失败。。"); break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTED: print("连接成功。。"); break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED: print("连接断开。。"); break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_STOP: print("停止播放。。"); break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO: print("分辨率信息: width: " + param1 + ", height: " + param2); p1 = param1; p2 = param2; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED: print("收不到媒体数据,可能是url错误。。"); break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL: print("切换播放URL。。"); break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE: print("快照: " + param1 + " 路径:" + param3); if (int.parse(param1) == 0) { print("截取快照成功。."); } else { print("截取快照失败。."); } p1 = param1; p2 = param3; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE: print("[record]开始一个新的录像文件 : " + param3); p3 = param3; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED: print("[record]已生成一个录像文件 : " + param3); p3 = param3; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_START_BUFFERING: print("Start_Buffering"); break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_BUFFERING: print("Buffering: " + param1 + "%"); p1 = param1; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_STOP_BUFFERING: print("Stop_Buffering"); break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_DOWNLOAD_SPEED: print("download_speed:" + (double.parse(param1) * 8 / 1000).toStringAsFixed(0) + "kbps" + ", " + (double.parse(param1) / 1024).toStringAsFixed(0) + "KB/s"); p1 = param1; break; } if (_eventCallback != null) { _eventCallback(evCode, p1, p2, p3); } }}
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

调用实例:

////  main.dart//  main////  GitHub: https://github.com/daniulive/SmarterStreaming//  website: https://www.daniulive.com////  Created by daniulive on 2019/02/25.//  Copyright © 2014~2019 daniulive. All rights reserved.//import 'dart:io';import 'package:flutter/services.dart';import 'package:flutter/material.dart';import 'package:flutter/cupertino.dart';import 'package:flutter/foundation.dart';import 'package:smartplayer_native_view/smartplayer.dart';import 'package:smartplayer_native_view/smartplayer_plugin.dart';void main() {  ///  /// 强制竖屏  ///  SystemChrome.setPreferredOrientations(      [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);  runApp(new MyApp());}class MyApp extends StatefulWidget {  @override  _MyAppState createState() => new _MyAppState();}class _MyAppState extends State
{ SmartPlayerController player; double aspectRatio = 4.0 / 3.0; //输入需要播放的RTMP/RTSP url TextEditingController playback_url_controller_ = TextEditingController(); //Event事件回调显示 TextEditingController event_controller_ = TextEditingController(); bool is_playing_ = false; bool is_mute_ = false; var rotate_degrees_ = 0; Widget smartPlayerView() { return SmartPlayerWidget( onSmartPlayerCreated: onSmartPlayerCreated, ); } @override void initState() { print("initState called.."); super.initState(); } @override void didChangeDependencies() { print('didChangeDependencies called..'); super.didChangeDependencies(); } @override void deactivate() { print('deactivate called..'); super.deactivate(); } @override void dispose() { print("dispose called.."); player.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('Flutter SmartPlayer Demo'), ), body: new SingleChildScrollView( child: new Column( children:
[ new Container( color: Colors.black, child: AspectRatio( child: smartPlayerView(), aspectRatio: aspectRatio, ), ), new TextField( controller: playback_url_controller_, keyboardType: TextInputType.text, decoration: InputDecoration( contentPadding: EdgeInsets.all(10.0), icon: Icon(Icons.link), labelText: '请输入RTSP/RTMP url', ), autofocus: false, ), new Row( children: [ new RaisedButton( onPressed: this.onSmartPlayerStartPlay, child: new Text("开始播放")), new Container(width: 20), new RaisedButton( onPressed: this.onSmartPlayerStopPlay, child: new Text("停止播放")), new Container(width: 20), new RaisedButton( onPressed: this.onSmartPlayerMute, child: new Text("实时静音")), ], ), new Row( children: [ new RaisedButton( onPressed: this.onSmartPlayerSwitchUrl, child: new Text("实时切换URL")), new Container(width: 20), new RaisedButton( onPressed: this.onSmartPlayerSetRotation, child: new Text("实时旋转View")), ], ), new TextField( controller: event_controller_, keyboardType: TextInputType.text, decoration: InputDecoration( contentPadding: EdgeInsets.all(10.0), icon: Icon(Icons.event_note), labelText: 'Event状态回调', ), autofocus: false, ), ], ), )), ); } void _eventCallback(int code, String param1, String param2, String param3) { String event_str; switch (code) { case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_STARTED: event_str = "开始.."; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTING: event_str = "连接中.."; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED: event_str = "连接失败.."; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTED: event_str = "连接成功.."; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED: event_str = "连接断开.."; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_STOP: event_str = "停止播放.."; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO: event_str = "分辨率信息: width: " + param1 + ", height: " + param2; setState(() { aspectRatio = double.parse(param1) / double.parse(param2); print('change aspectRatio:$aspectRatio'); }); break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED: event_str = "收不到媒体数据,可能是url错误.."; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL: event_str = "切换播放URL.."; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE: event_str = "快照: " + param1 + " 路径: " + param3; if (int.parse(param1) == 0) { print("截取快照成功。."); } else { print("截取快照失败。."); } break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE: event_str = "[record] new file: " + param3; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED: event_str = "[record] record finished: " + param3; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_START_BUFFERING: //event_str = "Start Buffering"; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_BUFFERING: event_str = "Buffering: " + param1 + "%"; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_STOP_BUFFERING: //event_str = "Stop Buffering"; break; case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_DOWNLOAD_SPEED: event_str = "download_speed:" + (double.parse(param1) * 8 / 1000).toStringAsFixed(0) + "kbps" + ", " + (double.parse(param1) / 1024).toStringAsFixed(0) + "KB/s"; break; } event_controller_.text = event_str; } void onSmartPlayerCreated(SmartPlayerController controller) async { player = controller; player.setEventCallback(_eventCallback); var ret = -1; //设置video decoder模式 var is_video_hw_decoder = 0; if (defaultTargetPlatform == TargetPlatform.android) { ret = await player.setVideoDecoderMode(is_video_hw_decoder); } else if(defaultTargetPlatform == TargetPlatform.iOS) { is_video_hw_decoder = 1; ret = await player.setVideoDecoderMode(is_video_hw_decoder); } //设置缓冲时间 var play_buffer = 100; ret = await player.setBuffer(play_buffer); //设置快速启动 var is_fast_startup = 1; ret = await player.setFastStartup(is_fast_startup); //是否开启低延迟模式 var is_low_latency_mode = 0; ret = await player.setPlayerLowLatencyMode(is_low_latency_mode); //set report download speed(默认5秒一次回调 用户可自行调整report间隔) ret = await player.setReportDownloadSpeed(1, 2); //设置RTSP超时时间 var rtsp_timeout = 10; ret = await player.setRTSPTimeout(rtsp_timeout); var is_auto_switch_tcp_udp = 1; ret = await player.setRTSPAutoSwitchTcpUdp(is_auto_switch_tcp_udp); // 设置RTSP TCP模式 //ret = await player.setRTSPTcpMode(1); //第一次启动 为方便测试 设置个初始url playback_url_controller_.text = "rtmp://live.hkstv.hk.lxdns.com/live/hks2"; } Future
onSmartPlayerStartPlay() async { var ret = -1; if (playback_url_controller_.text.length < 8) { playback_url_controller_.text = "rtmp://live.hkstv.hk.lxdns.com/live/hks1"; //给个初始url } //实时静音设置 ret = await player.setMute(is_mute_ ? 1 : 0); if (!is_playing_) { ret = await player.setUrl(playback_url_controller_.text); ret = await player.startPlay(); if (ret == 0) { is_playing_ = true; } } } Future
onSmartPlayerStopPlay() async { if (is_playing_) { await player.stopPlay(); playback_url_controller_.clear(); is_playing_ = false; is_mute_ = false; } } Future
onSmartPlayerMute() async { if (is_playing_) { is_mute_ = !is_mute_; await player.setMute(is_mute_ ? 1 : 0); } } Future
onSmartPlayerSwitchUrl() async { if (is_playing_) { if (playback_url_controller_.text.length < 8) { playback_url_controller_.text = "rtmp://live.hkstv.hk.lxdns.com/live/hks1"; } await player.switchPlaybackUrl(playback_url_controller_.text); } } Future
onSmartPlayerSetRotation() async { if (is_playing_) { rotate_degrees_ += 90; rotate_degrees_ = rotate_degrees_ % 360; if (0 == rotate_degrees_) { print("旋转90度"); } else if (90 == rotate_degrees_) { print("旋转180度"); } else if (180 == rotate_degrees_) { print("旋转270度"); } else if (270 == rotate_degrees_) { print("不旋转"); } await player.setRotation(rotate_degrees_); } }}
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

经测试,Flutter环境下,RTMP和RTSP播放,拥有Native SDK一样优异的播放体验。

相关资料:Github: 

 

转载地址:http://fnxlx.baihongyu.com/

你可能感兴趣的文章
中缀表达式转换为后缀表达式
查看>>
NYOJ117 求逆序数
查看>>
Python模拟实现Linux系统unix2dos功能
查看>>
658. Find K Closest Elements - Medium
查看>>
[经典算法] 归并排序
查看>>
离下班还有几分钟,做个小玩意儿
查看>>
超星toPDF
查看>>
Java的演变过程
查看>>
js 正则
查看>>
009_Palindrome Number
查看>>
hdu 3091 Necklace(状态压缩类似于TSP问题)
查看>>
Fibonacci(...刷的前几道题没有记博客的习惯,吃了大亏)
查看>>
ECMAScript 5 —— 函数
查看>>
C++学习笔记之
查看>>
android 第一次作业
查看>>
[文献阅读]基于卷积神经网络的高光谱图像深度特征提取与分类
查看>>
springcloud记录
查看>>
回合制游戏比较适合小孩玩。。网页游戏可多使用回合制。
查看>>
[debian7] WARNING **: Could not connect to session bus
查看>>
【软件差错警示钟】一起软件差错造成的多名病人死亡事故
查看>>