├── .gitattributes ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── .classpath ├── .gitignore ├── .project ├── .settings │ └── org.eclipse.buildship.core.prefs ├── build.gradle ├── gradle.properties ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── github │ └── tcking │ └── gplayer │ ├── FlutterPlayerListener.java │ ├── GPlayerPlugin.java │ ├── GiraffePlayer.java │ ├── HttpRequest.java │ ├── LazyLoadManager.java │ ├── Option.java │ ├── PlayerListener.java │ ├── PlayerManager.java │ ├── QueuingEventSink.java │ ├── SoInstaller.java │ └── VideoInfo.java ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .project │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── app │ │ ├── .classpath │ │ ├── .project │ │ ├── .settings │ │ │ └── org.eclipse.buildship.core.prefs │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── github │ │ │ │ │ └── tcking │ │ │ │ │ └── gplayerexample │ │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── flutter_export_environment.sh │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ └── contents.xcworkspacedata │ └── Runner │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── main.m ├── lib │ └── main.dart └── pubspec.yaml ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── GplayerPlugin.h │ └── GplayerPlugin.m └── gplayer.podspec ├── lib ├── defaultmediacontrol.dart ├── gplayer.dart └── util.dart ├── pubspec.yaml └── screencap └── s1.gif /.gitattributes: -------------------------------------------------------------------------------- 1 | *.java linguist-language=Dart -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | .packages 4 | .vscode/ 5 | .pub/ 6 | build/ 7 | .atom/ 8 | .idea 9 | ios/.generated/ 10 | pubspec.lock 11 | Podfile.lock 12 | *.iml 13 | 14 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 4e68b7c7522a4048892fcb4c71dfb10f338339e1 8 | channel: master 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * init release. 4 | 5 | ## 0.0.2 6 | 7 | * support float window 8 | * fixed some bugs 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Brian Egan 3 | 4 | Permission is hereby granted, free of charge, to any 5 | person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the 7 | Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, 9 | sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall 14 | be included in all copies or substantial portions of 15 | the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPlayer 2 | 3 | [![pub package](https://img.shields.io/pub/v/gplayer.svg)](https://pub.dartlang.org/packages/gplayer) 4 | 5 | Video Player plugin for Flutter,On Android, the backing player is base on [ijkplayer 0.8.8](https://github.com/Bilibili/ijkplayer) (not implement on iOS) 6 | 7 | ![The example app running in Android](https://raw.githubusercontent.com/tcking/GPlayer/master/screencap/s1.gif) 8 | 9 | 10 | ## features 11 | 1. base on ijkplayer(ffmpeg),support RTMP , HLS (http & https) , MP4,M4A etc. 12 | 2. gestures for volume control 13 | 3. gestures for brightness control 14 | 4. gestures for forward or backward 15 | 5. support fullscreen 16 | 6. try to replay when error(only for live video) 17 | 7. specify video scale type 18 | 8. support lazy load (download player on demand) 19 | 9. customize media controller (without change this project source code) 20 | 21 | 22 | *note:* this using lazy load for default,it will take a few seconds to download decoders before first play, 23 | if you want to include the decoder in your apk just find the `android/build.gradle` and add the dependencies 24 | which you want to support. 25 | 26 | ## Getting Started 27 | 28 | ### 1.add dependency 29 | First, add `gplayer` as a [dependency in your pubspec.yaml file](https://flutter.io/using-packages/). 30 | 31 | ``` yaml 32 | dependencies: 33 | flutter: 34 | sdk: flutter 35 | 36 | # add gplayer dependency 37 | gplayer: ^0.0.2 38 | ``` 39 | 40 | ### 2.create player 41 | 42 | ``` dart 43 | import 'package:flutter/material.dart'; 44 | import 'package:gplayer/gplayer.dart'; 45 | 46 | void main() => runApp(MyApp()); 47 | 48 | class MyApp extends StatefulWidget { 49 | @override 50 | _MyAppState createState() => _MyAppState(); 51 | } 52 | 53 | class _MyAppState extends State { 54 | GPlayer player; 55 | @override 56 | void initState() { 57 | super.initState(); 58 | //1.create & init player 59 | player = GPlayer(uri: 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4') 60 | ..init() 61 | ..addListener((_) { 62 | //update control button out of player 63 | setState(() {}); 64 | }); 65 | } 66 | 67 | @override 68 | void dispose() { 69 | player?.dispose(); //2.release player 70 | super.dispose(); 71 | } 72 | 73 | @override 74 | Widget build(BuildContext context) { 75 | return MaterialApp( 76 | title: 'Video Demo', 77 | home: Scaffold( 78 | appBar: AppBar( 79 | title: Text('GPlayer'), 80 | ), 81 | body: player.display,//3.put the player display in Widget tree 82 | floatingActionButton: FloatingActionButton( 83 | onPressed: () { 84 | setState(() { 85 | player.isPlaying ? player.pause() : player.start(); 86 | }); 87 | }, 88 | child: Icon( 89 | player.isPlaying ? Icons.pause : Icons.play_arrow, 90 | ), 91 | ), 92 | ), 93 | ); 94 | } 95 | } 96 | 97 | ``` 98 | ## Customize media contoller 99 | 100 | 1.define a class extend from `buildMediaController` 101 | 102 | 2.implement method `Widget buildMediaController(BuildContext context)` 103 | 104 | 3.pass the instance to player constructor `GPlayer(uri:'',mediaController:MyMeidaController())` -------------------------------------------------------------------------------- /android/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | gplayer 4 | Project gplayer created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=../example/android 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.github.tcking.gplayer' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.2.1' 12 | } 13 | } 14 | 15 | rootProject.allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | 24 | android { 25 | compileSdkVersion 28 26 | 27 | defaultConfig { 28 | minSdkVersion 21 29 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 30 | } 31 | lintOptions { 32 | disable 'InvalidPackage' 33 | } 34 | } 35 | 36 | dependencies{ 37 | compile 'com.github.tcking:ijkplayer-java:0.8.8' 38 | 39 | //not want to using lazy load,uncomment the line which aib you want to support 40 | // api 'com.github.tcking:ijkplayer-armv7a:0.8.8-full' //support armv7 41 | // api 'com.github.tcking:ijkplayer-arm64:0.8.8-full' //support arm64 42 | // api 'com.github.tcking:ijkplayer-armv5:0.8.8-full' //support armv5 43 | // api 'com.github.tcking:ijkplayer-x86:0.8.8-full' //support x86 44 | // api 'com.github.tcking:ijkplayer-x86_64:0.8.8-full' //support x86_64 45 | } -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'gplayer' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/tcking/gplayer/FlutterPlayerListener.java: -------------------------------------------------------------------------------- 1 | package com.github.tcking.gplayer; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import io.flutter.plugin.common.EventChannel; 7 | import tv.danmaku.ijk.media.player.IjkTimedText; 8 | 9 | /** 10 | * Created by TangChao on 2019/2/18. 11 | */ 12 | public class FlutterPlayerListener implements PlayerListener { 13 | private QueuingEventSink eventSink=new QueuingEventSink(); 14 | private EventChannel eventChannel; 15 | 16 | public FlutterPlayerListener(String fingerprint) { 17 | 18 | eventChannel = new EventChannel(PlayerManager.getInstance().getRegistrar().messenger(),"com.github.tcking/gplayer/"+fingerprint); 19 | eventChannel.setStreamHandler(new EventChannel.StreamHandler() { 20 | @Override 21 | public void onListen(Object o, EventChannel.EventSink eventSink) { 22 | FlutterPlayerListener.this.eventSink.setDelegate(eventSink); 23 | } 24 | 25 | @Override 26 | public void onCancel(Object o) { 27 | FlutterPlayerListener.this.eventSink.setDelegate(null); 28 | } 29 | }); 30 | } 31 | 32 | @Override 33 | public void onPrepared(GiraffePlayer giraffePlayer) { 34 | // eventSink.success(); 35 | HashMap p=new HashMap(); 36 | p.put("event","onPrepared"); 37 | p.put("duration",giraffePlayer.getDuration()); 38 | eventSink.success(p); 39 | } 40 | 41 | @Override 42 | public void onBufferingUpdate(GiraffePlayer giraffePlayer, int percent) { 43 | 44 | } 45 | 46 | @Override 47 | public boolean onInfo(GiraffePlayer giraffePlayer, int what, int extra) { 48 | return false; 49 | } 50 | 51 | @Override 52 | public void onCompletion(GiraffePlayer giraffePlayer) { 53 | 54 | } 55 | 56 | @Override 57 | public void onSeekComplete(GiraffePlayer giraffePlayer) { 58 | HashMap p=new HashMap(); 59 | p.put("event","onSeekComplete"); 60 | p.put("position",giraffePlayer.getCurrentPosition()); 61 | eventSink.success(p); 62 | } 63 | 64 | @Override 65 | public boolean onError(GiraffePlayer giraffePlayer, int what, int extra,String msg) { 66 | HashMap p=new HashMap(); 67 | p.put("event","onError"); 68 | p.put("what",what); 69 | p.put("extra",extra); 70 | p.put("msg",msg); 71 | eventSink.success(p); 72 | return true; 73 | } 74 | 75 | @Override 76 | public void onPause(GiraffePlayer giraffePlayer) { 77 | 78 | } 79 | 80 | @Override 81 | public void onRelease(GiraffePlayer giraffePlayer) { 82 | eventSink.endOfStream(); 83 | eventChannel.setStreamHandler(null); 84 | } 85 | 86 | @Override 87 | public void onStart(GiraffePlayer giraffePlayer) { 88 | 89 | } 90 | 91 | @Override 92 | public void onTargetStateChange(int oldState, int newState) { 93 | HashMap p=new HashMap(); 94 | p.put("event","onTargetStateChange"); 95 | p.put("oldState",oldState); 96 | p.put("newState",newState); 97 | eventSink.success(p); 98 | } 99 | 100 | @Override 101 | public void onCurrentStateChange(int oldState, int newState) { 102 | HashMap p=new HashMap(); 103 | p.put("event","onCurrentStateChange"); 104 | p.put("oldState",oldState); 105 | p.put("newState",newState); 106 | eventSink.success(p); 107 | } 108 | 109 | @Override 110 | public void onDisplayModelChange(int oldModel, int newModel) { 111 | 112 | } 113 | 114 | @Override 115 | public void onPreparing(GiraffePlayer giraffePlayer) { 116 | 117 | } 118 | 119 | @Override 120 | public void onTimedText(GiraffePlayer giraffePlayer, IjkTimedText text) { 121 | 122 | } 123 | 124 | @Override 125 | public void onLazyLoadProgress(GiraffePlayer giraffePlayer, int progress) { 126 | Map p=new HashMap(); 127 | p.put("event","onLazyLoadProgress"); 128 | p.put("progress",progress); 129 | eventSink.success(p); 130 | } 131 | 132 | @Override 133 | public void onLazyLoadError(GiraffePlayer giraffePlayer, String message) { 134 | Map p=new HashMap(); 135 | p.put("event","onLazyLoadError"); 136 | p.put("message",message); 137 | eventSink.success(p); 138 | } 139 | 140 | @Override 141 | public void onSetDisplay(GiraffePlayer giraffePlayer, long id) { 142 | Map p=new HashMap(); 143 | p.put("event","onSetDisplay"); 144 | p.put("textureId",id); 145 | eventSink.success(p); 146 | } 147 | 148 | @Override 149 | public void onVideoSizeChanged(GiraffePlayer giraffePlayer, int videoWidth, int videoHeight) { 150 | Map p=new HashMap(); 151 | p.put("event","onVideoSizeChanged"); 152 | p.put("videoWidth",videoWidth); 153 | p.put("videoHeight",videoHeight); 154 | eventSink.success(p); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/tcking/gplayer/GPlayerPlugin.java: -------------------------------------------------------------------------------- 1 | package com.github.tcking.gplayer; 2 | 3 | import android.content.Context; 4 | import android.media.AudioManager; 5 | import android.view.Window; 6 | import android.view.WindowManager; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import io.flutter.plugin.common.MethodCall; 12 | import io.flutter.plugin.common.MethodChannel; 13 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 14 | import io.flutter.plugin.common.MethodChannel.Result; 15 | import io.flutter.plugin.common.PluginRegistry; 16 | import io.flutter.plugin.common.PluginRegistry.Registrar; 17 | import io.flutter.view.FlutterNativeView; 18 | 19 | /** 20 | * GPlayerPlugin 21 | */ 22 | public class GPlayerPlugin implements MethodCallHandler { 23 | private final Registrar registrar; 24 | 25 | private GPlayerPlugin(Registrar registrar) { 26 | this.registrar = registrar; 27 | PlayerManager.getInstance().onPluginInit(registrar); 28 | } 29 | 30 | // ----------------- 31 | 32 | 33 | // ----------------- 34 | 35 | /** 36 | * Plugin registration. 37 | */ 38 | public static void registerWith(Registrar registrar) { 39 | final MethodChannel channel = new MethodChannel(registrar.messenger(), "com.github.tcking/gplayer"); 40 | final GPlayerPlugin plugin = new GPlayerPlugin(registrar); 41 | channel.setMethodCallHandler(plugin); 42 | 43 | registrar.addViewDestroyListener(new PluginRegistry.ViewDestroyListener() { 44 | @Override 45 | public boolean onViewDestroy(FlutterNativeView view) { 46 | plugin.onDestroy(); 47 | return false; // We are not interested in assuming ownership of the NativeView. 48 | } 49 | }); 50 | } 51 | 52 | private void onDestroy() { 53 | PlayerManager.getInstance().onPluginDestroy(); 54 | } 55 | 56 | @Override 57 | public void onMethodCall(MethodCall call, Result result) { 58 | String fingerprint = call.argument("fingerprint"); 59 | if (fingerprint == null) { 60 | result.error("fingerprint is null", null, null); 61 | return; 62 | } 63 | 64 | if (call.method.equals("init")) { 65 | PlayerManager.getInstance().createPlayer(VideoInfo.from((Map) call.arguments)); 66 | result.success(null); 67 | return; 68 | } 69 | 70 | GiraffePlayer player = PlayerManager.getInstance().getPlayerByFingerprint(fingerprint); 71 | if (player == null) { 72 | result.error("can't find player for fingerprint:" + fingerprint, null, null); 73 | return; 74 | } 75 | 76 | if (call.method.equals("start")) { 77 | player.start(); 78 | result.success(null); 79 | } else if (call.method.equals("pause")) { 80 | player.pause(); 81 | result.success(null); 82 | }else if (call.method.equals("release")) { 83 | player.release(); 84 | result.success(null); 85 | } else if (call.method.equals("getCurrentPosition")) { 86 | result.success(player.getCurrentPosition()); 87 | } else if (call.method.equals("seekTo")) { 88 | System.out.println("seekTo:" + ((Map) call.arguments).get("position")); 89 | player.seekTo((Integer) ((Map) call.arguments).get("position")); 90 | result.success(null); 91 | } else if (call.method.equals("getAllInfo")) { 92 | AudioManager am = (AudioManager) registrar.context().getSystemService(Context.AUDIO_SERVICE); 93 | Map rsp = new HashMap<>(); 94 | rsp.put("maxVolume", am.getStreamMaxVolume(AudioManager.STREAM_MUSIC)); 95 | rsp.put("volume", am.getStreamVolume(AudioManager.STREAM_MUSIC)); 96 | rsp.put("currentPosition", player.getCurrentPosition()); 97 | 98 | 99 | Window window = registrar.activity().getWindow(); 100 | rsp.put("screenBrightness", window.getAttributes().screenBrightness); 101 | 102 | result.success(rsp); 103 | } else if (call.method.equals("setStreamVolume")) { 104 | AudioManager am = (AudioManager) registrar.context().getSystemService(Context.AUDIO_SERVICE); 105 | am.setStreamVolume(AudioManager.STREAM_MUSIC, (Integer) ((Map) call.arguments).get("volume"), 0); 106 | result.success(null); 107 | } else if (call.method.equals("setScreenBrightness")) { 108 | Window window = registrar.activity().getWindow(); 109 | WindowManager.LayoutParams lpa = window.getAttributes(); 110 | double brightness = (Double) ((Map) call.arguments).get("brightness"); 111 | lpa.screenBrightness = (float) brightness; 112 | if (lpa.screenBrightness > 1.0f) { 113 | lpa.screenBrightness = 1.0f; 114 | } else if (lpa.screenBrightness < 0.01f) { 115 | lpa.screenBrightness = 0.01f; 116 | } 117 | window.setAttributes(lpa); 118 | result.success(null); 119 | } else { 120 | result.notImplemented(); 121 | } 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/tcking/gplayer/GiraffePlayer.java: -------------------------------------------------------------------------------- 1 | package com.github.tcking.gplayer; 2 | 3 | import android.content.Context; 4 | import android.media.AudioManager; 5 | import android.net.Uri; 6 | import android.os.Build; 7 | import android.os.Handler; 8 | import android.os.HandlerThread; 9 | import android.os.Looper; 10 | import android.os.Message; 11 | import android.os.Process; 12 | import android.util.Log; 13 | import android.view.Surface; 14 | import android.view.ViewGroup; 15 | import android.widget.MediaController; 16 | 17 | import java.io.IOException; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | import io.flutter.plugin.common.PluginRegistry; 22 | import io.flutter.view.TextureRegistry; 23 | import tv.danmaku.ijk.media.player.AndroidMediaPlayer; 24 | import tv.danmaku.ijk.media.player.IMediaPlayer; 25 | import tv.danmaku.ijk.media.player.IjkMediaPlayer; 26 | import tv.danmaku.ijk.media.player.IjkTimedText; 27 | import tv.danmaku.ijk.media.player.misc.ITrackInfo; 28 | 29 | 30 | /** 31 | * Created by tcking on 2017 32 | */ 33 | 34 | public class GiraffePlayer implements MediaController.MediaPlayerControl { 35 | public static final String TAG = "GiraffePlayer"; 36 | public static boolean debug = true; 37 | public static boolean nativeDebug = false; 38 | // Internal messages 39 | private static final int MSG_CTRL_PLAYING = 1; 40 | 41 | private static final int MSG_CTRL_PAUSE = 2; 42 | private static final int MSG_CTRL_SEEK = 3; 43 | private static final int MSG_CTRL_RELEASE = 4; 44 | private static final int MSG_CTRL_RETRY = 5; 45 | private static final int MSG_CTRL_SELECT_TRACK = 6; 46 | private static final int MSG_CTRL_DESELECT_TRACK = 7; 47 | private static final int MSG_CTRL_SET_VOLUME = 8; 48 | 49 | 50 | private static final int MSG_SET_DISPLAY = 12; 51 | 52 | 53 | // all possible internal states 54 | public static final int STATE_ERROR = -1; 55 | public static final int STATE_IDLE = 0; 56 | public static final int STATE_PREPARING = 1; 57 | public static final int STATE_PREPARED = 2; 58 | public static final int STATE_PLAYING = 3; 59 | public static final int STATE_PAUSED = 4; 60 | public static final int STATE_PLAYBACK_COMPLETED = 5; 61 | public static final int STATE_RELEASE = 6; 62 | public static final int STATE_LAZYLOADING = 7; 63 | public static final int STATE_BUFFERING = 8; 64 | 65 | private final HandlerThread internalPlaybackThread; 66 | private TextureRegistry.SurfaceTextureEntry surfaceTextureEntry; 67 | 68 | private int currentBufferPercentage = 0; 69 | private boolean canPause = true; 70 | private boolean canSeekBackward = true; 71 | private boolean canSeekForward = true; 72 | private int audioSessionId; 73 | 74 | private int currentState = STATE_IDLE; 75 | private int targetState = STATE_IDLE; 76 | private Uri uri; 77 | private Map headers = new HashMap<>(); 78 | 79 | private IMediaPlayer mediaPlayer; 80 | private volatile boolean released; 81 | private Handler handler; 82 | private PlayerListener playerListener; 83 | 84 | private volatile int startPosition = -1; 85 | private boolean mute = false; 86 | 87 | private VideoInfo videoInfo; 88 | private Context context; 89 | private Surface surface; 90 | 91 | 92 | private PlayerListener proxyListener() { 93 | return playerListener; 94 | } 95 | 96 | 97 | protected GiraffePlayer(final VideoInfo videoInfo) { 98 | final PluginRegistry.Registrar registrar = PlayerManager.getInstance().getRegistrar(); 99 | this.videoInfo = videoInfo; 100 | this.playerListener = new FlutterPlayerListener(videoInfo.getFingerprint()); 101 | 102 | this.context = registrar.context(); 103 | log("new GiraffePlayer"); 104 | internalPlaybackThread = new HandlerThread("GiraffePlayerInternal:Handler", Process.THREAD_PRIORITY_AUDIO); 105 | internalPlaybackThread.start(); 106 | handler = new Handler(internalPlaybackThread.getLooper(), new Handler.Callback() { 107 | @Override 108 | public boolean handleMessage(Message msg) { 109 | //init mediaPlayer before any actions 110 | log("handleMessage:" + msg.what); 111 | if (msg.what == MSG_CTRL_RELEASE) { 112 | if (!released) { 113 | handler.removeCallbacks(null); 114 | currentState(STATE_RELEASE); 115 | doRelease(((String) msg.obj)); 116 | } 117 | return true; 118 | } 119 | if (mediaPlayer == null || released) { 120 | handler.removeCallbacks(null); 121 | try { 122 | init(); 123 | handler.sendMessage(Message.obtain(msg)); 124 | } catch (UnsatisfiedLinkError e) { 125 | log("UnsatisfiedLinkError:" + e); 126 | currentState(STATE_LAZYLOADING); 127 | LazyLoadManager.Load(context, videoInfo.getFingerprint(), Message.obtain(msg)); 128 | } 129 | return true; 130 | } 131 | switch (msg.what) { 132 | case MSG_CTRL_PLAYING: 133 | if (currentState == STATE_ERROR) { 134 | handler.sendEmptyMessage(MSG_CTRL_RETRY); 135 | } else if (isInPlaybackState()) { 136 | if (canSeekForward) { 137 | if (currentState == STATE_PLAYBACK_COMPLETED) { 138 | startPosition = 0; 139 | } 140 | if (startPosition >= 0) { 141 | mediaPlayer.seekTo(startPosition); 142 | startPosition = -1; 143 | } 144 | } 145 | mediaPlayer.start(); 146 | currentState(STATE_PLAYING); 147 | } 148 | break; 149 | case MSG_CTRL_PAUSE: 150 | mediaPlayer.pause(); 151 | currentState(STATE_PAUSED); 152 | break; 153 | case MSG_CTRL_SEEK: 154 | if (!canSeekForward) { 155 | break; 156 | } 157 | int position = (int) msg.obj; 158 | mediaPlayer.seekTo(position); 159 | break; 160 | case MSG_CTRL_SELECT_TRACK: 161 | int track = (int) msg.obj; 162 | if (mediaPlayer instanceof IjkMediaPlayer) { 163 | ((IjkMediaPlayer) mediaPlayer).selectTrack(track); 164 | } else if (mediaPlayer instanceof AndroidMediaPlayer) { 165 | ((AndroidMediaPlayer) mediaPlayer).getInternalMediaPlayer().selectTrack(track); 166 | } 167 | break; 168 | case MSG_CTRL_DESELECT_TRACK: 169 | int deselectTrack = (int) msg.obj; 170 | if (mediaPlayer instanceof IjkMediaPlayer) { 171 | ((IjkMediaPlayer) mediaPlayer).deselectTrack(deselectTrack); 172 | } else if (mediaPlayer instanceof AndroidMediaPlayer) { 173 | ((AndroidMediaPlayer) mediaPlayer).getInternalMediaPlayer().deselectTrack(deselectTrack); 174 | } 175 | break; 176 | case MSG_SET_DISPLAY: 177 | // if(surfaceTextureEntry!=null){ 178 | // surfaceTextureEntry.release(); 179 | // } 180 | new Handler(Looper.getMainLooper()).post(new Runnable() { 181 | @Override 182 | public void run() { 183 | 184 | if (surfaceTextureEntry == null) { 185 | surfaceTextureEntry = registrar.textures().createSurfaceTexture(); 186 | } 187 | if (surface != null) { 188 | surface.release(); 189 | } 190 | surface = new Surface(surfaceTextureEntry.surfaceTexture()); 191 | mediaPlayer.setSurface(surface); 192 | proxyListener().onSetDisplay(GiraffePlayer.this, surfaceTextureEntry.id()); 193 | } 194 | }); 195 | break; 196 | case MSG_CTRL_RETRY: 197 | init(); 198 | handler.sendEmptyMessage(MSG_CTRL_PLAYING); 199 | break; 200 | case MSG_CTRL_SET_VOLUME: 201 | Map pram = (Map) msg.obj; 202 | mediaPlayer.setVolume(pram.get("left"), pram.get("right")); 203 | break; 204 | 205 | default: 206 | } 207 | return true; 208 | } 209 | }); 210 | } 211 | 212 | 213 | private boolean isInPlaybackState() { 214 | return (mediaPlayer != null && 215 | currentState != STATE_ERROR && 216 | currentState != STATE_IDLE && 217 | currentState != STATE_PREPARING); 218 | } 219 | 220 | 221 | @Override 222 | public void start() { 223 | if (currentState == STATE_PLAYBACK_COMPLETED && !canSeekForward) { 224 | releaseMediaPlayer(); 225 | } 226 | targetState(STATE_PLAYING); 227 | handler.sendEmptyMessage(MSG_CTRL_PLAYING); 228 | proxyListener().onStart(this); 229 | } 230 | 231 | private void targetState(final int newState) { 232 | final int oldTargetState = targetState; 233 | targetState = newState; 234 | if (oldTargetState != newState) { 235 | handler.post(new Runnable() { 236 | @Override 237 | public void run() { 238 | proxyListener().onTargetStateChange(oldTargetState, newState); 239 | } 240 | }); 241 | } 242 | } 243 | 244 | private void currentState(final int newState) { 245 | final int oldCurrentState = currentState; 246 | currentState = newState; 247 | if (oldCurrentState != newState) { 248 | handler.post(new Runnable() { 249 | @Override 250 | public void run() { 251 | proxyListener().onCurrentStateChange(oldCurrentState, newState); 252 | 253 | } 254 | }); 255 | } 256 | } 257 | 258 | @Override 259 | public void pause() { 260 | targetState(STATE_PAUSED); 261 | handler.sendEmptyMessage(MSG_CTRL_PAUSE); 262 | // playerListener().onPause(this); 263 | } 264 | 265 | @Override 266 | public int getDuration() { 267 | if (mediaPlayer == null) { 268 | return 0; 269 | } 270 | return (int) mediaPlayer.getDuration(); 271 | } 272 | 273 | @Override 274 | public int getCurrentPosition() { 275 | if (mediaPlayer == null) { 276 | return 0; 277 | } 278 | return (int) mediaPlayer.getCurrentPosition(); 279 | } 280 | 281 | @Override 282 | public void seekTo(int pos) { 283 | handler.obtainMessage(MSG_CTRL_SEEK, pos).sendToTarget(); 284 | } 285 | 286 | @Override 287 | public boolean isPlaying() { 288 | //mediaPlayer.isPlaying() 289 | return currentState == STATE_PLAYING; 290 | } 291 | 292 | @Override 293 | public int getBufferPercentage() { 294 | return currentBufferPercentage; 295 | } 296 | 297 | @Override 298 | public boolean canPause() { 299 | return canPause; 300 | } 301 | 302 | @Override 303 | public boolean canSeekBackward() { 304 | return canSeekBackward; 305 | } 306 | 307 | @Override 308 | public boolean canSeekForward() { 309 | return canSeekForward; 310 | } 311 | 312 | @Override 313 | public int getAudioSessionId() { 314 | if (audioSessionId == 0) { 315 | audioSessionId = mediaPlayer.getAudioSessionId(); 316 | } 317 | return audioSessionId; 318 | } 319 | 320 | 321 | /** 322 | * Sets video path. 323 | * 324 | * @param path the path of the video. 325 | */ 326 | private GiraffePlayer setVideoPath(String path) throws IOException { 327 | return setVideoURI(Uri.parse(path)); 328 | } 329 | 330 | /** 331 | * Sets video URI. 332 | * 333 | * @param uri the URI of the video. 334 | */ 335 | private GiraffePlayer setVideoURI(Uri uri) throws IOException { 336 | return setVideoURI(uri, null); 337 | } 338 | 339 | /** 340 | * Sets video URI using specific headers. 341 | * 342 | * @param uri the URI of the video. 343 | * @param headers the headers for the URI request. 344 | * Note that the cross domain redirection is allowed by default, but that can be 345 | * changed with key/value pairs through the headers parameter with 346 | * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value 347 | * to disallow or allow cross domain redirection. 348 | */ 349 | private GiraffePlayer setVideoURI(Uri uri, Map headers) throws IOException { 350 | this.uri = uri; 351 | this.headers.clear(); 352 | this.headers.putAll(headers); 353 | return this; 354 | } 355 | 356 | private void init() { 357 | log("init"); 358 | releaseMediaPlayer(); 359 | mediaPlayer = createMediaPlayer(); 360 | if (mediaPlayer instanceof IjkMediaPlayer) { 361 | IjkMediaPlayer.native_setLogLevel(nativeDebug ? IjkMediaPlayer.IJK_LOG_DEBUG : IjkMediaPlayer.IJK_LOG_ERROR); 362 | } 363 | setOptions(); 364 | released = false; 365 | mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 366 | mediaPlayer.setLooping(videoInfo.isLooping()); 367 | mediaPlayer.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() { 368 | @Override 369 | public void onPrepared(IMediaPlayer iMediaPlayer) { 370 | boolean live = mediaPlayer.getDuration() == 0; 371 | canSeekBackward = !live; 372 | canSeekForward = !live; 373 | handler.post(new Runnable() { 374 | @Override 375 | public void run() { 376 | proxyListener().onPrepared(GiraffePlayer.this); 377 | } 378 | }); 379 | currentState(STATE_PREPARED); 380 | if (targetState == STATE_PLAYING) { 381 | handler.sendEmptyMessage(MSG_CTRL_PLAYING); 382 | } 383 | } 384 | }); 385 | initInternalListener(); 386 | handler.obtainMessage(MSG_SET_DISPLAY).sendToTarget(); 387 | try { 388 | uri = videoInfo.getUri(); 389 | mediaPlayer.setDataSource(context, uri, headers); 390 | currentState(STATE_PREPARING); 391 | mediaPlayer.prepareAsync(); 392 | } catch (IOException e) { 393 | e.printStackTrace(); 394 | handler.post(new Runnable() { 395 | @Override 396 | public void run() { 397 | proxyListener().onError(GiraffePlayer.this, 0, 0, "network error"); 398 | } 399 | }); 400 | currentState(STATE_ERROR); 401 | } 402 | 403 | } 404 | 405 | private IMediaPlayer createMediaPlayer() { 406 | if (VideoInfo.PLAYER_IMPL_SYSTEM.equals(videoInfo.getPlayerImpl())) { 407 | return new AndroidMediaPlayer(); 408 | } 409 | return new IjkMediaPlayer(Looper.getMainLooper()); 410 | } 411 | 412 | private void setOptions() { 413 | headers.clear(); 414 | if (videoInfo.getOptions().size() <= 0) { 415 | return; 416 | } 417 | //https://ffmpeg.org/ffmpeg-protocols.html#http 418 | if (mediaPlayer instanceof IjkMediaPlayer) { 419 | IjkMediaPlayer ijkMediaPlayer = (IjkMediaPlayer) mediaPlayer; 420 | for (Option option : videoInfo.getOptions()) { 421 | if (option.getValue() instanceof String) { 422 | ijkMediaPlayer.setOption(option.getCategory(), option.getName(), ((String) option.getValue())); 423 | } else if (option.getValue() instanceof Integer) { 424 | ijkMediaPlayer.setOption(option.getCategory(), option.getName(), Long.valueOf((Integer) option.getValue())); 425 | } 426 | } 427 | } else if (mediaPlayer instanceof AndroidMediaPlayer) { 428 | for (Option option : videoInfo.getOptions()) { 429 | if (IjkMediaPlayer.OPT_CATEGORY_FORMAT == option.getCategory() && "headers".equals(option.getName())) { 430 | String h = "" + option.getValue(); 431 | String[] hs = h.split("\r\n"); 432 | for (String hd : hs) { 433 | String[] kv = hd.split(":"); 434 | String v = kv.length >= 2 ? kv[1] : ""; 435 | headers.put(kv[0], v); 436 | log("add header " + kv[0] + ":" + v); 437 | } 438 | break; 439 | } 440 | } 441 | } 442 | } 443 | 444 | private void initInternalListener() { 445 | //playerListener fire on main thread 446 | mediaPlayer.setOnBufferingUpdateListener(new IMediaPlayer.OnBufferingUpdateListener() { 447 | @Override 448 | public void onBufferingUpdate(IMediaPlayer iMediaPlayer, int percent) { 449 | currentBufferPercentage = percent; 450 | proxyListener().onBufferingUpdate(GiraffePlayer.this, percent); 451 | } 452 | }); 453 | mediaPlayer.setOnInfoListener(new IMediaPlayer.OnInfoListener() { 454 | private int lastCurrentState; 455 | 456 | //https://developer.android.com/reference/android/media/MediaPlayer.OnInfoListener.html 457 | @Override 458 | public boolean onInfo(IMediaPlayer iMediaPlayer, final int what, final int extra) { 459 | 460 | if (what == IMediaPlayer.MEDIA_INFO_VIDEO_ROTATION_CHANGED) { 461 | // ScalableTextureView currentDisplay = getCurrentDisplay(); 462 | // if (currentDisplay != null) { 463 | // currentDisplay.setRotation(extra); 464 | // } 465 | } 466 | log("onInfo:" + what); 467 | if (what == IMediaPlayer.MEDIA_INFO_BUFFERING_START) { 468 | lastCurrentState = currentState; 469 | currentState(STATE_BUFFERING); 470 | } else if (what == IMediaPlayer.MEDIA_INFO_BUFFERING_END) { 471 | if (lastCurrentState == targetState) { 472 | currentState(lastCurrentState); 473 | } 474 | } 475 | 476 | 477 | // handler.post(new Runnable() { 478 | // @Override 479 | // public void run() { 480 | // playerListener().onInfo(GiraffePlayer.this, what, extra); 481 | // } 482 | // }); 483 | 484 | return true; 485 | } 486 | }); 487 | mediaPlayer.setOnCompletionListener(new IMediaPlayer.OnCompletionListener() { 488 | @Override 489 | public void onCompletion(IMediaPlayer iMediaPlayer) { 490 | currentState(STATE_PLAYBACK_COMPLETED); 491 | proxyListener().onCompletion(GiraffePlayer.this); 492 | } 493 | }); 494 | mediaPlayer.setOnErrorListener(new IMediaPlayer.OnErrorListener() { 495 | @Override 496 | public boolean onError(IMediaPlayer iMediaPlayer, int what, int extra) { 497 | log("setOnErrorListener"); 498 | boolean b = proxyListener().onError(GiraffePlayer.this, what, extra, null); 499 | currentState(STATE_ERROR); 500 | int retryInterval = videoInfo.getRetryInterval(); 501 | if (retryInterval > 0) { 502 | log("replay delay " + retryInterval + " seconds"); 503 | handler.sendEmptyMessageDelayed(MSG_CTRL_RETRY, retryInterval * 1000); 504 | } 505 | return b; 506 | 507 | } 508 | }); 509 | mediaPlayer.setOnSeekCompleteListener(new IMediaPlayer.OnSeekCompleteListener() { 510 | @Override 511 | public void onSeekComplete(IMediaPlayer iMediaPlayer) { 512 | handler.post(new Runnable() { 513 | @Override 514 | public void run() { 515 | proxyListener().onSeekComplete(GiraffePlayer.this); 516 | } 517 | }); 518 | } 519 | }); 520 | mediaPlayer.setOnVideoSizeChangedListener(new IMediaPlayer.OnVideoSizeChangedListener() { 521 | @Override 522 | public void onVideoSizeChanged(final IMediaPlayer mp, int width, int height, int sarNum, int sarDen) { 523 | if (debug) { 524 | log("onVideoSizeChanged:width:" + width + ",height:" + height); 525 | } 526 | final int videoWidth = mp.getVideoWidth(); 527 | final int videoHeight = mp.getVideoHeight(); 528 | // int videoSarNum = mp.getVideoSarNum(); 529 | // int videoSarDen = mp.getVideoSarDen(); 530 | if (videoWidth != 0 && videoHeight != 0) { 531 | handler.post(new Runnable() { 532 | @Override 533 | public void run() { 534 | proxyListener().onVideoSizeChanged(GiraffePlayer.this, videoWidth, videoHeight); 535 | } 536 | }); 537 | } 538 | } 539 | }); 540 | mediaPlayer.setOnTimedTextListener(new IMediaPlayer.OnTimedTextListener() { 541 | @Override 542 | public void onTimedText(IMediaPlayer mp, IjkTimedText text) { 543 | proxyListener().onTimedText(GiraffePlayer.this, text); 544 | } 545 | }); 546 | } 547 | 548 | 549 | private void log(String msg) { 550 | if (debug) { 551 | Log.d(TAG, String.format("[fingerprint:%s] %s", videoInfo.getFingerprint(), msg)); 552 | } 553 | } 554 | 555 | 556 | private synchronized void doRelease(String fingerprint) { 557 | if (released) { 558 | return; 559 | } 560 | log("doRelease"); 561 | PlayerManager.getInstance().removePlayer(fingerprint); 562 | //1. quit handler thread 563 | internalPlaybackThread.quit(); 564 | //2. release media player 565 | releaseMediaPlayer(); 566 | //3. release surface 567 | if (surfaceTextureEntry != null) { 568 | surfaceTextureEntry.release(); 569 | surfaceTextureEntry = null; 570 | } 571 | if (surface != null) { 572 | surface.release(); 573 | surface = null; 574 | } 575 | released = true; 576 | } 577 | 578 | /** 579 | * only release media player not display 580 | */ 581 | private void releaseMediaPlayer() { 582 | if (mediaPlayer != null) { 583 | log("releaseMediaPlayer"); 584 | mediaPlayer.setSurface(null); 585 | mediaPlayer.release(); 586 | mediaPlayer = null; 587 | } 588 | } 589 | 590 | public void release() { 591 | log("try release"); 592 | if (released) { 593 | return; 594 | } 595 | //不能异步执行,release之后Flutter的NativeView对象已经销毁,surface release时会出错 596 | String fingerprint = videoInfo.getFingerprint(); 597 | // PlayerManager.getInstance().removePlayer(fingerprint); 598 | // handler.post(new Runnable() { 599 | // @Override 600 | // public void run() { 601 | // playerListener().onRelease(GiraffePlayer.this); 602 | // } 603 | // }); 604 | // handler.obtainMessage(MSG_CTRL_RELEASE, fingerprint).sendToTarget(); 605 | handler.removeCallbacks(null); 606 | // currentState(STATE_RELEASE); 607 | doRelease(fingerprint); 608 | } 609 | 610 | 611 | GiraffePlayer doMessage(Message message) { 612 | handler.sendMessage(message); 613 | return this; 614 | } 615 | 616 | 617 | void lazyLoadProgress(final int progress) { 618 | handler.post(new Runnable() { 619 | @Override 620 | public void run() { 621 | proxyListener().onLazyLoadProgress(GiraffePlayer.this, progress); 622 | } 623 | }); 624 | } 625 | 626 | public void lazyLoadError(final String message) { 627 | handler.post(new Runnable() { 628 | @Override 629 | public void run() { 630 | proxyListener().onLazyLoadError(GiraffePlayer.this, message); 631 | } 632 | }); 633 | } 634 | 635 | public void resume() { 636 | if (currentState == STATE_PAUSED) { 637 | targetState(STATE_PLAYING); 638 | handler.sendEmptyMessage(MSG_CTRL_PLAYING); 639 | } 640 | // playerListener().onPause(this); 641 | 642 | } 643 | 644 | class VideoViewAnimationListener { 645 | void onStart(ViewGroup src, ViewGroup target) { 646 | } 647 | 648 | void onEnd(ViewGroup src, ViewGroup target) { 649 | } 650 | } 651 | 652 | 653 | public void stop() { 654 | release(); 655 | } 656 | 657 | public boolean isReleased() { 658 | return released; 659 | } 660 | 661 | 662 | public ITrackInfo[] getTrackInfo() { 663 | if (mediaPlayer == null || released) { 664 | return new ITrackInfo[0]; 665 | } 666 | return mediaPlayer.getTrackInfo(); 667 | } 668 | 669 | public int getSelectedTrack(int trackType) { 670 | if (mediaPlayer == null || released) { 671 | return -1; 672 | } 673 | if (mediaPlayer instanceof IjkMediaPlayer) { 674 | return ((IjkMediaPlayer) mediaPlayer).getSelectedTrack(trackType); 675 | } else if (mediaPlayer instanceof AndroidMediaPlayer) { 676 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 677 | return ((AndroidMediaPlayer) mediaPlayer).getInternalMediaPlayer().getSelectedTrack(trackType); 678 | } 679 | } 680 | return -1; 681 | } 682 | 683 | public GiraffePlayer selectTrack(int track) { 684 | if (mediaPlayer == null || released) { 685 | return this; 686 | } 687 | handler.removeMessages(MSG_CTRL_SELECT_TRACK); 688 | handler.obtainMessage(MSG_CTRL_SELECT_TRACK, track).sendToTarget(); 689 | return this; 690 | } 691 | 692 | public GiraffePlayer deselectTrack(int selectedTrack) { 693 | if (mediaPlayer == null || released) { 694 | return this; 695 | } 696 | handler.removeMessages(MSG_CTRL_DESELECT_TRACK); 697 | handler.obtainMessage(MSG_CTRL_DESELECT_TRACK, selectedTrack).sendToTarget(); 698 | return this; 699 | } 700 | 701 | /** 702 | * get current player state 703 | * 704 | * @return state 705 | */ 706 | public int getCurrentState() { 707 | return currentState; 708 | } 709 | 710 | 711 | /** 712 | * set volume 713 | * 714 | * @param left [0,1] 715 | * @param right [0,1] 716 | * @return GiraffePlayer 717 | */ 718 | public GiraffePlayer setVolume(float left, float right) { 719 | if (mediaPlayer == null || released) { 720 | return this; 721 | } 722 | HashMap pram = new HashMap<>(); 723 | pram.put("left", left); 724 | pram.put("right", right); 725 | handler.removeMessages(MSG_CTRL_SET_VOLUME); 726 | handler.obtainMessage(MSG_CTRL_SET_VOLUME, pram).sendToTarget(); 727 | return this; 728 | } 729 | 730 | /** 731 | * set mute 732 | * 733 | * @param mute 734 | * @return GiraffePlayer 735 | */ 736 | public GiraffePlayer setMute(boolean mute) { 737 | this.mute = mute; 738 | AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 739 | audioManager.setStreamMute(AudioManager.STREAM_MUSIC, mute); 740 | return this; 741 | } 742 | 743 | /** 744 | * is mute 745 | * 746 | * @return true if mute 747 | */ 748 | public boolean isMute() { 749 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 750 | AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 751 | return audioManager.isStreamMute(AudioManager.STREAM_MUSIC); 752 | } else { 753 | return mute; 754 | } 755 | } 756 | 757 | /** 758 | * set looping play 759 | * 760 | * @param looping 761 | * @return 762 | */ 763 | public GiraffePlayer setLooping(boolean looping) { 764 | if (mediaPlayer != null && !released) { 765 | mediaPlayer.setLooping(looping); 766 | } 767 | return this; 768 | } 769 | 770 | /** 771 | * @return is looping play 772 | */ 773 | public boolean isLooping() { 774 | if (mediaPlayer != null && !released) { 775 | return mediaPlayer.isLooping(); 776 | } 777 | return false; 778 | } 779 | 780 | } 781 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/tcking/gplayer/LazyLoadManager.java: -------------------------------------------------------------------------------- 1 | package com.github.tcking.gplayer; 2 | 3 | import android.app.IntentService; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.pm.ApplicationInfo; 7 | import android.os.Build; 8 | import android.os.Message; 9 | import android.util.Log; 10 | 11 | 12 | import java.io.BufferedInputStream; 13 | import java.io.File; 14 | import java.io.FileInputStream; 15 | import java.io.FileOutputStream; 16 | import java.io.IOException; 17 | import java.net.HttpURLConnection; 18 | import java.util.Enumeration; 19 | import java.util.zip.ZipEntry; 20 | import java.util.zip.ZipFile; 21 | import java.util.zip.ZipInputStream; 22 | 23 | /** 24 | * Created by TangChao on 2018/2/11. 25 | */ 26 | 27 | public class LazyLoadManager extends IntentService { 28 | private static final String TAG = "LazyLoadManager"; 29 | private static String KEY_FINGERPRINT = "fingerprint"; 30 | private static final int STATUS_IDLE = 0; 31 | private static final int STATUS_LOADING = 1; 32 | private static final int STATUS_OK = 2; 33 | private static final int STATUS_ERROR = -1; 34 | private static int loadStatus = STATUS_IDLE; 35 | private static String lastFingprint; 36 | private static Message lastMessage; 37 | private static final String soVersion = "0.8.8"; 38 | private static String abi; 39 | private int progress = -1; 40 | 41 | public static void setSoFetcher(SoFetcher soFetcher) { 42 | if (soFetcher != null) { 43 | LazyLoadManager.soFetcher = soFetcher; 44 | } 45 | } 46 | 47 | private static SoFetcher soFetcher=new SoFetcher(){ 48 | 49 | @Override 50 | public String getURL(String abi, String soVersion) { 51 | return String.format("https://raw.githubusercontent.com/tcking/GiraffePlayerLazyLoadFiles/master/%s/%s/so.zip", soVersion, abi); 52 | } 53 | }; 54 | 55 | /** 56 | * Creates an IntentService. Invoked by your subclass's constructor. 57 | * 58 | * @param name Used to name the worker thread, important only for debugging. 59 | */ 60 | public LazyLoadManager() { 61 | super("GiraffePlayerLazyLoader"); 62 | } 63 | 64 | @Override 65 | protected void onHandleIntent(Intent intent) { 66 | String fingerprint = intent.getStringExtra(KEY_FINGERPRINT); 67 | log(fingerprint, "loadStatus:" + loadStatus); 68 | if (loadStatus > 0) { 69 | sendLastMessage(); 70 | return; 71 | } 72 | loadStatus = STATUS_LOADING; 73 | //https://raw.githubusercontent.com/tcking/GiraffePlayerLazyLoadFiles/master/0.1.17/armeabi/so.zip 74 | Context context = getApplicationContext(); 75 | try { 76 | File playerDir = getPlayerRootDir(context); 77 | File soDir = getPlayerSoDir(context); 78 | log(fingerprint,"soDir:"+soDir); 79 | if (new File(soDir, "libijkffmpeg.so").exists()) { 80 | log(fingerprint,"so files downloaded,try install"); 81 | installSo(soDir); 82 | return; 83 | } 84 | 85 | String abi = getCurrentAbi(context); 86 | String url = soFetcher.getURL(abi, soVersion); 87 | log(fingerprint, "download so from:" + url); 88 | progress = -1; 89 | HttpRequest request = HttpRequest.get(url); 90 | int code = request.code(); 91 | log(fingerprint, "server:" + code); 92 | if (code == HttpURLConnection.HTTP_OK) { 93 | File tmp = new File(new File(playerDir, "tmp"), "so.zip"); 94 | tmp.delete(); 95 | tmp.getParentFile().mkdirs(); 96 | tmp.createNewFile(); 97 | FileOutputStream fileOutputStream = new FileOutputStream(tmp); 98 | BufferedInputStream buffer = request.buffer(); 99 | byte data[] = new byte[4096]; 100 | long total = 0; 101 | int count; 102 | int contentLength = request.contentLength(); 103 | while ((count = buffer.read(data)) != -1) { 104 | total += count; 105 | // publishing the progress.... 106 | if (contentLength > 0) { 107 | publishProgress((int) (total * 100 / contentLength)); 108 | } 109 | fileOutputStream.write(data, 0, count); 110 | } 111 | fileOutputStream.close(); 112 | 113 | unZip(tmp, soDir); 114 | tmp.delete(); 115 | installSo(soDir); 116 | } else { 117 | error("server response " +code); 118 | } 119 | } catch (Exception e) { 120 | e.printStackTrace(); 121 | error(e.getMessage()); 122 | } 123 | 124 | 125 | } 126 | 127 | private void error(String message) { 128 | loadStatus = STATUS_ERROR; 129 | if (lastFingprint != null) { 130 | GiraffePlayer player = PlayerManager.getInstance().getPlayerByFingerprint(lastFingprint); 131 | if (player != null) { 132 | player.lazyLoadError(message); 133 | } 134 | } 135 | 136 | } 137 | 138 | private void installSo(File soDir) throws Exception { 139 | SoInstaller.installNativeLibraryPath(GiraffePlayer.class.getClassLoader(), soDir); 140 | log(lastFingprint,"so installed"); 141 | loadStatus = STATUS_OK; 142 | sendLastMessage(); 143 | } 144 | 145 | private void publishProgress(int _progress) { 146 | if (progress == _progress) { 147 | return; 148 | } 149 | progress = _progress; 150 | if (lastFingprint != null) { 151 | 152 | GiraffePlayer player = PlayerManager.getInstance().getPlayerByFingerprint(lastFingprint); 153 | if (player != null) { 154 | player.lazyLoadProgress(progress); 155 | } 156 | } 157 | } 158 | 159 | /** 160 | * replay last message to the player 161 | */ 162 | private void sendLastMessage() { 163 | GiraffePlayer player = PlayerManager.getInstance().getPlayerByFingerprint(lastFingprint); 164 | if (player != null) { 165 | player.doMessage(lastMessage); 166 | } 167 | lastMessage = null; 168 | lastFingprint = null; 169 | } 170 | 171 | public static void Load(Context context, String fingerprint, Message message) { 172 | 173 | lastFingprint = fingerprint; 174 | lastMessage = message; 175 | 176 | Intent service = new Intent(context, LazyLoadManager.class); 177 | service.putExtra(KEY_FINGERPRINT, fingerprint); 178 | context.startService(service); 179 | } 180 | 181 | private void log(String fingerprint, String msg) { 182 | if (GiraffePlayer.debug) { 183 | Log.d(TAG, String.format("[fingerprint:%s] %s", fingerprint, msg)); 184 | } 185 | } 186 | 187 | 188 | public static String getCurrentAbi(Context context) { 189 | if (abi != null) { 190 | return abi; 191 | } 192 | ApplicationInfo applicationInfo = context.getApplicationInfo(); 193 | String apkPath = applicationInfo.sourceDir; 194 | ZipFile apk; 195 | String[] abis; 196 | if (Build.VERSION.SDK_INT >= 21) { 197 | abis = Build.SUPPORTED_ABIS; 198 | } else { 199 | abis = new String[]{Build.CPU_ABI, Build.CPU_ABI2}; 200 | } 201 | try { 202 | apk = new ZipFile(apkPath); 203 | for (String _abi : abis) { 204 | Enumeration entryEnumeration = apk.entries(); 205 | while (entryEnumeration.hasMoreElements()) { 206 | ZipEntry entry = entryEnumeration.nextElement(); 207 | if (entry == null) { 208 | continue; 209 | } 210 | String entryName = entry.getName(); 211 | if (entryName.startsWith("lib/") && entryName.endsWith("so")) { 212 | int start = entryName.indexOf("/"); 213 | int end = entryName.lastIndexOf("/"); 214 | String temp = entryName.substring(start + 1, end); 215 | if (_abi.equalsIgnoreCase(temp)) { 216 | abi = _abi; 217 | return abi; 218 | } 219 | } 220 | } 221 | 222 | } 223 | } catch (IOException e) { 224 | e.printStackTrace(); 225 | } 226 | abi = abis[0]; 227 | return abi; 228 | } 229 | 230 | 231 | public static void unZip(File input, File output) throws Exception { 232 | output.mkdirs(); 233 | ZipInputStream inZip = new ZipInputStream(new FileInputStream(input)); 234 | ZipEntry zipEntry; 235 | String szName; 236 | while ((zipEntry = inZip.getNextEntry()) != null) { 237 | szName = zipEntry.getName(); 238 | if (zipEntry.isDirectory()) { 239 | // get the folder name of the widget 240 | szName = szName.substring(0, szName.length() - 1); 241 | File folder = new File(output.getAbsolutePath() + File.separator + szName); 242 | folder.mkdirs(); 243 | } else { 244 | 245 | File file = new File(output.getAbsolutePath() + File.separator + szName); 246 | file.createNewFile(); 247 | // get the output stream of the file 248 | FileOutputStream out = new FileOutputStream(file); 249 | int len; 250 | byte[] buffer = new byte[1024]; 251 | // read (len) bytes into buffer 252 | while ((len = inZip.read(buffer)) != -1) { 253 | // write (len) byte from buffer at the position 0 254 | out.write(buffer, 0, len); 255 | out.flush(); 256 | } 257 | out.close(); 258 | } 259 | } 260 | inZip.close(); 261 | } 262 | 263 | public static File getPlayerRootDir(Context context) { 264 | return context.getDir("giraffePlayer2", Context.MODE_PRIVATE); 265 | 266 | } 267 | 268 | public static File getPlayerSoDir(Context context) { 269 | return new File(getPlayerRootDir(context).getAbsolutePath()+ File.separator+soVersion+ File.separator+"so"); 270 | 271 | } 272 | 273 | public interface SoFetcher{ 274 | String getURL(String abi, String soVersion); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/tcking/gplayer/Option.java: -------------------------------------------------------------------------------- 1 | package com.github.tcking.gplayer; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | import java.util.Collection; 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | import tv.danmaku.ijk.media.player.IjkMediaPlayer; 12 | 13 | /** 14 | * Created by tcking on 2017 15 | */ 16 | 17 | public class Option implements Serializable, Cloneable { 18 | private int category; 19 | private String name; 20 | private Object value; 21 | 22 | private Option(int category, String name, Object value) { 23 | this.category = category; 24 | this.name = name; 25 | this.value = value; 26 | } 27 | 28 | public static Option create(int category, String name, Long value) { 29 | return new Option(category, name, value); 30 | } 31 | 32 | public static Collection from(List options) { 33 | Set