一起看电影开发日志
知道有一个app叫做微光,可以多人一起在线看电视电影听音乐等,实质上类似于直播形式的点播应用。虽然很好用,但是其存在两个问题:一是所能看的视频都由用户申请后才可能添加,资源很有限,就是说只能是看上面有什么想看的看什么,而不能想看什么看什么;二是由于其资源由平台提供,存在很大的版权问题。
为了能够将自己本地有的视频资源在多终端同步观看,计划开发一个app。
整体规划(草稿,随日志更新)
整个项目将分为客户端服务端两部分。
初期简化考虑,客户端仅支持iOS,需要具备创建房间、选择本地文件、推流、记录播放进度、进入房间、拉流等功能。服务端仅支持linux,需具备直播服务器、房间信息管理等功能。
为了学习新技术及便于移植,计划iOS开发使用Flutter,服务端开发使用Go。
App流程应为:
- 创建房间与服务端交互,获得一对token,分别为房主身份和房客身份,同时服务端创建唯一直播服务器地址;
【低优先级。初步使用固定地址,之后可使用多端口或多路径,再之后考虑CDN或P2P】 - 选择本地视频,开始推流,推流同时记录播放进度;
- 房客使用token进入房间,拉流播放;
- 可利用记录的播放进度进行重新推流以进行同步,也可考虑支持手动调节进度。
日志记录
2020-03-14
测试本地macOS使用
ffmpeg
推流rtmp,服务端使用找到的一个golang编写的直播服务器golive
,手机端使用VLC播放体验。- 本地推流命令:
1
2brew install ffmpeg
ffmpeg -re -i <本地文件路径> -c copy -f flv <rtmp服务器地址> - 使用livego:
1
2
3git clone https://github.com/gwuhaolin/livego.git
go build
./livego
- 本地推流命令:
2020-03-16
要支持将手机本地视频推流到服务器,肯定先得支持读取本地文件,找到Flutter pub包
path_provider
- 使用path_provider,在Flutter项目的pubspec.yaml中dependencies下加入path_provider: ^1.6.5
1
2
3
4
5
6
7
8
9
10
11
12import 'dart:io';
import 'dart:async';
import 'package:path_provider/path_provider.dart';
//获取应用文档目录
String dir = (await getApplicationDocumentsDirectory()).path;
//创建文件
File file = new File('$dir/counter.txt');
//文档读取
String content = await file.readAsString();
//文档写入
await file.writeAsString('$_counter'); - 要想让App的文档目录在iOS的“文件”中可见,需要在Info.plist文件中添加键值对: 这一步可在Flutter的VS Code环境中
1
2
3
4<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>ios/Runner
目录中修改,也可打开Xcode修改。1
open ios/Runner.xcworkspace
- 使用path_provider,在Flutter项目的pubspec.yaml中dependencies下加入path_provider: ^1.6.5
要实现iOS端推流,需要在iOS端集成ffmpeg。找到Flutter pub包flutter_ffmpeg
使用flutter_ffmpeg,在Flutter项目的pubspec.yaml中dependencies下加入flutter_ffmpeg: ^0.2.10
iOS使用flutter_ffmpeg,还需要修改
Podfile
的# Plugin Pods
部分如下:1
2
3
4
5
6
7symlink = File.join('.symlinks', 'plugins', name)
File.symlink(path, symlink)
if name == 'flutter_ffmpeg'
pod name+'/<package name>', :path => File.join(symlink, 'ios')
else
pod name, :path => File.join(symlink, 'ios')
end若找不到Podfile,尝试执行run后会生成
flutter_ffmpeg测试推流成功:
1
2
3
4
5
6
7
8import 'package:flutter_ffmpeg/flutter_ffmpeg.dart';
final FlutterFFmpeg _flutterFFmpeg = new FlutterFFmpeg();
String dir = (await getApplicationDocumentsDirectory()).path;
_flutterFFmpeg
.execute(
"-re -i $dir/<视频文件名> -c copy -f flv <直播服务器地址>")
.then((rc) => print("FFmpeg process exited with rc $rc"));
在安装这些包,尝试运行时常遇到卡在Pod Installing的状态,可尝试单独执行下面命令:
1
pod install --verbose
视频播放部分,考虑使用Flutter版的ijkplayer中……
2020-03-17
- 经过一晚的测试,发现
flutter_ijkplayer
主要存在两个问题:- 加入该包后会出现闪退,经调试发现,可能的原因是其本身也是基于ffmpeg的,所以会与已经引入的flutter_ffmpeg相冲突,当两个包都被引入,只要调用ffmpeg就会闪退;
- 后暂时去掉flutter_ffmpeg包,仅引入此包,发现无论播放本地mp4还是播放rtmp地址视频,均是有声音无图像(黑屏)。初步查询问题时根据网上说法以为可能是对mp4格式支持问题(但同时播放采用flv的rtmp流也有这问题其实可以排除这种可能性),尝试修改编译选项自编译flutter_ijkplayer,发现本身其默认编译选项就是很全的,而且替换为自编译的包之后,依然同样问题。后阅读flutter_ijkplayer的TODOList,发现其中写道:
- iOS 部分视频无法显示图像的问题: 可能很长时间内都无法解决
- 本身该包的使用方法还是记录一下吧:
- 在Flutter项目的pubspec.yaml中dependencies下加入flutter_ijkplayer: ^0.3.5+1
另外从本地和从Git引入包的方式为:1
2
3
4
5
6
7flutter_ijkplayer:
path: ./flutter_ijkplayer
flutter_ijkplayer:
git:
url: https://github.com/CaiJingLong/flutter_ijkplayer.git
ref: master - 使用代码如下:
1
2
3
4
5import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
String dir = (await getApplicationDocumentsDirectory()).path;
await controller.setDataSource(DataSource.file(File('$dir/BCSS05E01.mp4')),autoPlay: true);
// await controller.setNetworkDataSource('<rtmp服务器地址>',autoPlay:true);
- 在Flutter项目的pubspec.yaml中dependencies下加入flutter_ijkplayer: ^0.3.5+1
- 开始尝试其他Flutter下的播放器:分别尝试了flutter_vlc_player【本身体积比较大,且编译无法通过,会出现precompile issue,搁置】和video_player【编译正常,但使用中控件总是不显示,判断
controller.value.initialized
总为false】 - 又尝试了一遍flutter_ijkplayer,使用线上版本,发现可以播放rtmp地址视频了!但是似乎有些不稳定。
2020-03-20
- 之前尝试flutter_ijkplayer成功,但是依然存在和flutter_ffmpeg冲突的问题。这是因为两者最底层都用到了ffmpeg,从而导致有duplicated symbol。另外,flutter_ijkplayer基于的ffmpeg版本本身是3.4的(可更改支持到4.0),而flutter_ffmpeg基于的mobile_ffmpeg是基于ffmpeg4.3的,这两者版本也不一致。
- 经过两天多的痛苦尝试,终于有了进展:
先将
flutter_ijkplayer
的使用方式更改为本地包的使用方式:- 将flutter_ijkplayer包下到本地,放置在flutter项目目录中;
- 获取ijkplayer源码进行本地编译,修改
init-ios.sh
修改1
IJK_FFMPEG_COMMIT=ff4.0--ijk0.8.25--20200221--001
config/module.sh
1
2
3
4
5
6
7
8
9
10#下面注释掉是因为升级ffmpeg到4.0版
#export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-ffserver"
#export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-vda"
#下面添加muxer是因为后面步骤需要支持flv的output format
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-muxer=flv"
#下面两个不确定是否要加(源于升级到4.0版ffmpeg)
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-protocol=https"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-bsf=eac3_core" - 执行如下命令进行编译:
1
2
3
4
5
6
7% ./init-config.sh
% ./init-ios.sh
% cd ios
% ./compile-ffmpeg.sh clean
% ./compile-common.sh
% open ios/IJKMediaPlayer/IJKMediaPlayer.xcodeproj - Edit Scheme中修改Run的构建配置为Release,然后分别选择构建目标为模拟器(如iPhone 8 Pus)和真机(Generic iOS Device),按
Command+b
进行编译构建; - 之后进入生成的framework目录,将真机和模拟器库合并为通用库:
1
2
3
4% cd ~/Library/Developer/Xcode/DerivedData/IJKMediaPlayer-?????????/Build/Products
% lipo -create Release-iphoneos/IJKMediaFramework.framework/IJKMediaFramework Release-iphonesimulator/IJKMediaFramework.framework/IJKMediaFramework -output IJKMediaFramework
% cp IJKMediaFramework Release-iphoneos/IJKMediaFramework.framework
% open Release-iphoneos/ - 将得到的
IJKMediaFramework.framework
复制进前面第一步flutter_ijkplayer目录中ios目录下; - 本地包的引入为修改
pubspec.yaml
的依赖项如下:在flutter_ijkplayer本地包的ios/.podspec中修改如下:1
2flutter_ijkplayer: #^0.3.5+1
path: ./flutter_ijkplayer1
2
3
4s.ios.vendored_frameworks = 'IJKMediaFramework.framework'
s.frameworks = "AudioToolbox", "AVFoundation", "CoreGraphics", "CoreMedia", "CoreVideo", "MobileCoreServices", "OpenGLES", "QuartzCore", "VideoToolbox", "Foundation", "UIKit", "MediaPlayer"
s.libraries = "bz2", "z", "stdc++"
#s.dependency 'FlutterIJK', '~> 0.2.3'
接下来就是尝试将
mobile-ffmpeg
整合进ijkplayer
,尝试将flutter_ffmpeg
整合进flutter_ijkplayer
。其中前两者是iOS库,后两者是Flutter包。在整合过程中本着最小化原则,且尽可能只做增量。- 【mobile-ffmpeg】入【ijkplayer】:在上一环节第3步后,在打开的Xcode中进行操作,将下列文件拖拽复制添加在左侧工程文件树的
Classses/IJKFFMoviePlayerController/ijkmedia/ijkplayer
下:其实以添加mobile_ffmpeg为主,逐渐发现编译问题及后期加入Flutter项目之后编译问题再慢慢修改添加。1
2
3
4
5
6
7
8
9
10
11
12
13mobileffmpeg.c
mobileffmpeg.h
ffmpeg.c
ffmpeg.h
cmdutils.c
cmdutils.h
ffmpeg_opt.c
av_device.h
av_device.c
ffmpeg_hw.c
ffmpeg_filter.c
exception.c
exception.h - 【mobile-ffmpeg】入【ijkplayer】:依然在Xcode中,修改
Classses/IJKFFMoviePlayerController/ffmpeg/IJKFFMoviePlayerController.h
加入接口函数声明:修改1
+ (int)executeWithArguments: (NSArray*)arguments;
Classses/IJKFFMoviePlayerController/ffmpeg/IJKFFMoviePlayerController.m
加入接口函数:1
2
3
4
5
6
7
8
9
10
11
12
13+ (int)executeWithArguments: (NSArray*)arguments {
char **commandCharPArray = (char **)av_malloc(sizeof(char*) * ([arguments count]));
for (int i=0; i < [arguments count]; i++) {
NSString *argument = [arguments objectAtIndex:i];
commandCharPArray[i] = (char *) [argument UTF8String];
}
int lastReturnCode = mobileffmpeg_execute(([arguments count]), commandCharPArray);
av_free(commandCharPArray);
return lastReturnCode;
}注意:修改完成后重复进行前一环节的后续步骤。
- 【flutter-ffmpeg】入【fluter-ijkplayer】:在本地包flutter_ijkplayer下
lib/src/controller.dart
下加入如下代码:在1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57static List<String> parseArguments(String command) {
List<String> argumentList = new List();
StringBuffer currentArgument = new StringBuffer();
bool singleQuoteStarted = false;
bool doubleQuoteStarted = false;
for (int i = 0; i < command.length; i++) {
var previousChar;
if (i > 0) {
previousChar = command.codeUnitAt(i - 1);
} else {
previousChar = null;
}
var currentChar = command.codeUnitAt(i);
if (currentChar == ' '.codeUnitAt(0)) {
if (singleQuoteStarted || doubleQuoteStarted) {
currentArgument.write(String.fromCharCode(currentChar));
} else if (currentArgument.length > 0) {
argumentList.add(currentArgument.toString());
currentArgument = new StringBuffer();
}
} else if (currentChar == '\''.codeUnitAt(0) &&
(previousChar == null || previousChar != '\\'.codeUnitAt(0))) {
if (singleQuoteStarted) {
singleQuoteStarted = false;
} else if (doubleQuoteStarted) {
currentArgument.write(String.fromCharCode(currentChar));
} else {
singleQuoteStarted = true;
}
} else if (currentChar == '\"'.codeUnitAt(0) &&
(previousChar == null || previousChar != '\\'.codeUnitAt(0))) {
if (doubleQuoteStarted) {
doubleQuoteStarted = false;
} else if (singleQuoteStarted) {
currentArgument.write(String.fromCharCode(currentChar));
} else {
doubleQuoteStarted = true;
}
} else {
currentArgument.write(String.fromCharCode(currentChar));
}
}
if (currentArgument.length > 0) {
argumentList.add(currentArgument.toString());
}
return argumentList;
}
Future<int> executeWithArguments(String arguments) async {
_ijkStatus = IjkStatus.preparing;
await _initDataSource(false);
final Map<dynamic, dynamic> result = await _plugin
.executeFFmpegWithArguments(arguments: parseArguments(arguments));
_ijkStatus = IjkStatus.prepared;
return result['rc'];
}lib/src/controller/plugin.dart
中加入如下代码:1
2
3
4
5
6
7Future<Map<dynamic, dynamic>> executeFFmpegWithArguments(
{List<String> arguments} ) async {
if (isDisposed) {
return null;
}
return await channel.invokeMethod("executeFFmpegWithArguments", <String, dynamic>{'arguments': arguments});
} - 【flutter-ffmpeg】入【fluter-ijkplayer】:在本地包flutter_ijkplayer下
ios/Classes/CoolFlutterIJK.m
中增加如下代码:在1
2
3
4
5- (NSDictionary *)toIntDictionary:(NSString*)key :(NSNumber*)value {
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
dictionary[key] = value;
return dictionary;
}handleMethodCall
方法中头部加定义,并在其中判断call.method
的判断语句中加入分支如下:如上述这几步骤后,即可令flutter_ffmpeg的主要功能“带参数执行ffmpeg命令”引入flutter_ijkplayer。但后来编译还发现1
2
3
4
5
6
7
8
9
10
11
12NSArray* arguments = call.arguments[@"arguments"];
NSString* command = call.arguments[@"command"];
NSString* delimiter = call.arguments[@"delimiter"];
else if ([@"executeFFmpegWithArguments" isEqualToString:call.method]) {
NSLog(@"Running FFmpeg with arguments: %@.\n", arguments);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
int rc = [IJKFFMoviePlayerController executeWithArguments:arguments];
NSLog(@"FFmpeg exited with rc: %d\n", rc);
result([self toIntDictionary:@"rc" :[NSNumber numberWithInt:rc]]);
});
}ffmpeg.c
中存在个小问题(似乎是fd_set溢出)如下修复:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/*
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(0, &rfds);
*/
struct pollfd rfds;
rfds.fd=0;
rfds.events=POLLIN;
tv.tv_sec = 0; //未改动
tv.tv_usec = 0; //未改动
// n = select(1, &rfds, NULL, NULL, &tv);
n=poll(&rfds,1,&tv);
- 【mobile-ffmpeg】入【ijkplayer】:在上一环节第3步后,在打开的Xcode中进行操作,将下列文件拖拽复制添加在左侧工程文件树的
经过前面的整合,在Flutter项目中使用
controller.executeWithArguments
和controller.setNetworkDataSource
均可成功了(需要注意这两个均是async函数,谨慎使用await放置前一操作的等待阻拦后一操作)但如此做似乎仍然存在问题,那便是在点击按钮执行一次
executeWithArguments
后如果再点一次按钮执行此操作即会闪退。这个问题还需要解决,因为app中可能存在需求需要在已发推流后重发推流。那么这种需要两种实现,要么设法取消前一命令,要么重推一命令覆盖掉前一命令。前者需研究如何实现通信,后者则需解决当前的这个“再触发闪退问题”。
2020-03-24
- 如之前记录,尝试将flutter_ffmpeg(mobile_ffmpeg)整合进flutter_ijkplayer后出现在第一次发出ffmpeg命令后再次发出会有读空数据的情况导致闪退。认为该情况是由于某种原因在再次执行命令时,使得前一次或后一次执行的变量被清除所致。
- 由于之前是尝试代码级整合,就用的和ijkplayer使用的ffmpeg版本(4.0)最接近的旧版mobile_ffmpeg(v2.0)。然后和flutter_ffmpeg插件使用的最新版mobile_ffmpeg进行比对,发现新版中很多变量被改为
__thread
(即线程本地)的,怀疑此可以解决变量被清除的问题。于是尝试手动对照修改已整合入的旧版的代码。但是经过尝试,发现改完后虽然不会闪退,但却出现更坏情况,命令参数解析的环节报错无效参数“-re”,断入手动追踪,发现在“cmdutil.c”中的split_commandline()
函数while循环中第一处if判断时还为正常“-re”值得opt在第二处if时就变成了NULL……之后经过再三思考与查询,也并未有头绪。 - 之后索性放弃代码级整合,尝试直接将最新版mobile_ffmpeg(及flutter_ffmpeg)的外层代码(及除ffmpeg之外由mobile_ffmpeg添加的,位于
mobile_ffmpeg/ios/src
)文件直接引入使用,即改为文件级别整合:- 最初的步骤跟前面的一样,构建flutter_ijkplayer本地库(使用ffmpeg4.0);
- 之后将mobile_ffmpeg(最新版)整合进ijkplayer framework的方式为:
- 将下列文件Xcode中放入IJKMediaFramework工程的
Classes/IJKFFMoviePlayerController/ffmpeg
下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20mobileffmpeg_exception.h
mobileffmpeg_exception.m
MobileFFmpeg.h
MobileFFmpeg.m
ArchDetect.h
ArchDetect.m
LogDelegate.h
MediaInformation.h
MediaInformation.m
MediaInformationParser.h
MediaInformationParser.m
MobileFFmpegConfig.h
MobileFFmpegConfig.m
MobileFFprobe.h
MobileFFprobe.m
Statistics.h
Statistics.m
StatisticsDelegate.h
StreamInformation.h
StreamInformation.m - 将下列文件Xcode放入IJKMediaFramework工程的
Classes/IJKFFMoviePlayerController/ijkmedia/ijkplayer
下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18attributes.h
avdevice.h
avio.h
fftools_cmdutils.c
fftools_cmdutils.h
fftools_ffmpeg_filter.c
fftools_ffmpeg_hw.c
fftools_ffmpeg_opt.c
fftools_ffmpeg.c
fftools_ffmpeg.h
fftools_ffprobe.c
intfloat.h
libm.h
mathematics.h
network.h
os_support.h
url.h
version.h - 之后将其中一些include路径进行订正(因为由原目录层级变成可直接使用的)
- 需要将之后要暴露在framework库外供Flutter Plugin调用的头文件设置为公共可见
- 将下列文件Xcode中放入IJKMediaFramework工程的
- 将flutter_ffmpeg整合入flutter_ijkplayer插件,最开始是想依然将原文件保留(即进行文件级别整合),但是会出现invokeMethod无法在Channel中找到的情况,后来发现,flutter插件需要在其
pubspec.yaml
中flutter->plugin->platforms->ios->pluginClass
设置插件类,之后在构建过程中便会由此在项目(而非插件)的ios/Runner
下生成GeneratedPluginRegistrant.h
和GeneratedPluginRegistrant.m
。其m文件结构如下:
也即是说,只有登记在插件的pubspec.yaml
中的插件类,才会被调用其registerWithRegistrar
方法,从而注册MethodChannel等。尝试在一个插件的pubspec中注册两个插件类,似乎没有办法。于是,也只能在Flutter插件这边,Dart文件进行文件级整合、iOS实现进行代码(类)级别整合:- 将flutter_ffmpeg插件的lib中两个dart文件
flutter_ffmpeg.dart
和log_level.dart
复制到本地flutter_ijkplayer的lib文件夹中; - 将前一步骤生成的Framework目录放入本地flutter_ijkplayer的ios文件夹中;
- 在flutter_ijkplayer的ios文件夹中将IjkplayerPlugin.m修改如下:
- 加入
1
- 将全局常量、成员变量和方法、invokeMethod的判断分支整合到
IjkplayerPlugin
中;
- 加入
- 将flutter_ffmpeg插件的lib中两个dart文件
- 在使用时即可flutter项目只设置pubspec使用本地修改过的flutter_ijkplayer,在需要用到flutter_ffmpeg的方法时
import 'package:flutter_ijkplayer/flutter_ffmpeg.dart';
- 至此,flutter选择文件、执行推流命令、拉流播放的基本功能点打通。接下来暂时不考虑客户端了,因为客户端除UI/UE外功能部分剩余为播放时间点记录与同步、创建进入房间,而这两点基本都需要仰仗于服务端,故接下来的计划是先改造服务端golang版流媒体服务器livego。