一起看电影开发日志
知道有一个app叫做微光,可以多人一起在线看电视电影听音乐等,实质上类似于直播形式的点播应用。虽然很好用,但是其存在两个问题:一是所能看的视频都由用户申请后才可能添加,资源很有限,就是说只能是看上面有什么想看的看什么,而不能想看什么看什么;二是由于其资源由平台提供,存在很大的版权问题。
为了能够将自己本地有的视频资源在多终端同步观看,计划开发一个app。
整体规划(草稿,随日志更新)
整个项目将分为客户端服务端两部分。
初期简化考虑,客户端仅支持iOS,需要具备创建房间、选择本地文件、推流、记录播放进度、进入房间、拉流等功能。服务端仅支持linux,需具备直播服务器、房间信息管理等功能。
为了学习新技术及便于移植,计划iOS开发使用Flutter,服务端开发使用Go。
App流程应为:
- 创建房间与服务端交互,获得一对token,分别为房主身份和房客身份,同时服务端创建唯一直播服务器地址;
【低优先级。初步使用固定地址,之后可使用多端口或多路径,再之后考虑CDN或P2P】 - 选择本地视频,开始推流,推流同时记录播放进度;
- 房客使用token进入房间,拉流播放;
- 可利用记录的播放进度进行重新推流以进行同步,也可考虑支持手动调节进度。
日志记录
2020-03-14
测试本地macOS使用
ffmpeg
推流rtmp,服务端使用找到的一个golang编写的直播服务器golive
,手机端使用VLC播放体验。- 本地推流命令:
% brew install ffmpeg % ffmpeg -re -i <本地文件路径> -c copy -f flv <rtmp服务器地址>
- 使用livego:
% git 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
import '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环境中<key>UIFileSharingEnabled</key> <true/> <key>LSSupportsOpeningDocumentsInPlace</key> <true/>
ios/Runner
目录中修改,也可打开Xcode修改。% 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
部分如下:symlink = 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测试推流成功:
import '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的状态,可尝试单独执行下面命令:
% 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引入包的方式为:flutter_ijkplayer: path: ./flutter_ijkplayer flutter_ijkplayer: git: url: https://github.com/CaiJingLong/flutter_ijkplayer.git ref: master
- 使用代码如下:
import '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
修改IJK_FFMPEG_COMMIT=ff4.0--ijk0.8.25--20200221--001
config/module.sh
#下面注释掉是因为升级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"
- 执行如下命令进行编译:
% ./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目录,将真机和模拟器库合并为通用库:
% 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中修改如下:flutter_ijkplayer: #^0.3.5+1 path: ./flutter_ijkplayer
s.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项目之后编译问题再慢慢修改添加。mobileffmpeg.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
加入接口函数声明:
修改+ (int)executeWithArguments: (NSArray*)arguments;
Classses/IJKFFMoviePlayerController/ffmpeg/IJKFFMoviePlayerController.m
加入接口函数:+ (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
下加入如下代码:
在static 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
中加入如下代码:Future<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
中增加如下代码:
在- (NSDictionary *)toIntDictionary:(NSString*)key :(NSNumber*)value { NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; dictionary[key] = value; return dictionary; }
handleMethodCall
方法中头部加定义,并在其中判断call.method
的判断语句中加入分支如下:
如上述这几步骤后,即可令flutter_ffmpeg的主要功能“带参数执行ffmpeg命令”引入flutter_ijkplayer。但后来编译还发现NSArray* 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溢出)如下修复:/* 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
下:mobileffmpeg_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
下:attributes.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修改如下:
- 加入
#import <IJKMediaFramework/IJKMediaFramework.h>
- 将全局常量、成员变量和方法、invokeMethod的判断分支整合到
IjkplayerPlugin
中;
- 加入
- 将flutter_ffmpeg插件的lib中两个dart文件
- 在使用时即可flutter项目只设置pubspec使用本地修改过的flutter_ijkplayer,在需要用到flutter_ffmpeg的方法时
import 'package:flutter_ijkplayer/flutter_ffmpeg.dart';
- 至此,flutter选择文件、执行推流命令、拉流播放的基本功能点打通。接下来暂时不考虑客户端了,因为客户端除UI/UE外功能部分剩余为播放时间点记录与同步、创建进入房间,而这两点基本都需要仰仗于服务端,故接下来的计划是先改造服务端golang版流媒体服务器livego。