diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a5b4f05..c3fb8e50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ ## next -------------------------------- +## 0.1.0 +- update ijkplauer to f0.3.5 +- fijkplayer err state and FijkException +- support playing flutter asset file +- unit test and widget test +- pass fijkoption arguments and set player's option + ## 0.0.9 -------------------------------- - update ijkplayer to f0.3.4 diff --git a/README.en.md b/README.en.md index 77ed51a6..5cc96c8a 100644 --- a/README.en.md +++ b/README.en.md @@ -1,5 +1,6 @@ # fijkplayer (Video player plugin for Flutter) + [![pub package](https://img.shields.io/pub/v/fijkplayer.svg)](https://pub.dartlang.org/packages/fijkplayer)     [![Build Status](https://travis-ci.org/befovy/fijkplayer.svg?branch=master)](https://travis-ci.org/befovy/fijkplayer)     @@ -9,7 +10,6 @@ A Flutter media player plugin for iOS and android based on [ijkplayer](https://g *Read this in other languages: [English](README.en.md), [简体中文](README.zh-cn.md).* -*Note*: This plugin is still under development, and some APIs might not be available yet. [Feedback welcome](https://github.com/befovy/fijkplayer/issues) and [Pull Requests](https://github.com/befovy/fijkplayer/pulls) are most welcome! @@ -19,7 +19,7 @@ Add `fijkplayer` as a [dependency in your pubspec.yaml file](https://flutter.io/ ```yaml dependencies: - fijkplayer: ^0.0.6 + fijkplayer: ^0.1.0 ``` diff --git a/README.md b/README.md deleted file mode 120000 index b636b478..00000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -README.zh-cn.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..5cc96c8a --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# fijkplayer (Video player plugin for Flutter) + + +[![pub package](https://img.shields.io/pub/v/fijkplayer.svg)](https://pub.dartlang.org/packages/fijkplayer)     +[![Build Status](https://travis-ci.org/befovy/fijkplayer.svg?branch=master)](https://travis-ci.org/befovy/fijkplayer)     + +A Flutter media player plugin for iOS and android based on [ijkplayer](https://github.com/befovy/ijkplayer) + + +*Read this in other languages: [English](README.en.md), [简体中文](README.zh-cn.md).* + + +[Feedback welcome](https://github.com/befovy/fijkplayer/issues) and +[Pull Requests](https://github.com/befovy/fijkplayer/pulls) are most welcome! + +## Installation + +Add `fijkplayer` as a [dependency in your pubspec.yaml file](https://flutter.io/using-packages/). + +```yaml +dependencies: + fijkplayer: ^0.1.0 +``` + + +## Example + +```dart +import 'package:fijkplayer/fijkplayer.dart'; +import 'package:flutter/material.dart'; + +class VideoScreen extends StatefulWidget { + final String url; + + VideoScreen({@required this.url}); + + @override + _VideoScreenState createState() => _VideoScreenState(); +} + +class _VideoScreenState extends State { + final FijkPlayer player = FijkPlayer(); + + _VideoScreenState(); + + @override + void initState() { + super.initState(); + player.setDataSource(widget.url, autoPlay: true); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("Fijkplayer Example")), + body: Container( + alignment: Alignment.center, + child: FijkView( + player: player, + ), + )); + } + + @override + void dispose() { + super.dispose(); + player.release(); + } +} + +``` + +## Demo Screenshots + +iOS screenshots +
+ios_input +      +ios_video +
+ +android screenshots + +
+android_home +      +android_video +
+ + + +## iOS Warning + +Warning: The fijkplayer video player plugin is not functional on iOS simulators. An iOS device must be used during development/testing. For more details, please refer to this [issue](https://github.com/flutter/flutter/issues/14647). + diff --git a/README.zh-cn.md b/README.zh-cn.md index d6702ffd..7e70e0ff 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -22,7 +22,7 @@ ```yaml dependencies: - fijkplayer: ^0.0.9 + fijkplayer: ^0.1.0 ``` ## 基础用法 @@ -111,8 +111,7 @@ pod 'FIJKPlayer' ```gradle dependencies { // fijkplayer-full include the java lib and native shared libs for armv5 armv7 arm64 x86 x86_64 - implementation 'com.befovy.fijkplayer:fijkplayer-full:0.3.4' - implementation 'com.befovy.fijkplayer:fijkplayer-full:0.3.4' + implementation 'com.befovy.fijkplayer:fijkplayer-full:0.3.5' } ``` diff --git a/android/.gitignore b/android/.gitignore index c6cbe562..bf1af7d5 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -6,3 +6,4 @@ .DS_Store /build /captures +/aars diff --git a/android/build.gradle b/android/build.gradle index 35ea4f66..1a6b99b6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -32,13 +32,19 @@ android { lintOptions { disable 'InvalidPackage' } + + aaptOptions { + noCompress '.flv', '.mp4' + } + } dependencies { // implementation(name: 'fijkplayer-full-release', ext: 'aar') + // fijkplayer-full include the java lib and native shared libs for armv5 armv7 arm64 x86 x86_64 - implementation 'com.befovy.fijkplayer:fijkplayer-full:0.3.4' + implementation 'com.befovy.fijkplayer:fijkplayer-full:0.3.5' implementation 'com.android.support:support-annotations:28.0.0' } diff --git a/android/src/main/java/com/befovy/fijkplayer/FijkPlayer.java b/android/src/main/java/com/befovy/fijkplayer/FijkPlayer.java index 7ebefee1..b134b902 100644 --- a/android/src/main/java/com/befovy/fijkplayer/FijkPlayer.java +++ b/android/src/main/java/com/befovy/fijkplayer/FijkPlayer.java @@ -1,7 +1,6 @@ package com.befovy.fijkplayer; import android.content.Context; -import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import android.graphics.SurfaceTexture; import android.net.Uri; @@ -12,7 +11,9 @@ import androidx.annotation.NonNull; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -30,7 +31,22 @@ public class FijkPlayer implements MethodChannel.MethodCallHandler, IjkEventList final private static AtomicInteger atomicId = new AtomicInteger(0); + final private static int idle = 0; + final private static int initialized = 1; + final private static int asyncPreparing = 2; + @SuppressWarnings("unused") + final private static int prepared = 3; + @SuppressWarnings("unused") + final private static int started = 4; + final private static int paused = 5; + final private static int completed = 6; + final private static int stopped = 7; + @SuppressWarnings("unused") + final private static int error = 8; + final private static int end = 9; + final private int mPlayerId; + private int mState; final private IjkMediaPlayer mIjkMediaPlayer; final private Context mContext; @@ -53,6 +69,7 @@ public class FijkPlayer implements MethodChannel.MethodCallHandler, IjkEventList FijkPlayer(PluginRegistry.Registrar registrar) { mRegistrar = registrar; mPlayerId = atomicId.incrementAndGet(); + mState = 0; mIjkMediaPlayer = new IjkMediaPlayer(); mIjkMediaPlayer.addIjkEventListener(this); @@ -93,9 +110,8 @@ long setupSurface() { } void release() { - mIjkMediaPlayer.stop(); + handleEvent(PLAYBACK_STATE_CHANGED, end, mState, null); mIjkMediaPlayer.release(); - if (mSurfaceTextureEntry != null) { mSurfaceTextureEntry.release(); mSurfaceTextureEntry = null; @@ -121,6 +137,7 @@ private void handleEvent(int what, int arg1, int arg2, Object extra) { mEventSink.success(event); break; case PLAYBACK_STATE_CHANGED: + mState = arg1; event.put("event", "state_change"); event.put("new", arg1); event.put("old", arg2); @@ -151,6 +168,9 @@ private void handleEvent(int what, int arg1, int arg2, Object extra) { event.put("height", arg2); mEventSink.success(event); break; + case ERROR: + mEventSink.error(String.valueOf(arg1), extra.toString(), arg2); + break; default: // Log.d("FLUTTER", "jonEvent:" + what); break; @@ -166,6 +186,7 @@ public void onEvent(IjkMediaPlayer ijkMediaPlayer, int what, int arg1, int arg2, case BUFFERING_END: case BUFFERING_UPDATE: case VIDEO_SIZE_CHANGED: + case ERROR: handleEvent(what, arg1, arg2, extra); break; default: @@ -200,6 +221,7 @@ private void applyOptions(Object options) { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + //noinspection IfCanBeSwitch if (call.method.equals("setupSurface")) { long viewId = setupSurface(); result.success(viewId); @@ -223,7 +245,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result String url = call.argument("url"); Uri uri = Uri.parse(url); boolean openAsset = false; - if ("assets".equals(uri.getScheme())) { + if ("asset".equals(uri.getScheme())) { openAsset = true; String host = uri.getHost(); String path = uri.getPath() != null ? uri.getPath().substring(1) : ""; @@ -237,9 +259,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result try { if (openAsset) { AssetManager assetManager = mRegistrar.context().getAssets(); - AssetFileDescriptor fd = assetManager.openFd(uri.getPath() != null ? uri.getPath() : ""); - //mIjkMediaPlayer.setDataSource(fd.getFileDescriptor()); - mIjkMediaPlayer.setDataSource(new RawMediaDataSource(fd, mRegistrar.context(), uri.getPath())); + InputStream is = assetManager.open(uri.getPath() != null ? uri.getPath() : "", AssetManager.ACCESS_RANDOM); + mIjkMediaPlayer.setDataSource(new RawMediaDataSource(is)); } else { if (TextUtils.isEmpty(uri.getScheme()) || "file".equals(uri.getScheme())) { IMediaDataSource dataSource = new FileMediaDataSource(new File(uri.toString())); @@ -248,12 +269,16 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result mIjkMediaPlayer.setDataSource(mContext, uri); } } + handleEvent(PLAYBACK_STATE_CHANGED, initialized, -1, null); result.success(null); + } catch (FileNotFoundException e) { + result.error("-875574348", "Local File not found:" + e.getMessage(), null); } catch (IOException e) { - result.error(e.getMessage(), null, null); + result.error("-1162824012", "Local IOException:" + e.getMessage(), null); } } else if (call.method.equals("prepareAsync")) { mIjkMediaPlayer.prepareAsync(); + handleEvent(PLAYBACK_STATE_CHANGED, asyncPreparing, -1, null); result.success(null); } else if (call.method.equals("start")) { mIjkMediaPlayer.start(); @@ -263,9 +288,11 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result.success(null); } else if (call.method.equals("stop")) { mIjkMediaPlayer.stop(); + handleEvent(PLAYBACK_STATE_CHANGED, stopped, -1, null); result.success(null); } else if (call.method.equals("reset")) { mIjkMediaPlayer.reset(); + handleEvent(PLAYBACK_STATE_CHANGED, idle, -1, null); result.success(null); } else if (call.method.equals("getCurrentPosition")) { long pos = mIjkMediaPlayer.getCurrentPosition(); @@ -277,12 +304,13 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result.success(null); } else if (call.method.equals("seekTo")) { final Integer msec = call.argument("msec"); + if (mState == completed) + handleEvent(PLAYBACK_STATE_CHANGED, paused, -1, null); mIjkMediaPlayer.seekTo(msec != null ? msec.longValue() : 0); result.success(null); } else if (call.method.equals("setLoop")) { final Integer loopCount = call.argument("loop"); - // todo update ijkplayer, add set loop count api - mIjkMediaPlayer.setLooping(loopCount != null && loopCount == 0); + mIjkMediaPlayer.setLoopCount(loopCount != null ? loopCount : 1); result.success(null); } else if (call.method.equals("setSpeed")) { final Double speed = call.argument("speed"); diff --git a/android/src/main/java/com/befovy/fijkplayer/FijkPlugin.java b/android/src/main/java/com/befovy/fijkplayer/FijkPlugin.java index e5eb9c58..ba056875 100644 --- a/android/src/main/java/com/befovy/fijkplayer/FijkPlugin.java +++ b/android/src/main/java/com/befovy/fijkplayer/FijkPlugin.java @@ -11,7 +11,6 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.Registrar; -import io.flutter.view.TextureRegistry; /** * FijkPlugin diff --git a/android/src/main/java/com/befovy/fijkplayer/RawMediaDataSource.java b/android/src/main/java/com/befovy/fijkplayer/RawMediaDataSource.java index d05319f4..2720ea8e 100644 --- a/android/src/main/java/com/befovy/fijkplayer/RawMediaDataSource.java +++ b/android/src/main/java/com/befovy/fijkplayer/RawMediaDataSource.java @@ -1,76 +1,41 @@ package com.befovy.fijkplayer; -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.util.Base64; - -import java.io.BufferedInputStream; -import java.io.File; import java.io.IOException; -import java.io.RandomAccessFile; +import java.io.InputStream; import tv.danmaku.ijk.media.player.misc.IMediaDataSource; public class RawMediaDataSource implements IMediaDataSource { - private AssetFileDescriptor mDescriptor; - private BufferedInputStream mStream; - private RandomAccessFile mRandomAccessFile; - private long writeSize; - - public RawMediaDataSource(AssetFileDescriptor descriptor, Context context, String asset) { - this.mDescriptor = descriptor; - File cacheFile = new File(context.getCacheDir().getAbsolutePath(), Base64.encodeToString(asset.getBytes(), Base64.DEFAULT)); + private InputStream mIs; + private long mPosition = 0; - try { - mStream = new BufferedInputStream(descriptor.createInputStream()); - mRandomAccessFile = new RandomAccessFile(cacheFile, "rw"); - } catch (IOException e) { - e.printStackTrace(); - } + public RawMediaDataSource(InputStream is) { + mIs = is; } @Override public int readAt(long position, byte[] buffer, int offset, int size) throws IOException { - int read = -1; - if (mRandomAccessFile.length() == getSize()) { - writeSize = getSize(); - } - if (writeSize < position + size) { - if (mRandomAccessFile.getFilePointer() != writeSize) - mRandomAccessFile.seek(writeSize); - int len; - byte[] rBuf = new byte[1024]; - while (writeSize < position + size && (len = mStream.read(rBuf)) != -1) { - writeSize += len; - mRandomAccessFile.write(rBuf, 0, len); - } + if (size <= 0) + return size; + if (mPosition != position) { + mIs.reset(); + mPosition = mIs.skip(position); } - - if (mRandomAccessFile.getFilePointer() != position) - mRandomAccessFile.seek(position); - - if (size > 0) - read = mRandomAccessFile.read(buffer, 0, size); - return read; - + int length = mIs.read(buffer, offset, size); + mPosition += length; + return length; } @Override - public long getSize() { - return mDescriptor.getLength(); + public long getSize() throws IOException { + return mIs.available(); } @Override public void close() throws IOException { - if (mDescriptor != null) - mDescriptor.close(); - if (mStream != null) - mStream.close(); - if (mRandomAccessFile != null) - mRandomAccessFile.close(); - mStream = null; - mDescriptor = null; - mRandomAccessFile = null; + if (mIs != null) + mIs.close(); + mIs = null; } } diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 0314f4a7..9b168648 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -16,7 +16,7 @@ GEM commonmarker (0.17.13) ruby-enum (~> 0.5) concurrent-ruby (1.1.5) - dnsruby (1.61.2) + dnsruby (1.61.3) addressable (~> 2.5) em-websocket (0.5.1) eventmachine (>= 0.12.9) @@ -81,7 +81,7 @@ GEM octokit (~> 4.0) public_suffix (~> 3.0) typhoeus (~> 1.3) - html-pipeline (2.11.1) + html-pipeline (2.12.0) activesupport (>= 2) nokogiri (>= 1.4) http_parser.rb (0.6.0) @@ -205,7 +205,7 @@ GEM jekyll-seo-tag (~> 2.1) minitest (5.11.3) multipart-post (2.1.1) - nokogiri (1.10.3) + nokogiri (1.10.4) mini_portile2 (~> 2.4.0) octokit (4.14.0) sawyer (~> 0.8.0, >= 0.5.3) diff --git a/docs/_docs/en/1.1-install.md b/docs/_docs/en/1.1-install.md index 7d82c0cc..d132c450 100644 --- a/docs/_docs/en/1.1-install.md +++ b/docs/_docs/en/1.1-install.md @@ -10,7 +10,7 @@ And update the version number to the latest stable version, see the badge bellow ```yaml dependencies: - fijkplayer: ^0.0.9 + fijkplayer: ^0.1.0 ``` [![pub package](https://img.shields.io/pub/v/fijkplayer.svg)](https://pub.dartlang.org/packages/fijkplayer)     diff --git a/docs/_docs/zh/1.1-install.md b/docs/_docs/zh/1.1-install.md index 1f905871..99c7fac8 100644 --- a/docs/_docs/zh/1.1-install.md +++ b/docs/_docs/zh/1.1-install.md @@ -20,7 +20,7 @@ fijkplayer 是 ijkplayer 的 Flutter 封装, 是一款支持 android 和 iOS ```yaml dependencies: - fijkplayer: ^0.0.9 + fijkplayer: ^0.1.0 ``` [![pub package](https://img.shields.io/pub/v/fijkplayer.svg)](https://pub.dartlang.org/packages/fijkplayer)     @@ -76,14 +76,14 @@ Doctor summary (to see all details, run flutter doctor -v): ### 安装 fijkplayer 依赖 -进入上一步中创建的目录 playerapp 中,编辑其中的文件 `pubspec.yaml` ,增加依赖 `fijkplayer: ^0.0.9` +进入上一步中创建的目录 playerapp 中,编辑其中的文件 `pubspec.yaml` ,增加依赖 `fijkplayer: ^0.1.0` ```diff dependencies: flutter: sdk: flutter -+ fijkplayer: ^0.0.9 ++ fijkplayer: ^0.1.0 ``` 然后在 playerapp 目录中运行如下命令安装依赖,等待命令成功完成。 diff --git a/example/android/build.gradle b/example/android/build.gradle index a3e4d390..b65471a4 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -13,6 +13,9 @@ allprojects { repositories { google() jcenter() + flatDir{ + dirs "$rootProject.projectDir/../../android/aars" + } } } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 7e0c0f6c..2e394d79 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,8 +1,8 @@ PODS: - - fijkplayer (0.0.9): - - FIJKPlayer (~> 0.3.4) + - fijkplayer (0.1.0): + - FIJKPlayer (~> 0.3.5) - Flutter - - FIJKPlayer (0.3.4) + - FIJKPlayer (0.3.5) - Flutter (1.0.0) - path_provider (0.0.1): - Flutter @@ -35,8 +35,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences/ios" SPEC CHECKSUMS: - FIJKPlayer: 737c50419fcd97adad72ec84c0decc84051c67cc - fijkplayer: b1239f9356daa00fc1e3ad73e2434b351d350b22 + FIJKPlayer: fd7bd9729530db5bec20a2036d54ee5f7bb3a9c0 + fijkplayer: 479093ff73f22dbdcc78803e452f90a0e5cc3ef7 Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a path_provider: f96fff6166a8867510d2c25fdcc346327cc4b259 permission_handler: 6a1b2f94f866da8bd19fa67f3c8f4b9f38580efb diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 3a68e2f8..751fef85 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,7 +11,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 702BE3CB2549E2321BB96951 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AF1F63E720156266ACD46196 /* libPods-Runner.a */; }; + 52788FF52026B8BD57EB9F31 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D48DD3B576E12F0394D631B /* libPods-Runner.a */; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; @@ -39,7 +39,9 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 4D9B18E8C6B2F2879F707791 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 3FEB5AD822E3AC5EB80A596E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 4CD5EB5EB6C911A05F43BC74 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 4D48DD3B576E12F0394D631B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -52,9 +54,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AD8E28DF8D8667DE3FC8CADB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - AF1F63E720156266ACD46196 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - D80E7D2B6C902A101277DE79 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + A60CA36C49DB232CE31ADC18 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -63,7 +63,7 @@ buildActionMask = 2147483647; files = ( 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 702BE3CB2549E2321BB96951 /* libPods-Runner.a in Frameworks */, + 52788FF52026B8BD57EB9F31 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -73,9 +73,9 @@ 01811CABE3979E0AB0035758 /* Pods */ = { isa = PBXGroup; children = ( - 4D9B18E8C6B2F2879F707791 /* Pods-Runner.debug.xcconfig */, - D80E7D2B6C902A101277DE79 /* Pods-Runner.release.xcconfig */, - AD8E28DF8D8667DE3FC8CADB /* Pods-Runner.profile.xcconfig */, + 3FEB5AD822E3AC5EB80A596E /* Pods-Runner.debug.xcconfig */, + 4CD5EB5EB6C911A05F43BC74 /* Pods-Runner.release.xcconfig */, + A60CA36C49DB232CE31ADC18 /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -100,7 +100,7 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 01811CABE3979E0AB0035758 /* Pods */, - E187693BDC1AAE168A201D50 /* Frameworks */, + AC368B7B40DD291C8A7351BA /* Frameworks */, ); sourceTree = ""; }; @@ -136,10 +136,10 @@ name = "Supporting Files"; sourceTree = ""; }; - E187693BDC1AAE168A201D50 /* Frameworks */ = { + AC368B7B40DD291C8A7351BA /* Frameworks */ = { isa = PBXGroup; children = ( - AF1F63E720156266ACD46196 /* libPods-Runner.a */, + 4D48DD3B576E12F0394D631B /* libPods-Runner.a */, ); name = Frameworks; sourceTree = ""; @@ -151,14 +151,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 4B97441C70AE80C2C17041AD /* [CP] Check Pods Manifest.lock */, + D8FC02BD87C1D34251AC8B66 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 056930003195F1277DF58ECB /* [CP] Embed Pods Frameworks */, + 2ECFC889B76FBDAA15B6EFBA /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -218,7 +218,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 056930003195F1277DF58ECB /* [CP] Embed Pods Frameworks */ = { + 2ECFC889B76FBDAA15B6EFBA /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -250,41 +250,41 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; }; - 4B97441C70AE80C2C17041AD /* [CP] Check Pods Manifest.lock */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( ); + name = "Run Script"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + D8FC02BD87C1D34251AC8B66 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Run Script"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index 314ba819..d0e33f01 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -73,6 +73,7 @@ class HomeScreen extends StatelessWidget { }, text: "Online Samples", ), + /* Container( color: Theme.of(context).primaryColorLight, padding: EdgeInsets.only(left: 15, top: 3, bottom: 3, right: 15), @@ -84,6 +85,7 @@ class HomeScreen extends StatelessWidget { Expanded( child: list, ), + */ ], ), ), diff --git a/example/lib/recent_list.dart b/example/lib/recent_list.dart index 3f018db3..718833ca 100644 --- a/example/lib/recent_list.dart +++ b/example/lib/recent_list.dart @@ -5,9 +5,11 @@ import 'app_bar.dart'; import 'media_item.dart'; const List samples = [ - MediaUrl(title: "local file", url: "/Var/assets/butterfly.mp4"), - MediaUrl(title: "assets file", url: "assets://assets/butterfly.mp4"), - MediaUrl(title: "assets file", url: "assets:///assets/butterfly.mp4"), + MediaUrl( + title: "http 404", url: "https://fijkplayer.befovy.com/butterfly.flv"), + MediaUrl(title: "assets file", url: "asset:///assets/butterfly.mp4"), + MediaUrl( + title: "Protocol not found", url: "noprotocol://assets/butterfly.mp4"), MediaUrl( title: "rtsp test", url: "rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov"), diff --git a/example/lib/video_page.dart b/example/lib/video_page.dart index 9ecd525c..40e99d56 100644 --- a/example/lib/video_page.dart +++ b/example/lib/video_page.dart @@ -21,27 +21,33 @@ class _VideoScreenState extends State { @override void initState() { super.initState(); - player.setDataSource(widget.url, autoPlay: true); + player.setDataSource(widget.url, autoPlay: true).catchError((e) { + FijkException fe = e as FijkException; + //setState(() { + // errorMsg = fe.message; + //}); + print("setDataSource exception: $fe"); + }, test: (e) => e is FijkException); } @override Widget build(BuildContext context) { return Scaffold( - appBar: FijkAppBar.defaultSetting(title: "Video"), - body: Container( + appBar: FijkAppBar.defaultSetting(title: "Video"), + body: Container( child: FijkView( - player: player, - // panelBuilder: simplestUI, - // panelBuilder: (FijkPlayer player, BuildContext context, - // Size viewSize, Rect texturePos) { - // return CustomFijkPanel( - // player: player, - // buildContext: context, - // viewSize: viewSize, - // texturePos: texturePos); - // }, - ), - )); + player: player, + // panelBuilder: simplestUI, + // panelBuilder: (FijkPlayer player, BuildContext context, + // Size viewSize, Rect texturePos) { + // return CustomFijkPanel( + // player: player, + // buildContext: context, + // viewSize: viewSize, + // texturePos: texturePos); + // }, + )), + ); } @override diff --git a/example/pubspec.lock b/example/pubspec.lock index 0f1fe1f2..e971d6ce 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.4" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.2" async: dependency: transitive description: @@ -29,23 +43,138 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.1" fijkplayer: dependency: "direct dev" description: path: ".." relative: true source: path - version: "0.0.9" + version: "0.1.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.8" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_driver: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.19" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.19" matcher: dependency: transitive description: @@ -60,6 +189,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.6" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+3" + multi_server_socket: + dependency: transitive + description: + name: multi_server_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.6" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" path: dependency: transitive description: @@ -88,6 +252,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.1+1" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.9" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" quiver: dependency: transitive description: @@ -102,11 +294,53 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.5.3+4" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.8" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.5" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" source_span: dependency: transitive description: @@ -149,6 +383,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.3" test_api: dependency: transitive description: @@ -156,6 +397,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.5" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5" typed_data: dependency: transitive description: @@ -170,6 +418,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + vm_service_client: + dependency: transitive + description: + name: vm_service_client + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.6+2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+12" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.13" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.16" sdks: dart: ">=2.2.2 <3.0.0" flutter: ">=1.5.0 <2.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c6688bf0..4a89e194 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -21,6 +21,11 @@ dev_dependencies: flutter_test: sdk: flutter + flutter_driver: + sdk: flutter + + test: any + fijkplayer: path: ../ diff --git a/example/test_driver/app.dart b/example/test_driver/app.dart new file mode 100644 index 00000000..25c370c8 --- /dev/null +++ b/example/test_driver/app.dart @@ -0,0 +1,28 @@ +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in all +//copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//SOFTWARE. +// + +import 'package:fijkplayer_example/main.dart' as app; +import 'package:flutter_driver/driver_extension.dart'; + +void main() { + // This line enables the extension. + enableFlutterDriverExtension(); + + // Call the `main()` function of the app, or call `runApp` with + // any widget you are interested in testing. + app.main(); +} diff --git a/example/test_driver/app_test.dart b/example/test_driver/app_test.dart new file mode 100644 index 00000000..218c70e0 --- /dev/null +++ b/example/test_driver/app_test.dart @@ -0,0 +1,39 @@ +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in all +//copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//SOFTWARE. +// + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +// run in terminal +// flutter drive --target=test_driver/app.dart + +void main() { + test("test driver test", () { + expect(0, 0); + }); + + group('Counter App', () { + FlutterDriver driver; + setUpAll(() async { + driver = await FlutterDriver.connect(); + }); + + tearDownAll(() async { + await driver?.close(); + }); + }); +} diff --git a/fijkplayer.iml b/fijkplayer.iml index 5c58495b..eaf54619 100644 --- a/fijkplayer.iml +++ b/fijkplayer.iml @@ -22,6 +22,9 @@ + + + diff --git a/ios/Classes/FijkPlayer.m b/ios/Classes/FijkPlayer.m index 362385e3..2cac8def 100644 --- a/ios/Classes/FijkPlayer.m +++ b/ios/Classes/FijkPlayer.m @@ -29,10 +29,22 @@ @implementation FijkPlayer { CVPixelBufferRef _cachePixelBufer; CVPixelBufferRef _Atomic _pixelBuffer; + int _state; int _pid; int64_t _vid; } +static const int idle = 0; +static const int initialized = 1; +static const int asyncPreparing = 2; +static const int __attribute__((unused)) prepared = 3; +static const int __attribute__((unused)) started = 4; +static const int paused = 5; +static const int completed = 6; +static const int stopped = 7; +static const int __attribute__((unused)) error = 8; +static const int end = 9; + - (instancetype)initWithRegistrar:(id)registrar { self = [super init]; if (self) { @@ -45,6 +57,7 @@ - (instancetype)initWithRegistrar:(id)registrar { _cachePixelBufer = nil; _pixelBuffer = nil; _vid = -1; + _state = 0; [_ijkMediaPlayer addIJKMPEventHandler:self]; @@ -83,8 +96,11 @@ - (instancetype)initWithRegistrar:(id)registrar { } - (void)shutdown { + [self handleEvent:IJKMPET_PLAYBACK_STATE_CHANGED + andArg1:end + andArg2:_state + andExtra:nil]; if (_ijkMediaPlayer) { - [_ijkMediaPlayer stop]; [_ijkMediaPlayer shutdown]; _ijkMediaPlayer = nil; } @@ -149,12 +165,14 @@ - (void)handleEvent:(int)what andArg2:(int)arg2 andExtra:(void *)extra { switch (what) { - case IJKMPET_PREPARED: { - long duration = [_ijkMediaPlayer getDuration]; - [_eventSink - success:@{@"event" : @"prepared", @"duration" : @(duration)}]; - } break; + case IJKMPET_PREPARED: + [_eventSink success:@{ + @"event" : @"prepared", + @"duration" : @([_ijkMediaPlayer getDuration]) + }]; + break; case IJKMPET_PLAYBACK_STATE_CHANGED: + _state = arg1; [_eventSink success:@{ @"event" : @"state_change", @"new" : @(arg1), @@ -163,7 +181,6 @@ - (void)handleEvent:(int)what break; case IJKMPET_BUFFERING_START: case IJKMPET_BUFFERING_END: - // _displayLink.paused = what == IJKMPET_BUFFERING_START; [_eventSink success:@{ @"event" : @"freeze", @"value" : [NSNumber numberWithBool:what == IJKMPET_BUFFERING_START] @@ -182,6 +199,12 @@ - (void)handleEvent:(int)what @"width" : @(arg1), @"height" : @(arg2) }]; + break; + case IJKMPET_ERROR: + [_eventSink error:[NSString stringWithFormat:@"%d", arg1] + message:extra ? [NSString stringWithUTF8String:extra] : nil + details:@(arg2)]; + break; default: break; } @@ -199,6 +222,7 @@ - (void)onEvent4Player:(IJKFFMediaPlayer *)player case IJKMPET_BUFFERING_END: case IJKMPET_BUFFERING_UPDATE: case IJKMPET_VIDEO_SIZE_CHANGED: + case IJKMPET_ERROR: [self handleEvent:what andArg1:arg1 andArg2:arg2 andExtra:extra]; break; default: @@ -255,7 +279,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call } else if ([@"setDateSource" isEqualToString:call.method]) { NSString *url = argsMap[@"url"]; NSURL *aUrl = [NSURL URLWithString:url]; - if ([@"assets" isEqualToString:aUrl.scheme]) { + bool file404 = false; + if ([@"asset" isEqualToString:aUrl.scheme]) { NSString *host = aUrl.host; NSString *asset = [host length] == 0 ? [_registrar lookupKeyForAsset:aUrl.path] @@ -268,16 +293,34 @@ - (void)handleMethodCall:(FlutterMethodCall *)call url = path; } if ([url isEqualToString:argsMap[@"url"]]) { - result([FlutterError errorWithCode:@"assets not found" - message:url - details:nil]); - return; + file404 = true; + } + } else if ([@"file" isEqualToString:aUrl.scheme] || + [aUrl.scheme length] == 0) { + NSFileManager *fileManager = [[NSFileManager alloc] init]; + if (![fileManager fileExistsAtPath:aUrl.path]) { + file404 = true; } } - [_ijkMediaPlayer setDataSource:url]; - result(nil); + if (file404) { + result([FlutterError errorWithCode:@"-875574348" + message:[@"Local File not found:" + stringByAppendingString:url] + details:nil]); + } else { + [_ijkMediaPlayer setDataSource:url]; + [self handleEvent:IJKMPET_PLAYBACK_STATE_CHANGED + andArg1:initialized + andArg2:-1 + andExtra:nil]; + result(nil); + } } else if ([@"prepareAsync" isEqualToString:call.method]) { [_ijkMediaPlayer prepareAsync]; + [self handleEvent:IJKMPET_PLAYBACK_STATE_CHANGED + andArg1:asyncPreparing + andArg2:-1 + andExtra:nil]; result(nil); } else if ([@"start" isEqualToString:call.method]) { int ret = [_ijkMediaPlayer start]; @@ -288,9 +331,17 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result(nil); } else if ([@"stop" isEqualToString:call.method]) { [_ijkMediaPlayer stop]; + [self handleEvent:IJKMPET_PLAYBACK_STATE_CHANGED + andArg1:stopped + andArg2:-1 + andExtra:nil]; result(nil); } else if ([@"reset" isEqualToString:call.method]) { [_ijkMediaPlayer reset]; + [self handleEvent:IJKMPET_PLAYBACK_STATE_CHANGED + andArg1:idle + andArg2:-1 + andExtra:nil]; result(nil); } else if ([@"getCurrentPosition" isEqualToString:call.method]) { long pos = [_ijkMediaPlayer getCurrentPosition]; @@ -302,6 +353,11 @@ - (void)handleMethodCall:(FlutterMethodCall *)call } else if ([@"seekTo" isEqualToString:call.method]) { long pos = [argsMap[@"msec"] longValue]; [_ijkMediaPlayer seekTo:pos]; + if (_state == completed) + [self handleEvent:IJKMPET_PLAYBACK_STATE_CHANGED + andArg1:paused + andArg2:-1 + andExtra:nil]; result(nil); } else if ([@"setLoop" isEqualToString:call.method]) { int loopCount = [argsMap[@"loop"] intValue]; diff --git a/ios/Classes/FijkQueuingEventSink.h b/ios/Classes/FijkQueuingEventSink.h index 33856475..bc8825a4 100644 --- a/ios/Classes/FijkQueuingEventSink.h +++ b/ios/Classes/FijkQueuingEventSink.h @@ -17,8 +17,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)endOfStream; - (void)error:(NSString *)code - message:(NSString *)message - details:(NSObject *)details; + message:(NSString *_Nullable)message + details:(id _Nullable)details; - (void)success:(NSObject *)event; diff --git a/ios/Classes/FijkQueuingEventSink.m b/ios/Classes/FijkQueuingEventSink.m index 19ebfa49..5183b937 100644 --- a/ios/Classes/FijkQueuingEventSink.m +++ b/ios/Classes/FijkQueuingEventSink.m @@ -51,8 +51,8 @@ - (void)endOfStream { } - (void)error:(NSString *)code - message:(NSString *)message - details:(NSObject *)details { + message:(NSString *_Nullable)message + details:(id _Nullable)details { [self enqueue:[FlutterError errorWithCode:code message:message details:details]]; diff --git a/ios/fijkplayer.podspec b/ios/fijkplayer.podspec index d686b8c6..266e9b50 100644 --- a/ios/fijkplayer.podspec +++ b/ios/fijkplayer.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'fijkplayer' - s.version = '0.0.9' + s.version = '0.1.0' s.summary = 'Flutter plugin for ijkplayer' s.description = <<-DESC Flutter plugin for ijkplayer @@ -25,7 +25,7 @@ Flutter plugin for ijkplayer s.dependency 'Flutter' # s.use_frameworks! - s.dependency 'FIJKPlayer', '~> 0.3.4' + s.dependency 'FIJKPlayer', '~> 0.3.5' s.ios.deployment_target = '8.0' end diff --git a/lib/fijkplayer.dart b/lib/fijkplayer.dart index 0f2b2ff3..48e2b726 100644 --- a/lib/fijkplayer.dart +++ b/lib/fijkplayer.dart @@ -27,3 +27,4 @@ export 'src/fijkplayer.dart'; export 'src/fijkview.dart'; export 'src/fijkpanel.dart'; export 'src/fijkoption.dart'; +export 'src/fijkvalue.dart'; diff --git a/lib/src/fijkoption.dart b/lib/src/fijkoption.dart index 1885582a..dc90e4e2 100644 --- a/lib/src/fijkoption.dart +++ b/lib/src/fijkoption.dart @@ -31,6 +31,12 @@ class FijkOption { final Map _playerOption = HashMap(); final Map _swrOption = HashMap(); + static const int formatCategory = 1; + static const int codecCategory = 2; + static const int swsCategory = 3; + static const int playerCategory = 4; + static const int swrCategory = 5; + Map> get data { final Map> options = HashMap(); options[1] = Map.from(_formatOption); diff --git a/lib/src/fijkpanel.dart b/lib/src/fijkpanel.dart index 86e130b2..614040c6 100644 --- a/lib/src/fijkpanel.dart +++ b/lib/src/fijkpanel.dart @@ -29,6 +29,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'fijkplayer.dart'; +import 'fijkvalue.dart'; /// The signature of the [LayoutBuilder] builder function. /// @@ -40,7 +41,7 @@ typedef FijkPanelWidgetBuilder = Widget Function( /// Default builder generate default [FijkPanel] UI Widget defaultFijkPanelBuilder( FijkPlayer player, BuildContext context, Size viewSize, Rect texturePos) { - return DefaultFijkPanel( + return _DefaultFijkPanel( player: player, buildContext: context, viewSize: viewSize, @@ -48,13 +49,13 @@ Widget defaultFijkPanelBuilder( } /// Default Panel Widget -class DefaultFijkPanel extends StatefulWidget { +class _DefaultFijkPanel extends StatefulWidget { final FijkPlayer player; final BuildContext buildContext; final Size viewSize; final Rect texturePos; - const DefaultFijkPanel({ + const _DefaultFijkPanel({ @required this.player, this.buildContext, this.viewSize, @@ -81,7 +82,7 @@ String _duration2String(Duration duration) { : "$twoDigitMinutes:$twoDigitSeconds"; } -class _DefaultFijkPanelState extends State { +class _DefaultFijkPanelState extends State<_DefaultFijkPanel> { FijkPlayer get player => widget.player; Duration _duration = Duration(); @@ -90,6 +91,7 @@ class _DefaultFijkPanelState extends State { // Duration _bufferPos = Duration(); bool _playing = false; bool _prepared = false; + String _exception; // bool _buffering = false; @@ -116,6 +118,7 @@ class _DefaultFijkPanelState extends State { //_bufferPos = player.bufferPos; _prepared = player.state.index >= FijkState.prepared.index; _playing = player.state == FijkState.started; + _exception = player.value.exception.message; // _buffering = player.isBuffering; player.addListener(_playerValueChanged); @@ -153,10 +156,14 @@ class _DefaultFijkPanelState extends State { bool playing = (value.state == FijkState.started); bool prepared = value.prepared; - if (playing != _playing || prepared != _prepared) { + String exception = value.exception.message; + if (playing != _playing || + prepared != _prepared || + exception != _exception) { setState(() { _playing = playing; _prepared = prepared; + _exception = exception; }); } } @@ -318,27 +325,35 @@ class _DefaultFijkPanelState extends State { height: double.infinity, width: double.infinity, child: Center( - child: _prepared - ? AnimatedOpacity( - opacity: _hideStuff ? 0.0 : 0.7, - duration: Duration(milliseconds: 400), - child: IconButton( - iconSize: barHeight * 2, - icon: Icon( - _playing - ? Icons.pause - : Icons.play_arrow, - color: Colors.white), - padding: EdgeInsets.only( - left: 10.0, right: 10.0), - onPressed: _playOrPause)) - : SizedBox( - width: barHeight * 1.5, - height: barHeight * 1.5, - child: CircularProgressIndicator( - valueColor: - AlwaysStoppedAnimation(Colors.white)), - )), + child: _exception != null + ? Text( + _exception, + style: TextStyle( + color: Colors.white, + fontSize: 25, + ), + ) + : _prepared + ? AnimatedOpacity( + opacity: _hideStuff ? 0.0 : 0.7, + duration: Duration(milliseconds: 400), + child: IconButton( + iconSize: barHeight * 2, + icon: Icon( + _playing + ? Icons.pause + : Icons.play_arrow, + color: Colors.white), + padding: EdgeInsets.only( + left: 10.0, right: 10.0), + onPressed: _playOrPause)) + : SizedBox( + width: barHeight * 1.5, + height: barHeight * 1.5, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Colors.white)), + )), ), ), ), diff --git a/lib/src/fijkplayer.dart b/lib/src/fijkplayer.dart index f61f566e..a2ca7f89 100644 --- a/lib/src/fijkplayer.dart +++ b/lib/src/fijkplayer.dart @@ -29,186 +29,7 @@ import 'package:flutter/services.dart'; import 'fijkoption.dart'; import 'fijkplugin.dart'; - -/// State of the [FijkPlayer] -/// -/// This is the state machine of ijkplayer. FijkPlayer has the same state as native ijkplayer. -/// The state changed after method called or when some error occurs. -/// One state can only change into the new state it can reach. -/// -/// For example, [idle] can't becomes [asyncPreparing] directly. -/// -/// -enum FijkState { - /// The state when a [FijkPlayer] is just created. - /// Native ijkplayer memory and objects also be alloced or created when a [FijkPlayer] is created. - /// - /// * setDataSource() -> [initialized] - /// * reset() -> self - /// * release() -> [end] - idle, - - /// After call [FijkPlayer.setDataSource] on state [idle], the state becomes [initialized]. - /// - /// * prepareAsync() -> [asyncPreparing] - /// * reset() -> [idle] - /// * release() -> [end] - initialized, - - /// There're many tasks to do during prepare, such as detect stream info in datasource, find and open decoder, start decode and refresh thread. - /// So ijkplayer export a async api prepareAsync. - /// When [FijkPlayer.prepareAsync] is called on state [initialized], ths state changed to [asyncPreparing] immediately. - /// After all task in prepare have finished, the state changed to [prepared]. - /// Additionally, if any error occurs during prepare, the state will change to [error]. - /// - /// * ..... -> [prepared] - /// * ..... -> [error] - /// * reset() -> [idle] - /// * release() -> [end] - asyncPreparing, - - /// After finish all the heavy tasks during [FijkPlayer.prepareAsync], - /// the state becomes [prepared] from [asyncPreparing]. - /// - /// * seekTo() -> self - /// * start() -> [started] - /// * reset() -> [idle] - /// * release() -> [end] - prepared, - - /// * seekTo() -> self - /// * start() -> self - /// * pause() -> [paused] - /// * stop() -> [stopped] - /// * ...... -> [completed] - /// * ...... -> [error] - /// * reset() -> [idle] - /// * release() -> [end] - started, - - /// * seekTo() -> self - /// * start() -> [started] - /// * pause() -> self - /// * stop() -> [stopped] - /// * reset() -> [idle] - /// * release() -> [end] - paused, - - /// * seekTo() -> [paused] - /// * start() -> [started] (from beginning) - /// * pause() -> self - /// * stop() -> [stopped] - /// * reset() -> [idle] - /// * release() -> [end] - completed, - - /// * stop() -> self - /// * prepareAsync() -> [asyncPreparing] - /// * reset() -> [idle] - /// * release() -> [end] - stopped, - - /// * reset() -> [idle] - /// * release() -> [end] - error, - - /// * release() -> self - end -} - -/// FijkValue include the properties of a [FijkPlayer] which update not frequently. -/// -/// To get the updated value of other frequently updated properties, -/// add listener of the value stream. -/// See -/// * [FijkPlayer.onBufferPosUpdate] -/// * [FijkPlayer.onCurrentPosUpdate] -/// * [FijkPlayer.onBufferStateUpdate] -@immutable -class FijkValue { - /// Indicates if the player is ready - final bool prepared; - - /// Indicates if the player is completed - /// - /// If the playback stream is realtime/live, [completed] never be true. - final bool completed; - - /// Current state of the player - final FijkState state; - - /// The pixel [size] of current video - /// - /// Is null when [prepared] is false. - /// Is negative width and height if playback is audio only. - final Size size; - - /// The current playback duration - /// - /// Is null when [prepared] is false. - /// Is zero when playback is realtime stream. - final Duration duration; - - /// whether if player should be displayed in full screen mode - final bool fullScreen; - - /// A constructor requires all value. - const FijkValue({ - @required this.prepared, - @required this.completed, - @required this.state, - @required this.size, - @required this.duration, - @required this.fullScreen, - }); - - /// Construct FijkValue with uninitialized value - const FijkValue.uninitialized() - : this( - prepared: false, - completed: false, - state: FijkState.idle, - size: null, - duration: const Duration(), - fullScreen: false, - ); - - /// Return new FijkValue which combines the old value and the assigned new value - FijkValue copyWith({ - bool prepared, - bool completed, - FijkState state, - Size size, - Duration duration, - bool fullScreen, - }) { - return FijkValue( - prepared: prepared ?? this.prepared, - completed: completed ?? this.completed, - state: state ?? this.state, - size: size ?? this.size, - duration: duration ?? this.duration, - fullScreen: fullScreen ?? this.fullScreen, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FijkValue && - runtimeType == other.runtimeType && - hashCode == other.hashCode; - - @override - int get hashCode => - hashValues(prepared, completed, state, size, duration, fullScreen); - - @override - String toString() { - return "prepared:$prepared, completed:$completed, state:$state, size:$size, " - "duration:$duration, fullScreen:$fullScreen"; - } -} +import 'fijkvalue.dart'; /// FijkPlayer present as a playback. It interacts with native object. /// @@ -225,7 +46,6 @@ class FijkPlayer extends ChangeNotifier implements ValueListenable { bool _startAfterSetup = false; FijkValue _value; - FijkState _epState; /// return the current state FijkState get state => _value.state; @@ -278,36 +98,32 @@ class FijkPlayer extends ChangeNotifier implements ValueListenable { : _nativeSetup = Completer(), super() { _value = FijkValue.uninitialized(); - _epState = FijkState.error; _doNativeSetup(); } Future _startFromAnyState() async { await _nativeSetup.future; - if (_epState == FijkState.error || _epState == FijkState.stopped) { + if (state == FijkState.error || state == FijkState.stopped) { await reset(); } - if (_epState == FijkState.idle) { + if (state == FijkState.idle) { await setDataSource(_dataSource); } - if (_epState == FijkState.initialized) { + if (state == FijkState.initialized) { await prepareAsync(); } - if (_epState == FijkState.prepared || - _epState == FijkState.completed || - _epState == FijkState.paused) { + if (state == FijkState.asyncPreparing || + state == FijkState.prepared || + state == FijkState.completed || + state == FijkState.paused) { await start(); } - return Future.value(); } Future _doNativeSetup() async { _playerId = await FijkPlugin.createPlayer(); _channel = MethodChannel('befovy.com/fijkplayer/' + _playerId.toString()); - _epState = FijkState.idle; - - print("native player id: $_playerId"); _nativeEventSubscription = EventChannel('befovy.com/fijkplayer/event/' + _playerId.toString()) @@ -324,6 +140,18 @@ class FijkPlayer extends ChangeNotifier implements ValueListenable { _looperSub.pause(); } + /// Check if player is playable + /// + /// Only the four state [FijkState.prepared] \ [FijkState.started] \ + /// [FijkState.paused] \ [FijkState.completed] are playable + bool isPlayable() { + FijkState current = value.state; + return FijkState.prepared == current || + FijkState.started == current || + FijkState.paused == current || + FijkState.completed == current; + } + Future setOption(int category, String key, String value) { return _channel.invokeMethod("setOption", {"cat": category, "key": key, "str": value}); @@ -344,35 +172,40 @@ class FijkPlayer extends ChangeNotifier implements ValueListenable { return _channel.invokeMethod("setupSurface"); } + /// Set data source for this player + /// + /// [path] must be a valid uri, otherwise this method return ArgumentError Future setDataSource( String path, { bool autoPlay = false, }) async { + if (path == null || path.length == 0 || Uri.tryParse(path) == null) { + return Future.error( + ArgumentError.value(path, "path must be a valid url")); + } await _nativeSetup.future; - if (_epState == FijkState.idle) { - _epState = FijkState.initialized; - await _channel - .invokeMethod("setDateSource", {'url': path}); - + if (state == FijkState.idle || state == FijkState.initialized) { + try { + await _channel + .invokeMethod("setDateSource", {'url': path}); + } on PlatformException catch (e) { + return _errorListener(e); + } if (autoPlay == true) { - await this.start(); + await start(); } } else { - return Future.error(StateError("setDataSource at illegal state")); + return Future.error(StateError("setDataSource on invalid state $state")); } } Future prepareAsync() async { - // ckeck state await _nativeSetup.future; - int ret = 0; - if (_epState == FijkState.initialized) { - _epState = FijkState.prepared; + if (state == FijkState.initialized) { await _channel.invokeMethod("prepareAsync"); } else { - ret = -1; + return Future.error(StateError("prepareAsync on invalid state $state")); } - //return Future.value(); } Future setVolume(double volume) async { @@ -393,67 +226,93 @@ class FijkPlayer extends ChangeNotifier implements ValueListenable { Future start() async { await _nativeSetup.future; - int ret = 0; - - if (_epState == FijkState.initialized) { + if (state == FijkState.initialized) { await _channel.invokeMethod("prepareAsync"); await _channel.invokeMethod("start"); - _epState = FijkState.started; - } else if (_epState == FijkState.prepared || - _epState == FijkState.paused || + } else if (state == FijkState.asyncPreparing || + state == FijkState.prepared || + state == FijkState.paused || value.state == FijkState.completed) { await _channel.invokeMethod("start"); - _epState = FijkState.started; } else { - ret = -1; + Future.error(StateError("call start on invalid state $state")); } - - print("call start $_epState ${value.state} ret:$ret"); } Future pause() async { await _nativeSetup.future; - _epState = FijkState.paused; - await _channel.invokeMethod("pause"); - print("call pause"); + if (isPlayable()) + await _channel.invokeMethod("pause"); + else { + Future.error(StateError("call pause on invalid state $state")); + } } Future stop() async { await _nativeSetup.future; - - _epState = FijkState.stopped; - await _channel.invokeMethod("stop"); + if (state == FijkState.end || + state == FijkState.idle || + state == FijkState.initialized) + Future.error(StateError("call stop on invalid state $state")); + else + await _channel.invokeMethod("stop"); } Future reset() async { await _nativeSetup.future; - _epState = FijkState.idle; - await _channel.invokeMethod("reset"); + if (state == FijkState.end) + Future.error(StateError("call reset on invalid state $state")); + else + await _channel.invokeMethod("reset"); } Future seekTo(int msec) async { await _nativeSetup.future; - - // if (_epState == ) - await _channel.invokeMethod("seekTo", {"msec": msec}); + if (msec == null || msec < 0) + return Future.error( + ArgumentError.value(msec, "speed must be not null and >= 0")); + if (!isPlayable()) + Future.error(StateError("Non playable state $state")); + else + _channel.invokeMethod("seekTo", {"msec": msec}); } + /// Release native player. Release memory and resource Future release() async { await _nativeSetup.future; - await this.stop(); - await _nativeEventSubscription.cancel(); - await _looperSub.cancel(); - return FijkPlugin.releasePlayer(_playerId); + if (isPlayable()) await this.stop(); + _setValue(value.copyWith(state: FijkState.end)); + await _looperSub?.cancel(); + _looperSub = null; + await _nativeEventSubscription?.cancel(); + _nativeEventSubscription = null; + await FijkPlugin.releasePlayer(_playerId); } + /// Set player loop count + /// + /// [loopCount] must not null and greater than or equal to 0. + /// Default loopCount of player is 1, which also means no loop. + /// A positive value of [loopCount] means special repeat times. + /// If [loopCount] is 0, is means infinite repeat. Future setLoop(int loopCount) async { await _nativeSetup.future; + if (loopCount == null || loopCount < 0) + return Future.error(ArgumentError.value( + loopCount, "loopCount must not be null and >= 0")); return _channel .invokeMethod("setLoop", {"loop": loopCount}); } + /// Set playback speed + /// + /// [speed] must not null and greater than 0. + /// Default speed is 1 Future setSpeed(double speed) async { await _nativeSetup.future; + if (speed == null || speed <= 0) + return Future.error(ArgumentError.value( + speed, "speed must be not null and greater than 0")); return _channel.invokeMethod("setSpeed", {"speed": speed}); } @@ -469,30 +328,38 @@ class FijkPlayer extends ChangeNotifier implements ValueListenable { final Map map = event; switch (map['event']) { case 'prepared': - int duration = map['duration']; + int duration = map['duration'] ?? 0; Duration dur = Duration(milliseconds: duration); _setValue(value.copyWith(duration: dur, prepared: true)); break; case 'state_change': - int newState = map['new']; - FijkState fpState = FijkState.values[newState]; - - if (fpState == FijkState.started) { - _looperSub.resume(); - } else { - if (!_looperSub.isPaused) _looperSub.pause(); - } - - if (fpState == FijkState.error) { - _epState = FijkState.error; - } - - if (newState == FijkState.prepared.index) { - _setValue(value.copyWith(prepared: true, state: fpState)); - } else if (newState < FijkState.prepared.index) { - _setValue(value.copyWith(prepared: false, state: fpState)); - } else { - _setValue(value.copyWith(state: fpState)); + int newStateId = map['new']; + int _oldState = map['old']; + FijkState fpState = FijkState.values[newStateId]; + FijkState oldState = + (_oldState >= 0 && _oldState < FijkState.values.length) + ? FijkState.values[_oldState] + : state; + + if (fpState != oldState) { + debugPrint("state_change: new: $fpState <= old: $oldState"); + + if (fpState == FijkState.started) { + _looperSub.resume(); + } else { + if (!_looperSub.isPaused) _looperSub.pause(); + } + FijkException fijkException = + (fpState != FijkState.error) ? FijkException.noException : null; + if (newStateId == FijkState.prepared.index) { + _setValue(value.copyWith( + prepared: true, state: fpState, exception: fijkException)); + } else if (newStateId < FijkState.prepared.index) { + _setValue(value.copyWith( + prepared: false, state: fpState, exception: fijkException)); + } else { + _setValue(value.copyWith(state: fpState, exception: fijkException)); + } } break; case 'freeze': @@ -519,6 +386,8 @@ class FijkPlayer extends ChangeNotifier implements ValueListenable { void _errorListener(Object obj) { final PlatformException e = obj; - print("onError: $e"); + FijkException exception = FijkException.fromPlatformException(e); + debugPrint("errorListerner: $e, $exception"); + _setValue(value.copyWith(exception: exception)); } } diff --git a/lib/src/fijkplugin.dart b/lib/src/fijkplugin.dart index 34b76c08..c7d0d3d4 100644 --- a/lib/src/fijkplugin.dart +++ b/lib/src/fijkplugin.dart @@ -20,8 +20,8 @@ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; +import 'dart:io'; + import 'package:flutter/services.dart'; import 'fijkplayer.dart'; @@ -45,32 +45,25 @@ class FijkPlugin { .invokeMethod("releasePlayer", {'pid': pid}); } - static Future setOrientationPortrait( - {@required BuildContext context}) async { - final platform = Theme.of(context).platform; - + static Future setOrientationPortrait() async { // ios crash Supported orientations has no common orientation with the application - if (platform == TargetPlatform.android) { + if (Platform.isAndroid) { SystemChrome.setPreferredOrientations( [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); } return _channel.invokeMethod("setOrientationPortrait"); } - static Future setOrientationLandscape( - {@required BuildContext context}) async { - final platform = Theme.of(context).platform; - if (platform == TargetPlatform.android) { + static Future setOrientationLandscape() async { + if (Platform.isAndroid) { SystemChrome.setPreferredOrientations( [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]); } return _channel.invokeMethod("setOrientationLandscape"); } - static Future setOrientationAuto( - {@required BuildContext context}) async { - final platform = Theme.of(context).platform; - if (platform == TargetPlatform.android) { + static Future setOrientationAuto() async { + if (Platform.isAndroid) { SystemChrome.setPreferredOrientations(DeviceOrientation.values); } return _channel.invokeMethod("setOrientationAuto"); diff --git a/lib/src/fijkvalue.dart b/lib/src/fijkvalue.dart new file mode 100644 index 00000000..d26535a6 --- /dev/null +++ b/lib/src/fijkvalue.dart @@ -0,0 +1,323 @@ +//MIT License +// +//Copyright (c) [2019] [Befovy] +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in all +//copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//SOFTWARE. + +import 'dart:core'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'fijkplayer.dart'; + +/// State of the [FijkPlayer] +/// +/// This is the state machine of ijkplayer. FijkPlayer has the same state as native ijkplayer. +/// The state changed after method called or when some error occurs. +/// One state can only change into the new state it can reach. +/// +/// For example, [idle] can't becomes [asyncPreparing] directly. +/// +/// +enum FijkState { + /// The state when a [FijkPlayer] is just created. + /// Native ijkplayer memory and objects also be alloced or created when a [FijkPlayer] is created. + /// + /// * setDataSource() -> [initialized] + /// * reset() -> self + /// * release() -> [end] + idle, + + /// After call [FijkPlayer.setDataSource] on state [idle], the state becomes [initialized]. + /// + /// * prepareAsync() -> [asyncPreparing] + /// * reset() -> [idle] + /// * release() -> [end] + initialized, + + /// There're many tasks to do during prepare, such as detect stream info in datasource, find and open decoder, start decode and refresh thread. + /// So ijkplayer export a async api prepareAsync. + /// When [FijkPlayer.prepareAsync] is called on state [initialized], ths state changed to [asyncPreparing] immediately. + /// After all task in prepare have finished, the state changed to [prepared]. + /// Additionally, if any error occurs during prepare, the state will change to [error]. + /// + /// * ..... -> [prepared] + /// * ..... -> [error] + /// * reset() -> [idle] + /// * release() -> [end] + asyncPreparing, + + /// After finish all the heavy tasks during [FijkPlayer.prepareAsync], + /// the state becomes [prepared] from [asyncPreparing]. + /// + /// * seekTo() -> self + /// * start() -> [started] + /// * reset() -> [idle] + /// * release() -> [end] + prepared, + + /// * seekTo() -> self + /// * start() -> self + /// * pause() -> [paused] + /// * stop() -> [stopped] + /// * ...... -> [completed] + /// * ...... -> [error] + /// * reset() -> [idle] + /// * release() -> [end] + started, + + /// * seekTo() -> self + /// * start() -> [started] + /// * pause() -> self + /// * stop() -> [stopped] + /// * reset() -> [idle] + /// * release() -> [end] + paused, + + /// * seekTo() -> [paused] + /// * start() -> [started] (from beginning) + /// * pause() -> self + /// * stop() -> [stopped] + /// * reset() -> [idle] + /// * release() -> [end] + completed, + + /// * stop() -> self + /// * prepareAsync() -> [asyncPreparing] + /// * reset() -> [idle] + /// * release() -> [end] + stopped, + + /// * reset() -> [idle] + /// * release() -> [end] + error, + + /// * release() -> self + end +} + +/// FijkValue include the properties of a [FijkPlayer] which update not frequently. +/// +/// To get the updated value of other frequently updated properties, +/// add listener of the value stream. +/// See +/// * [FijkPlayer.onBufferPosUpdate] +/// * [FijkPlayer.onCurrentPosUpdate] +/// * [FijkPlayer.onBufferStateUpdate] +@immutable +class FijkValue { + /// Indicates if the player is ready + final bool prepared; + + /// Indicates if the player is completed + /// + /// If the playback stream is realtime/live, [completed] never be true. + final bool completed; + + /// Current state of the player + final FijkState state; + + /// The pixel [size] of current video + /// + /// Is null when [prepared] is false. + /// Is negative width and height if playback is audio only. + final Size size; + + /// The current playback duration + /// + /// Is null when [prepared] is false. + /// Is zero when playback is realtime stream. + final Duration duration; + + /// whether if player should be displayed in full screen mode + final bool fullScreen; + + final FijkException exception; + + /// A constructor requires all value. + const FijkValue({ + @required this.prepared, + @required this.completed, + @required this.state, + @required this.size, + @required this.duration, + @required this.fullScreen, + @required this.exception, + }); + + /// Construct FijkValue with uninitialized value + const FijkValue.uninitialized() + : this( + prepared: false, + completed: false, + state: FijkState.idle, + size: null, + duration: const Duration(), + fullScreen: false, + exception: FijkException.noException, + ); + + /// Return new FijkValue which combines the old value and the assigned new value + FijkValue copyWith({ + bool prepared, + bool completed, + FijkState state, + Size size, + Duration duration, + bool fullScreen, + FijkException exception, + }) { + return FijkValue( + prepared: prepared ?? this.prepared, + completed: completed ?? this.completed, + state: state ?? this.state, + size: size ?? this.size, + duration: duration ?? this.duration, + fullScreen: fullScreen ?? this.fullScreen, + exception: exception ?? this.exception, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FijkValue && + runtimeType == other.runtimeType && + hashCode == other.hashCode; + + @override + int get hashCode => hashValues( + prepared, completed, state, size, duration, fullScreen, exception); + + @override + String toString() { + return "prepared:$prepared, completed:$completed, state:$state, size:$size, " + "duration:$duration, fullScreen:$fullScreen, exception:$exception"; + } +} + +@immutable +class FijkException implements Exception { + static const int ok = 0; + static const FijkException noException = FijkException(ok); + + /// local file or asset not found, + static const int local404 = -875574348; + + /// local io exception + static const int localIOe = -1162824012; + + /// Internal bug + static const int interBug = -558323010; + + /// Buffer too small + static const int smallBuf = -1397118274; + + /// Decoder not found + static const int noDecoder = -1128613112; + + /// Demuxer not found + static const int noDemuxer = -1296385272; + + /// Encoder not found + static const int noEncoder = -1129203192; + + /// End of file + static const int fileEnd = -541478725; + + /// Immediate exit was requested + static const int exitImm = -1414092869; + + /// Generic error in an external library + static const int extErr = -542398533; + + /// Filter not found + static const int noFilter = -1279870712; + + /// Invalid data found when processing input + static const int badData = -1094995529; + + /// Muxer not found + static const int noMuxer = -1481985528; + + /// Option not found + static const int noOption = -1414549496; + + /// Not yet implemented in FFmpeg, patches welcome + static const int noImplemented = -1163346256; + + /// Protocol not found + static const int noProtocol = -1330794744; + + /// Stream not found + static const int noStream = -1381258232; + + /// unknown error + static const int unknown = -1313558101; + + /// Http 400 + static const int http400 = -808465656; + + /// Http 401 + static const int http401 = -825242872; + + /// Http 403 + static const int http403 = -858797304; + + /// Http 404 + static const int http404 = -875574520; + + /// Http 4xx + static const int http4xx = -1482175736; + + /// Http 5xx + static const int http5xx = -1482175992; + + /// exception code + final int code; + + /// human readable exception message + final String message; + + const FijkException(code, [this.message]) : code = code; + + static FijkException fromPlatformException(PlatformException e) { + int code = int.tryParse(e.code); + return code != null + ? FijkException(code, e.message) + : FijkException(unknown, e.message); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FijkException && + runtimeType == other.runtimeType && + hashCode == other.hashCode; + + @override + int get hashCode => hashValues(code, message); + + @override + String toString() { + return "FijkException($code, $message)"; + } +} diff --git a/lib/src/fijkview.dart b/lib/src/fijkview.dart index 9a0d9dac..cce32286 100644 --- a/lib/src/fijkview.dart +++ b/lib/src/fijkview.dart @@ -23,15 +23,18 @@ // import 'dart:math'; +import 'dart:ui'; import 'package:fijkplayer/src/fijkpanel.dart'; import 'package:fijkplayer/src/fijkplugin.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'fijkpanel.dart'; import 'fijkplayer.dart'; +import 'fijkvalue.dart'; /// How a video should be inscribed into [FijkView]. /// @@ -205,8 +208,6 @@ class _FijkViewState extends State { void dispose() { super.dispose(); widget.player.removeListener(_fijkValueListener); - widget.player.release(); - print("FijkView dispose"); } double getAspectRatio(BoxConstraints constraints) { @@ -270,13 +271,13 @@ class _FijkViewState extends State { ); await SystemChrome.setEnabledSystemUIOverlays([]); - await FijkPlugin.setOrientationLandscape(context: context); + await FijkPlugin.setOrientationLandscape(); await Navigator.of(context).push(route); _fullScreen = false; widget.player.exitFullScreen(); await SystemChrome.setEnabledSystemUIOverlays( [SystemUiOverlay.top, SystemUiOverlay.bottom]); - await FijkPlugin.setOrientationPortrait(context: context); + await FijkPlugin.setOrientationPortrait(); } Widget buildTexture() { @@ -356,9 +357,11 @@ class _FijkViewState extends State { color: widget.color, ), Positioned.fromRect( - rect: pos, - child: buildTexture(), - ), + rect: pos, + child: Container( + color: Color(0xFF000000), + child: buildTexture(), + )), ]; if (widget.panelBuilder != null) { diff --git a/pubspec.yaml b/pubspec.yaml index e7aea0c5..cc4abc33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,9 @@ name: fijkplayer description: A Video Player Flutter plugin based on ijkplayer, support most popular protocols and codecs -version: 0.0.9 +version: 0.1.0 author: befovy homepage: https://fijkplayer.befovy.com +git: https://github.com/befovy/fijkplayer environment: sdk: ">=2.1.0 <3.0.0" diff --git a/test/fijkplayer_test.dart b/test/fijkplayer_test.dart index 6296453b..48169fca 100644 --- a/test/fijkplayer_test.dart +++ b/test/fijkplayer_test.dart @@ -15,19 +15,256 @@ //SOFTWARE. // +import 'dart:async'; +import 'dart:collection'; + +import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +class FijkPlayerTester { + final int id; + final MethodChannel playerEvent; + final MethodChannel playerMethod; + + FijkPlayerTester({this.id}) + : playerEvent = MethodChannel("befovy.com/fijkplayer/event/$id"), + playerMethod = MethodChannel("befovy.com/fijkplayer/$id") { + playerEvent.setMockMethodCallHandler(this.eventHandler); + playerMethod.setMockMethodCallHandler(this.playerHandler); + } + + MethodCodec get codec { + return playerEvent.codec; + } + + Future eventHandler(MethodCall call) { + switch (call.method) { + case 'listen': + case 'cancel': + return null; + default: + return Future.error("event Method invalid call name ${call.method}"); + } + } + + Future playerHandler(MethodCall call) async { + switch (call.method) { + case 'setupSurface': + return 1; + case 'setDateSource': + await sendEvent({'event': 'prepared'}); + await sendEvent( + {'event': 'state_change', 'new': 1, 'old': 0}); + return null; + case 'prepareAsync': + await sendEvent( + {'event': 'state_change', 'new': 2, 'old': 1}); + Future.delayed(Duration(milliseconds: 100), () { + sendEvent( + {'event': 'state_change', 'new': 3, 'old': 2}); + }); + return null; + case 'start': + return null; + case 'stop': + return null; + case 'setLoop': + return null; + case 'setSpeed': + return null; + default: + return Future.error("invalid call name ${call.method}"); + } + } + + void release() { + playerEvent.setMockMethodCallHandler(null); + playerMethod.setMockMethodCallHandler(null); + } + + Future sendEvent(dynamic event) { + return defaultBinaryMessenger.handlePlatformMessage( + "befovy.com/fijkplayer/event/$id", + codec.encodeSuccessEnvelope(event), + (ByteData data) {}); + } +} + +/// how to mock EventChannel from native side +/// https://github.com/flutter/flutter/issues/38954 void main() { - const MethodChannel channel = MethodChannel('befovy.com/fijk'); + const MethodChannel pluginChannel = MethodChannel('befovy.com/fijk'); + + Map playerTesters = HashMap(); + + int playerIncId = 0; + + setUpAll(() { + pluginChannel.setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'createPlayer': + playerIncId = playerIncId + 1; + FijkPlayerTester tester = FijkPlayerTester(id: playerIncId); + playerTesters[playerIncId] = tester; + return Future.value(playerIncId); + case 'releasePlayer': + Map args = methodCall.arguments as Map; + int pid = args["pid"]; + expect(pid, isNotNull); + FijkPlayerTester tester = playerTesters.remove(pid); + tester?.release(); + return null; + default: + return null; + } + }); + }); + + tearDownAll(() { + pluginChannel.setMockMethodCallHandler(null); + }); + + group("test FijkPlayer State", () { + test("Fijkplayer state", () async { + FijkPlayer player = FijkPlayer(); + await player.setupSurface(); + expect(player.state, FijkState.idle); + expect(player.value.prepared, false); + expect(player.value.completed, false); + await player.setDataSource("assets://butterfly.mp4"); + expect(player.state, FijkState.initialized); + + Completer prepared = Completer(); + player.addListener(() { + FijkValue value = player.value; + if (value.state == FijkState.prepared) { + prepared.complete(); + } + }); + + await player.prepareAsync(); + expect(player.state, FijkState.asyncPreparing); + await prepared.future; + expect(player.state, FijkState.prepared); + + expect(() async { + await player.setLoop(-1); + }, throwsArgumentError); + + expect(() async { + await player.setLoop(null); + }, throwsArgumentError); + + await player.setLoop(1); - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '42'; + await player.release(); + + expect(player.state, FijkState.end); }); }); - tearDown(() { - channel.setMockMethodCallHandler(null); + group("test FijkPlayer api", () { + test("create, release", () async { + FijkPlayer player = FijkPlayer(); + expect(player, isNotNull); + player.release(); + }); + + test("read only value", () async { + FijkPlayer player = FijkPlayer(); + bool changed = false; + player.addListener(() { + changed = true; + }); + FijkValue value = player.value; + expect(player.value.prepared, false); + value = value.copyWith(prepared: true); + expect(player.value.prepared, false); + expect(changed, false); + await player.release(); + }); + + test("setupSurface", () async { + FijkPlayer player = FijkPlayer(); + int tid = await player.setupSurface(); + expect(tid > 0, true); + }); + + test("setDataSource", () async { + FijkPlayer player = FijkPlayer(); + expect(() async { + await player.setDataSource(null); + }, throwsArgumentError); + expect(() async { + await player.setDataSource(""); + }, throwsArgumentError); + + await player.setDataSource("asset://butterfly.mp4"); + expect(player.state, FijkState.initialized); + await player.setDataSource("asset://butterfly.mp4"); + expect(player.state, FijkState.initialized); + + await player.prepareAsync(); + + expect(() async { + await player.setDataSource("asset://butterfly.mp4"); + }, throwsStateError); + }); + + test("setLoop", () async { + FijkPlayer player = FijkPlayer(); + await player.setLoop(1); + await player.setLoop(0); + await player.setLoop(2); + expect(() async { + await player.setLoop(-1); + }, throwsArgumentError); + expect(() async { + await player.setLoop(null); + }, throwsArgumentError); + }); + + test("setSpeed", () async { + FijkPlayer player = FijkPlayer(); + expect(() async { + await player.setSpeed(null); + }, throwsArgumentError); + expect(() async { + await player.setSpeed(0); + }, throwsArgumentError); + expect(() async { + await player.setSpeed(-1); + }, throwsArgumentError); + await player.setSpeed(1.5); + }); + + test("isPlayable", () async { + FijkPlayer player = FijkPlayer(); + expect(player.isPlayable(), false); + await player.setupSurface(); + expect(player.isPlayable(), false); + + Completer prepared = Completer(); + player.addListener(() { + FijkValue value = player.value; + if (value.state == FijkState.prepared) { + prepared.complete(); + } + }); + + await player.setDataSource("asset://butterfly.mp4"); + expect(player.isPlayable(), false); + expect(player.state, FijkState.initialized); + + await player.prepareAsync(); + expect(player.isPlayable(), false); + + await prepared.future; + expect(player.isPlayable(), true); + + await player.start(); + expect(player.isPlayable(), true); + }); }); } diff --git a/test/fijkvalue_test.dart b/test/fijkvalue_test.dart index 6e0dc2ff..d60f5c04 100644 --- a/test/fijkvalue_test.dart +++ b/test/fijkvalue_test.dart @@ -41,4 +41,17 @@ void main() { expect(value1.hashCode, value2.hashCode); }); }); + + test("FijkState must has same value as native state", () { + expect(FijkState.idle.index, 0); + expect(FijkState.initialized.index, 1); + expect(FijkState.asyncPreparing.index, 2); + expect(FijkState.prepared.index, 3); + expect(FijkState.started.index, 4); + expect(FijkState.paused.index, 5); + expect(FijkState.completed.index, 6); + expect(FijkState.stopped.index, 7); + expect(FijkState.error.index, 8); + expect(FijkState.end.index, 9); + }); }