├── .github └── workflows │ ├── build_android.yml │ ├── build_ios.yml │ ├── build_web.yml │ ├── create-documentation-pr.yml │ ├── create-release-from-changelog.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── flutter │ │ └── plugins │ │ └── GeneratedPluginRegistrant.java ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── video │ └── api │ └── flutter │ └── player │ ├── ApiVideoPlayerPlugin.kt │ ├── Extensions.kt │ ├── FlutterPlayerController.kt │ ├── FlutterPlayerInterface.kt │ ├── FlutterPlayerView.kt │ └── MethodCallHandler.kt ├── example ├── .gitignore ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── video │ │ │ │ │ └── api │ │ │ │ │ └── apivideo_player_example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ ├── ic_api_video.xml │ │ │ │ └── launch_background.xml │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── 1024.png │ │ │ ├── 120 1.png │ │ │ ├── 120 2.png │ │ │ ├── 152.png │ │ │ ├── 167.png │ │ │ ├── 180.png │ │ │ ├── 20.png │ │ │ ├── 29.png │ │ │ ├── 40 1.png │ │ │ ├── 40 2.png │ │ │ ├── 40.png │ │ │ ├── 58 1.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 76.png │ │ │ ├── 80 1.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ └── Contents.json │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ └── main.dart ├── pubspec.yaml ├── test │ └── widget_test.dart └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png │ ├── index.html │ └── manifest.json ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── ApiVideoPlayerPlugin.h │ ├── ApiVideoPlayerPlugin.m │ ├── FlutterPlayerController.swift │ ├── FlutterPlayerView.swift │ ├── MethodCallHandler.swift │ └── SwiftApiVideoPlayerPlugin.swift └── apivideo_player.podspec ├── lib ├── apivideo_player.dart └── src │ ├── apivideo_player_controller.dart │ ├── apivideo_player_life_cycle_observer.dart │ ├── apivideo_types.dart │ ├── apivideo_types.g.dart │ ├── platform │ ├── apivideo_mobile_player_platform.dart │ ├── apivideo_player_platform_interface.dart │ ├── apivideo_player_web.dart │ └── web │ │ ├── javascript_controller.dart │ │ └── utils │ │ ├── conversion.dart │ │ └── player_event_type_extension.dart │ ├── style │ ├── apivideo_colors.dart │ ├── apivideo_icons.dart │ ├── apivideo_label.dart │ ├── apivideo_style.dart │ └── fonts │ │ └── ApiVideoIcons.ttf │ ├── utils │ └── extensions │ │ └── duration_extension.dart │ └── widgets │ ├── apivideo_player.dart │ ├── apivideo_player_controls_bar.dart │ ├── apivideo_player_opacity.dart │ ├── apivideo_player_overlay.dart │ ├── apivideo_player_settings_bar.dart │ ├── apivideo_player_time_slider.dart │ ├── apivideo_player_video.dart │ ├── apivideo_player_volume_slider.dart │ └── common │ └── apivideo_player_multi_text_button.dart ├── pubspec.yaml └── test ├── apivideo_player_test.dart └── apivideo_types.dart /.github/workflows/build_android.yml: -------------------------------------------------------------------------------- 1 | name: Build Android 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.dart' 7 | - '**.yaml' 8 | - 'android/**' 9 | 10 | jobs: 11 | build_android: 12 | name: Build Android 13 | uses: apivideo/.github/.github/workflows/flutter_build_android.yml@main 14 | with: 15 | cache: true 16 | -------------------------------------------------------------------------------- /.github/workflows/build_ios.yml: -------------------------------------------------------------------------------- 1 | name: Build iOS 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.dart' 7 | - '**.yaml' 8 | - 'ios/**' 9 | 10 | jobs: 11 | build_ios: 12 | name: Build iOS 13 | uses: apivideo/.github/.github/workflows/flutter_build_ios.yml@main 14 | with: 15 | cache: true 16 | -------------------------------------------------------------------------------- /.github/workflows/build_web.yml: -------------------------------------------------------------------------------- 1 | name: Build web 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.dart' 7 | - '**.yaml' 8 | 9 | jobs: 10 | build_web: 11 | name: Build web 12 | uses: apivideo/.github/.github/workflows/flutter_build_web.yml@main 13 | with: 14 | cache: true 15 | -------------------------------------------------------------------------------- /.github/workflows/create-documentation-pr.yml: -------------------------------------------------------------------------------- 1 | name: Create documentation PR 2 | on: 3 | # Trigger the workflow on pull requests targeting the main branch 4 | pull_request: 5 | types: [assigned, unassigned, opened, reopened, synchronize, edited, labeled, unlabeled, edited, closed] 6 | branches: 7 | - main 8 | 9 | jobs: 10 | create_documentation_pr: 11 | if: github.event.action != 'closed' 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check out current repository code 17 | uses: actions/checkout@v2 18 | 19 | - name: Create the documentation pull request 20 | uses: apivideo/api.video-create-readme-file-pull-request-action@main 21 | with: 22 | source-file-path: "README.md" 23 | destination-repository: apivideo/api.video-documentation 24 | destination-path: sdks/player 25 | destination-filename: apivideo-flutter-player.md 26 | pat: "${{ secrets.PAT }}" 27 | -------------------------------------------------------------------------------- /.github/workflows/create-release-from-changelog.yml: -------------------------------------------------------------------------------- 1 | name: Create draft release from CHANGELOG.md 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'CHANGELOG.md' 7 | 8 | jobs: 9 | update-documentation: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Create draft release if needed 14 | uses: apivideo/api.video-release-from-changelog-action@main 15 | with: 16 | github-auth-token: ${{ secrets.GITHUB_TOKEN }} 17 | prefix: v 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to pub.dev 2 | on: 3 | release: 4 | types: [ published ] 5 | jobs: 6 | build_ios: 7 | name: Build iOS 8 | uses: apivideo/.github/.github/workflows/flutter_build_ios.yml@main 9 | build_android: 10 | name: Build Android 11 | uses: apivideo/.github/.github/workflows/flutter_build_android.yml@main 12 | build_web: 13 | name: Build web 14 | uses: apivideo/.github/.github/workflows/flutter_build_web.yml@main 15 | publish: 16 | name: Publish to pub.dev 17 | needs: [ build_ios, build_android, build_web ] 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Publish 22 | uses: sakebook/actions-flutter-pub-publisher@v1.4.1 23 | with: 24 | credential: ${{ secrets.CREDENTIAL_JSON }} 25 | flutter_package: true 26 | skip_test: false 27 | dry_run: false 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests and analysis 2 | 3 | on: [ push ] 4 | 5 | jobs: 6 | run_tests: 7 | name: Run tests 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Setup Flutter 12 | id: flutter-action 13 | uses: subosito/flutter-action@v2 14 | with: 15 | channel: 'stable' 16 | cache: ${{ inputs.cache }} 17 | cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.yaml') }} 18 | - name: Install dependencies 19 | run: flutter pub get 20 | - name: Run tests 21 | run: flutter test 22 | 23 | analyze_code: 24 | name: Analyze code 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Setup Flutter 29 | id: flutter-action 30 | uses: subosito/flutter-action@v2 31 | with: 32 | channel: 'stable' 33 | cache: ${{ inputs.cache }} 34 | cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.yaml') }} 35 | - name: Install dependencies 36 | run: flutter pub get 37 | - name: Flutter analyze 38 | run: flutter analyze 39 | 40 | calculate_score: 41 | name: Calculate score 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v3 45 | - name: Setup Flutter 46 | id: flutter-action 47 | uses: subosito/flutter-action@v2 48 | with: 49 | channel: 'stable' 50 | cache: ${{ inputs.cache }} 51 | cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.yaml') }} 52 | - name: Install dependencies 53 | run: flutter pub get 54 | - name: Install pana 55 | run: flutter pub global activate pana 56 | - name: Calculate pub points 57 | run: flutter pub pub global run pana --exit-code-threshold 0 . 58 | 59 | test_publishing: 60 | name: Test publishing 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v3 64 | - name: Setup Flutter 65 | id: flutter-action 66 | uses: subosito/flutter-action@v2 67 | with: 68 | channel: 'stable' 69 | cache: ${{ inputs.cache }} 70 | cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.yaml') }} 71 | - name: Install dependencies 72 | run: flutter pub get 73 | - name: Flutter publish dry run 74 | run: flutter pub publish --dry-run 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: eb6d86ee27deecba4a83536aa20f366a6044895c 8 | channel: stable 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c 17 | base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c 18 | - platform: android 19 | create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c 20 | base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c 21 | - platform: ios 22 | create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c 23 | base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c 24 | - platform: web 25 | create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c 26 | base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c 27 | 28 | # User provided section 29 | 30 | # List of Local paths (relative to this file) that should be 31 | # ignored by the migrate tool. 32 | # 33 | # Files that are not part of the templates will be ignored by default. 34 | unmanaged_files: 35 | - 'lib/main.dart' 36 | - 'ios/Runner.xcodeproj/project.pbxproj' 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All changes to this project will be documented in this file. 4 | 5 | ## [1.4.0] - 2024-07-26 6 | 7 | - Use Analytics endpoint v2 8 | - android: gradle: remove imperative apply 9 | - android: fix `setIsMuted` method. See [#56](https://github.com/apivideo/api.video-flutter-player/issues/56) 10 | - android: fix crash due to missing `release` of the `MediaSession`. See [#58](https://github.com/apivideo/api.video-flutter-player/issues/58) 11 | - example: infer video type from mediaId 12 | - upgrade dependencies 13 | 14 | ## [1.3.0] - 2024-03-01 15 | 16 | - iOS: add support for private live stream 17 | - web: format file to fix pub points 18 | 19 | ## [1.2.2] - 2024-02-15 20 | 21 | - Android: upgrade to gradle 8, AGP and Kotlin to 1.9 22 | - Fix few warnings 23 | 24 | ## [1.2.1] - 2023-12-05 25 | 26 | - Add an API to set the duration when the overlay is displayed 27 | - Web: Inject the player sdk bundle to simplify web integration 28 | - Web: Fix a crash when the player is created 29 | - Fix a crash when the player is disposed due to double dispose 30 | - Improve comments 31 | - Privatize some methods 32 | 33 | ## [1.2.0] - 2023-10-11 34 | 35 | - Add support for live stream videos 36 | - Add support for Android >= 21 37 | - Add support for Android 34 38 | - Add a `fit` parameter to `ApiVideoPlayer` to set how the video is displayed in its box 39 | - Improve the customization of `ApiVideoPlayer` with `PlayerStyle` 40 | - Refactor widgets to split into several widgets 41 | 42 | ## [1.1.0] - 2023-07-26 43 | 44 | - Add support for private videos 45 | - Add support for playback speed 46 | - iOS: add support from iOS 11 47 | - Web: close player events when player is disposed 48 | - Web: remove CSS border on player 49 | - Web: fix player sdk event on release and profile mode 50 | - Web: fix `getVideoSize` API that caused a bad aspect ratio with border 51 | - Android: fix the duration of the video when the video is not loaded 52 | - Android: fix crash when the current time < 0 53 | - Android: fix a crash due to obfuscation ( 54 | see [#43](https://github.com/apivideo/api.video-flutter-player/issues/43)) 55 | 56 | ## [1.0.0] - 2022-10-10 57 | 58 | - First version 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 api.video 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .cxx 10 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins; 2 | 3 | import androidx.annotation.Keep; 4 | import androidx.annotation.NonNull; 5 | import io.flutter.Log; 6 | 7 | import io.flutter.embedding.engine.FlutterEngine; 8 | 9 | /** 10 | * Generated file. Do not edit. 11 | * This file is generated by the Flutter tool based on the 12 | * plugins that support the Android platform. 13 | */ 14 | @Keep 15 | public final class GeneratedPluginRegistrant { 16 | private static final String TAG = "GeneratedPluginRegistrant"; 17 | public static void registerWith(@NonNull FlutterEngine flutterEngine) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'video.api.flutter.player' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.9.22' 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:8.2.2' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | 27 | android { 28 | compileSdk 34 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | minSdkVersion 21 45 | } 46 | 47 | namespace = "video.api.flutter.player" 48 | } 49 | 50 | dependencies { 51 | implementation 'video.api:android-player:1.6.0' 52 | } 53 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'apivideo_player' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /android/src/main/kotlin/video/api/flutter/player/ApiVideoPlayerPlugin.kt: -------------------------------------------------------------------------------- 1 | package video.api.flutter.player 2 | 3 | import android.content.Context 4 | import io.flutter.embedding.engine.plugins.FlutterPlugin 5 | import io.flutter.plugin.common.BinaryMessenger 6 | import io.flutter.view.TextureRegistry 7 | 8 | /** ApiVideoPlayerPlugin */ 9 | class ApiVideoPlayerPlugin : FlutterPlugin { 10 | private lateinit var textureRegistry: TextureRegistry 11 | private lateinit var messenger: BinaryMessenger 12 | private lateinit var applicationContext: Context 13 | private lateinit var api: MethodCallHandler 14 | 15 | override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 16 | textureRegistry = flutterPluginBinding.textureRegistry 17 | messenger = flutterPluginBinding.binaryMessenger 18 | applicationContext = flutterPluginBinding.applicationContext 19 | api = MethodCallHandler( 20 | messenger, 21 | FlutterPlayerController(textureRegistry, messenger, applicationContext) 22 | ) 23 | } 24 | 25 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { 26 | api.dispose() 27 | } 28 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/video/api/flutter/player/Extensions.kt: -------------------------------------------------------------------------------- 1 | package video.api.flutter.player 2 | 3 | import video.api.player.models.VideoOptions 4 | import video.api.player.models.VideoType 5 | import java.security.InvalidParameterException 6 | 7 | val Map.videoOptions: VideoOptions 8 | get() = VideoOptions( 9 | this["videoId"] as String, 10 | (this["type"] as String).toVideoType(), 11 | this["token"] as String? 12 | ) 13 | 14 | fun String.toVideoType(): VideoType { 15 | return if (this == "vod") { 16 | VideoType.VOD 17 | } else if (this == "live") { 18 | VideoType.LIVE 19 | } else { 20 | throw InvalidParameterException("$this is an unknown video type") 21 | } 22 | } 23 | 24 | fun VideoType.toFlutterString(): String { 25 | return when (this) { 26 | VideoType.VOD -> "vod" 27 | VideoType.LIVE -> "live" 28 | else -> throw InvalidParameterException("$this is an unknown video type") 29 | } 30 | } 31 | 32 | fun Int.msToFloat(): Float { 33 | return this.toFloat() / 1000 34 | } 35 | 36 | fun Float.toMs(): Int { 37 | return (this * 1000).toInt() 38 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/video/api/flutter/player/FlutterPlayerController.kt: -------------------------------------------------------------------------------- 1 | package video.api.flutter.player 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import android.util.Size 6 | import io.flutter.plugin.common.BinaryMessenger 7 | import io.flutter.view.TextureRegistry 8 | import video.api.player.models.VideoOptions 9 | 10 | 11 | class FlutterPlayerController( 12 | private val textureRegistry: TextureRegistry, 13 | private val messenger: BinaryMessenger, 14 | private val applicationContext: Context 15 | ) : FlutterPlayerInterface { 16 | private val players = mutableMapOf() 17 | 18 | override fun isCreated(textureId: Long): Boolean { 19 | return players.containsKey(textureId) 20 | } 21 | 22 | override fun initialize(autoplay: Boolean): Long { 23 | val player = FlutterPlayerView( 24 | applicationContext, 25 | messenger, 26 | textureRegistry, 27 | null, 28 | autoplay 29 | ) 30 | players[player.textureId] = player 31 | 32 | return player.textureId 33 | } 34 | 35 | override fun dispose(textureId: Long) { 36 | players[textureId]?.release() ?: Log.e(TAG, "Unknown player $textureId") 37 | players.remove(textureId) 38 | } 39 | 40 | override fun disposeAll() { 41 | players.values.forEach { it.release() } 42 | players.clear() 43 | } 44 | 45 | override fun isPlaying(textureId: Long): Boolean { 46 | return players[textureId]?.isPlaying ?: run { 47 | Log.e(TAG, "Unknown player $textureId") 48 | false 49 | } 50 | } 51 | 52 | override fun isLive(textureId: Long): Boolean { 53 | return players[textureId]?.isLive ?: run { 54 | Log.e(TAG, "Unknown player $textureId") 55 | false 56 | } 57 | } 58 | 59 | override fun setCurrentTime(textureId: Long, currentTime: Int) { 60 | players[textureId]?.let { it.currentTime = currentTime.msToFloat() } ?: Log.e( 61 | TAG, 62 | "Unknown player $textureId" 63 | ) 64 | } 65 | 66 | override fun getCurrentTime(textureId: Long): Int { 67 | return players[textureId]?.currentTime?.toMs() ?: run { 68 | Log.e(TAG, "Unknown player $textureId") 69 | 0 70 | } 71 | } 72 | 73 | override fun getDuration(textureId: Long): Int { 74 | return players[textureId]?.duration?.toMs() ?: run { 75 | Log.e(TAG, "Unknown player $textureId") 76 | 0 77 | } 78 | } 79 | 80 | override fun getVideoOptions(textureId: Long): VideoOptions? { 81 | return players[textureId]?.videoOptions ?: run { 82 | Log.e(TAG, "Unknown player $textureId") 83 | null 84 | } 85 | } 86 | 87 | override fun setVideoOptions(textureId: Long, videoOptions: VideoOptions) { 88 | players[textureId]?.let { it.videoOptions = videoOptions } ?: Log.e( 89 | TAG, 90 | "Unknown player $textureId" 91 | ) 92 | } 93 | 94 | override fun getAutoplay(textureId: Long): Boolean { 95 | return players[textureId]?.isAutoplay ?: run { 96 | Log.e(TAG, "Unknown player $textureId") 97 | false 98 | } 99 | } 100 | 101 | override fun setAutoplay(textureId: Long, autoplay: Boolean) { 102 | players[textureId]?.let { it.isAutoplay = autoplay } ?: Log.e( 103 | TAG, 104 | "Unknown player $textureId" 105 | ) 106 | } 107 | 108 | override fun getIsMuted(textureId: Long): Boolean { 109 | return players[textureId]?.isMuted ?: run { 110 | Log.e(TAG, "Unknown player $textureId") 111 | false 112 | } 113 | } 114 | 115 | override fun setIsMuted(textureId: Long, isMuted: Boolean) { 116 | players[textureId]?.let { it.isMuted = isMuted } ?: Log.e( 117 | TAG, 118 | "Unknown player $textureId" 119 | ) 120 | } 121 | 122 | override fun getIsLooping(textureId: Long): Boolean { 123 | return players[textureId]?.isLooping ?: run { 124 | Log.e(TAG, "Unknown player $textureId") 125 | false 126 | } 127 | } 128 | 129 | override fun setIsLooping(textureId: Long, isLooping: Boolean) { 130 | players[textureId]?.let { it.isLooping = isLooping } ?: Log.e( 131 | TAG, 132 | "Unknown player $textureId" 133 | ) 134 | } 135 | 136 | override fun getVolume(textureId: Long): Float { 137 | return players[textureId]?.volume ?: run { 138 | Log.e(TAG, "Unknown player $textureId") 139 | 0.0F 140 | } 141 | } 142 | 143 | override fun setVolume(textureId: Long, volume: Float) { 144 | players[textureId]?.let { it.volume = volume } ?: Log.e( 145 | TAG, 146 | "Unknown player $textureId" 147 | ) 148 | } 149 | 150 | override fun getVideoSize(textureId: Long): Size? { 151 | return players[textureId]?.videoSize ?: run { 152 | Log.e(TAG, "Unknown player $textureId") 153 | null 154 | } 155 | } 156 | 157 | override fun getPlaybackSpeed(textureId: Long): Double { 158 | return players[textureId]?.playbackSpeed?.toDouble() ?: run { 159 | Log.e(TAG, "Unknown player $textureId") 160 | 0.0 161 | } 162 | } 163 | 164 | override fun setPlaybackSpeed(textureId: Long, playbackSpeed: Double) { 165 | players[textureId]?.let { it.playbackSpeed = playbackSpeed.toFloat() } ?: Log.e( 166 | TAG, 167 | "Unknown player $textureId" 168 | ) 169 | } 170 | 171 | override fun play(textureId: Long) { 172 | players[textureId]?.play() ?: Log.e(TAG, "Unknown player $textureId") 173 | } 174 | 175 | override fun pause(textureId: Long) { 176 | players[textureId]?.pause() ?: Log.e(TAG, "Unknown player $textureId") 177 | } 178 | 179 | override fun seek(textureId: Long, offset: Int) { 180 | players[textureId]?.seek(offset.msToFloat()) ?: Log.e(TAG, "Unknown player $textureId") 181 | } 182 | 183 | companion object { 184 | private const val TAG = "FlutterPlayerController" 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /android/src/main/kotlin/video/api/flutter/player/FlutterPlayerInterface.kt: -------------------------------------------------------------------------------- 1 | package video.api.flutter.player 2 | 3 | import android.util.Size 4 | import video.api.player.models.VideoOptions 5 | 6 | interface FlutterPlayerInterface { 7 | fun isCreated(textureId: Long): Boolean 8 | fun initialize(autoplay: Boolean): Long 9 | fun dispose(textureId: Long) 10 | fun disposeAll() 11 | 12 | fun isPlaying(textureId: Long): Boolean 13 | fun isLive(textureId: Long): Boolean 14 | fun getCurrentTime(textureId: Long): Int 15 | fun setCurrentTime(textureId: Long, currentTime: Int) 16 | fun getDuration(textureId: Long): Int 17 | fun getVideoOptions(textureId: Long): VideoOptions? 18 | fun setVideoOptions(textureId: Long, videoOptions: VideoOptions) 19 | fun getAutoplay(textureId: Long): Boolean 20 | fun setAutoplay(textureId: Long, autoplay: Boolean) 21 | fun getIsMuted(textureId: Long): Boolean 22 | fun setIsMuted(textureId: Long, isMuted: Boolean) 23 | fun getIsLooping(textureId: Long): Boolean 24 | fun setIsLooping(textureId: Long, isLooping: Boolean) 25 | fun getVolume(textureId: Long): Float 26 | fun setVolume(textureId: Long, volume: Float) 27 | fun getVideoSize(textureId: Long): Size? 28 | fun getPlaybackSpeed(textureId: Long): Double 29 | fun setPlaybackSpeed(textureId: Long, playbackSpeed: Double) 30 | 31 | fun play(textureId: Long) 32 | fun pause(textureId: Long) 33 | fun seek(textureId: Long, offset: Int) 34 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/video/api/flutter/player/FlutterPlayerView.kt: -------------------------------------------------------------------------------- 1 | package video.api.flutter.player 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import android.util.Size 6 | import android.view.Surface 7 | import io.flutter.plugin.common.BinaryMessenger 8 | import io.flutter.plugin.common.EventChannel 9 | import io.flutter.plugin.common.EventChannel.EventSink 10 | import io.flutter.view.TextureRegistry 11 | import video.api.player.ApiVideoPlayerController 12 | import video.api.player.models.VideoOptions 13 | import video.api.player.models.VideoType 14 | 15 | class FlutterPlayerView( 16 | context: Context, 17 | messenger: BinaryMessenger, 18 | textureRegistry: TextureRegistry, 19 | initialVideoOptions: VideoOptions? = null, 20 | autoplay: Boolean = false 21 | ) { 22 | private val surfaceTextureEntry = textureRegistry.createSurfaceTexture() 23 | val textureId = surfaceTextureEntry.id() 24 | private val surface = Surface(surfaceTextureEntry.surfaceTexture()) 25 | private val listener = object : ApiVideoPlayerController.Listener { 26 | override fun onReady() { 27 | val event = mutableMapOf() 28 | event["type"] = "ready" 29 | eventSink?.success(event) 30 | } 31 | 32 | override fun onPlay() { 33 | val event = mutableMapOf() 34 | event["type"] = "played" 35 | eventSink?.success(event) 36 | } 37 | 38 | override fun onPause() { 39 | val event = mutableMapOf() 40 | event["type"] = "paused" 41 | eventSink?.success(event) 42 | } 43 | 44 | override fun onEnd() { 45 | val event = mutableMapOf() 46 | event["type"] = "ended" 47 | eventSink?.success(event) 48 | } 49 | 50 | override fun onSeek() { 51 | val event = mutableMapOf() 52 | event["type"] = "seek" 53 | eventSink?.success(event) 54 | } 55 | 56 | override fun onError(error: Exception) { 57 | Log.e(TAG, "An error occurred: ${error.message}", error) 58 | eventSink?.error(error::class.java.name, error.message, error) 59 | } 60 | } 61 | 62 | private val playerController = initialVideoOptions?.let { 63 | ApiVideoPlayerController( 64 | context, 65 | it, 66 | listener = listener, 67 | surface = surface, 68 | initialAutoplay = autoplay 69 | ) 70 | } ?: ApiVideoPlayerController( 71 | context, 72 | null, 73 | listener = listener, 74 | surface = surface, 75 | initialAutoplay = autoplay 76 | ) 77 | private var eventSink: EventSink? = null 78 | private val eventChannel = EventChannel(messenger, "video.api.player/events$textureId") 79 | 80 | init { 81 | eventChannel.setStreamHandler(object : EventChannel.StreamHandler { 82 | override fun onListen(arguments: Any?, events: EventSink?) { 83 | eventSink = events 84 | } 85 | 86 | override fun onCancel(arguments: Any?) { 87 | eventSink?.endOfStream() 88 | eventSink = null 89 | } 90 | }) 91 | } 92 | 93 | var videoOptions: VideoOptions? 94 | get() = playerController.videoOptions 95 | set(value) { 96 | playerController.videoOptions = value 97 | } 98 | 99 | var isAutoplay: Boolean 100 | get() = playerController.autoplay 101 | set(value) { 102 | playerController.autoplay = value 103 | } 104 | 105 | var isMuted: Boolean 106 | get() = playerController.isMuted 107 | set(value) { 108 | playerController.isMuted = value 109 | } 110 | 111 | var isLooping: Boolean 112 | get() = playerController.isLooping 113 | set(value) { 114 | playerController.isLooping = value 115 | } 116 | 117 | var volume: Float 118 | get() = playerController.volume 119 | set(value) { 120 | playerController.volume = value 121 | } 122 | 123 | val isPlaying: Boolean 124 | get() = playerController.isPlaying 125 | 126 | val isLive: Boolean 127 | get() = playerController.isLive 128 | 129 | var currentTime: Float 130 | get() = playerController.currentTime 131 | set(value) { 132 | eventSink?.success(mapOf("type" to "seekStarted")) 133 | playerController.currentTime = value 134 | } 135 | 136 | val duration: Float 137 | get() = playerController.duration 138 | 139 | val videoSize: Size? 140 | get() = playerController.videoSize 141 | 142 | var playbackSpeed: Float 143 | get() = playerController.playbackSpeed 144 | set(value) { 145 | playerController.playbackSpeed = value 146 | } 147 | 148 | fun play() = playerController.play() 149 | fun pause() = playerController.pause() 150 | 151 | fun seek(offset: Float) { 152 | eventSink?.success(mapOf("type" to "seekStarted")) 153 | playerController.seek(offset) 154 | } 155 | 156 | fun release() { 157 | eventChannel.setStreamHandler(null) 158 | playerController.stop() 159 | surfaceTextureEntry.release() 160 | surface.release() 161 | playerController.release() 162 | } 163 | 164 | companion object { 165 | private const val TAG = "FlutterPlayerView" 166 | } 167 | } -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | .vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | /pubspec.lock 35 | 36 | # Web related 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # apivideo_player_example 2 | 3 | Demonstrates how to use the apivideo_player plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | compileSdk 34 27 | ndkVersion flutter.ndkVersion 28 | 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | 34 | kotlinOptions { 35 | jvmTarget = '1.8' 36 | } 37 | 38 | sourceSets { 39 | main.java.srcDirs += 'src/main/kotlin' 40 | } 41 | 42 | defaultConfig { 43 | applicationId "video.api.flutter.player.example" 44 | // You can update the following values to match your application needs. 45 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 46 | minSdkVersion 21 47 | targetSdkVersion flutter.targetSdkVersion 48 | versionCode flutterVersionCode.toInteger() 49 | versionName flutterVersionName 50 | } 51 | 52 | buildTypes { 53 | release { 54 | // TODO: Add your own signing config for the release build. 55 | // Signing with the debug keys for now, so `flutter run --release` works. 56 | minifyEnabled true 57 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | namespace 'video.api.flutter.player.example' 62 | } 63 | 64 | flutter { 65 | source '../..' 66 | } 67 | 68 | dependencies { 69 | } 70 | -------------------------------------------------------------------------------- /example/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -dontwarn org.slf4j.impl.StaticLoggerBinder -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/video/api/apivideo_player_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package video.api.flutter.player.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/ic_api_video.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.nonTransitiveRClass=false 5 | android.nonFinalResIds=false 6 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.2.2" apply false 22 | id "org.jetbrains.kotlin.android" version "1.9.22" apply false 23 | } 24 | 25 | include ":app" -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 14.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - apivideo_player (0.0.1): 3 | - ApiVideoPlayer (= 1.3.0) 4 | - Flutter 5 | - ApiVideoPlayer (1.3.0): 6 | - ApiVideoPlayerAnalytics (= 2.0.0) 7 | - ApiVideoPlayerAnalytics (2.0.0) 8 | - Flutter (1.0.0) 9 | - pointer_interceptor_ios (0.0.1): 10 | - Flutter 11 | 12 | DEPENDENCIES: 13 | - apivideo_player (from `.symlinks/plugins/apivideo_player/ios`) 14 | - Flutter (from `Flutter`) 15 | - pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`) 16 | 17 | SPEC REPOS: 18 | trunk: 19 | - ApiVideoPlayer 20 | - ApiVideoPlayerAnalytics 21 | 22 | EXTERNAL SOURCES: 23 | apivideo_player: 24 | :path: ".symlinks/plugins/apivideo_player/ios" 25 | Flutter: 26 | :path: Flutter 27 | pointer_interceptor_ios: 28 | :path: ".symlinks/plugins/pointer_interceptor_ios/ios" 29 | 30 | SPEC CHECKSUMS: 31 | apivideo_player: ebd05d204d7f8bf03aa8a01f603d7b869a9dca60 32 | ApiVideoPlayer: f29a6b54b3eb5904c1bd123d5fc2548d1699e7dc 33 | ApiVideoPlayerAnalytics: df5fe80fb7dbb333efd9b47e8797dc24182ee412 34 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 35 | pointer_interceptor_ios: 9280618c0b2eeb80081a343924aa8ad756c21375 36 | 37 | PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048 38 | 39 | COCOAPODS: 1.15.2 40 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/120 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/120 1.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/120 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/120 2.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/40 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/40 1.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/40 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/40 2.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/58 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/58 1.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/80 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/80 1.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "58 1.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "80 1.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "120 2.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120 1.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "40 1.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "58.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "40 2.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "80.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "152.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "167.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | FltPlayer 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | FltPlayer 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:apivideo_player/apivideo_player.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void main() { 5 | runApp(const MyApp()); 6 | } 7 | 8 | class MyApp extends StatefulWidget { 9 | const MyApp({super.key}); 10 | 11 | @override 12 | State createState() => _MyAppState(); 13 | } 14 | 15 | class _MyAppState extends State { 16 | final TextEditingController _videoIdTextEditingController = 17 | TextEditingController(text: 'vi77Dgk0F8eLwaFOtC5870yn'); 18 | ApiVideoPlayerController? _controller; 19 | final TextEditingController _tokenTextEditingController = 20 | TextEditingController(text: ''); 21 | 22 | @override 23 | void initState() { 24 | buildVideoOptions(); 25 | super.initState(); 26 | } 27 | 28 | void buildVideoOptions() { 29 | final token = _tokenTextEditingController.text.isEmpty 30 | ? null 31 | : _tokenTextEditingController.text; 32 | 33 | final videoOptions = VideoOptions( 34 | videoId: _videoIdTextEditingController.text, 35 | token: token); 36 | 37 | if (_controller == null) { 38 | _controller = 39 | ApiVideoPlayerController(videoOptions: videoOptions, autoplay: true); 40 | } else { 41 | _controller?.setVideoOptions(videoOptions); 42 | } 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return MaterialApp( 48 | theme: ThemeData( 49 | textButtonTheme: TextButtonThemeData( 50 | style: TextButton.styleFrom( 51 | foregroundColor: const Color(0xFFFA5B30), 52 | ), 53 | ), 54 | ), 55 | home: Builder(builder: (context) { 56 | return Scaffold( 57 | body: SafeArea( 58 | child: SingleChildScrollView( 59 | child: Column( 60 | children: [ 61 | Padding( 62 | padding: const EdgeInsets.all(30.0), 63 | child: TextField( 64 | decoration: const InputDecoration( 65 | border: OutlineInputBorder(), 66 | labelText: 'Enter a video id', 67 | ), 68 | controller: _videoIdTextEditingController, 69 | onSubmitted: (value) async { 70 | buildVideoOptions(); 71 | }, 72 | ), 73 | ), 74 | Padding( 75 | padding: const EdgeInsets.all(30.0), 76 | child: TextField( 77 | decoration: const InputDecoration( 78 | border: OutlineInputBorder(), 79 | labelText: 80 | 'Enter a token (leave empty if the video is public)', 81 | ), 82 | controller: _tokenTextEditingController, 83 | onSubmitted: (value) async { 84 | buildVideoOptions(); 85 | }, 86 | ), 87 | ), 88 | _controller != null 89 | ? PlayerWidget(controller: _controller!) 90 | : const SizedBox.shrink(), 91 | ], 92 | ), 93 | ), 94 | ), 95 | ); 96 | }), 97 | ); 98 | } 99 | } 100 | 101 | class PlayerWidget extends StatefulWidget { 102 | const PlayerWidget({ 103 | super.key, 104 | required this.controller, 105 | }); 106 | 107 | final ApiVideoPlayerController controller; 108 | 109 | @override 110 | State createState() => _PlayerWidgetState(); 111 | } 112 | 113 | class _PlayerWidgetState extends State { 114 | String _currentTime = 'Get current time'; 115 | String _duration = 'Get duration'; 116 | bool _hideControls = false; 117 | 118 | @override 119 | void initState() { 120 | super.initState(); 121 | widget.controller.initialize(); 122 | widget.controller.addListener(ApiVideoPlayerControllerEventsListener( 123 | onReady: () { 124 | setState(() { 125 | _duration = 'Get duration'; 126 | }); 127 | }, 128 | )); 129 | } 130 | 131 | @override 132 | void dispose() { 133 | widget.controller.dispose(); 134 | super.dispose(); 135 | } 136 | 137 | void _toggleLooping() { 138 | widget.controller.isLooping.then( 139 | (bool isLooping) { 140 | widget.controller.setIsLooping(!isLooping); 141 | ScaffoldMessenger.of(context).showSnackBar( 142 | SnackBar( 143 | content: Text( 144 | 'Your video is ${isLooping ? 'not on loop anymore' : 'on loop'}.', 145 | ), 146 | backgroundColor: Colors.blueAccent, 147 | ), 148 | ); 149 | }, 150 | ); 151 | } 152 | 153 | @override 154 | Widget build(BuildContext context) { 155 | return Column( 156 | children: [ 157 | SizedBox( 158 | width: 300.0, 159 | height: 300.0, 160 | child: _hideControls 161 | ? ApiVideoPlayer.noControls(controller: widget.controller) 162 | : ApiVideoPlayer( 163 | controller: widget.controller, 164 | style: PlayerStyle.defaultStyle), 165 | ), 166 | Row(mainAxisAlignment: MainAxisAlignment.center, children: [ 167 | IconButton( 168 | icon: const Icon(Icons.replay_10), 169 | onPressed: () { 170 | widget.controller.seek(const Duration(seconds: -10)); 171 | }, 172 | ), 173 | IconButton( 174 | icon: const Icon(Icons.play_arrow), 175 | onPressed: () { 176 | widget.controller.play(); 177 | }, 178 | ), 179 | IconButton( 180 | icon: const Icon(Icons.pause), 181 | onPressed: () { 182 | widget.controller.pause(); 183 | }, 184 | ), 185 | IconButton( 186 | icon: const Icon(Icons.forward_10), 187 | onPressed: () { 188 | widget.controller.seek(const Duration(seconds: 10)); 189 | }, 190 | ), 191 | ]), 192 | Row( 193 | mainAxisAlignment: MainAxisAlignment.center, 194 | children: [ 195 | IconButton( 196 | icon: const Icon(Icons.volume_off), 197 | onPressed: () { 198 | widget.controller.setIsMuted(true); 199 | }, 200 | ), 201 | IconButton( 202 | icon: const Icon(Icons.volume_up), 203 | onPressed: () { 204 | widget.controller.setIsMuted(false); 205 | }, 206 | ), 207 | IconButton( 208 | icon: const Icon(Icons.loop), 209 | onPressed: () => _toggleLooping(), 210 | ), 211 | ], 212 | ), 213 | TextButton( 214 | child: Text( 215 | _duration, 216 | textAlign: TextAlign.center, 217 | ), 218 | onPressed: () async { 219 | final Duration duration = await widget.controller.duration; 220 | setState(() { 221 | _duration = 'Duration: $duration'; 222 | }); 223 | }, 224 | ), 225 | TextButton( 226 | child: Text(_currentTime), 227 | onPressed: () async { 228 | final Duration currentTime = await widget.controller.currentTime; 229 | setState(() { 230 | _currentTime = 'Get current time: $currentTime'; 231 | }); 232 | }, 233 | ), 234 | TextButton( 235 | child: Text('${_hideControls ? 'Show' : 'Hide'} controls'), 236 | onPressed: () => setState(() { 237 | _hideControls = !_hideControls; 238 | }), 239 | ), 240 | ], 241 | ); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: apivideo_player_example 2 | description: Demonstrates how to use the apivideo_player plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: ">=2.17.1 <3.0.0" 10 | 11 | # Dependencies specify other packages that your package needs in order to work. 12 | # To automatically upgrade your package dependencies to the latest versions 13 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 14 | # dependencies can be manually updated by changing the version numbers below to 15 | # the latest version available on pub.dev. To see which dependencies have newer 16 | # versions available, run `flutter pub outdated`. 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | apivideo_player: 21 | # When depending on this package from a real application you should use: 22 | # apivideo_player: ^x.y.z 23 | # See https://dart.dev/tools/pub/dependencies#version-constraints 24 | # The example app is bundled with the plugin so we use a path dependency on 25 | # the parent directory to use the current plugin's version. 26 | path: ../ 27 | 28 | # The following adds the Cupertino Icons font to your application. 29 | # Use with the CupertinoIcons class for iOS style icons. 30 | cupertino_icons: ^1.0.2 31 | 32 | dev_dependencies: 33 | flutter_test: 34 | sdk: flutter 35 | 36 | # The "flutter_lints" package below contains a set of recommended lints to 37 | # encourage good coding practices. The lint set provided by the package is 38 | # activated in the `analysis_options.yaml` file located at the root of your 39 | # package. See that file for information about deactivating specific lint 40 | # rules and activating additional ones. 41 | flutter_lints: ^4.0.0 42 | 43 | # For information on the generic Dart part of this file, see the 44 | # following page: https://dart.dev/tools/pub/pubspec 45 | 46 | # The following section is specific to Flutter packages. 47 | flutter: 48 | 49 | # The following line ensures that the Material Icons font is 50 | # included with your application, so that you can use the icons in 51 | # the material Icons class. 52 | uses-material-design: true 53 | 54 | # To add assets to your application, add an assets section, like this: 55 | # assets: 56 | # - images/a_dot_burr.jpeg 57 | # - images/a_dot_ham.jpeg 58 | 59 | # An image asset can refer to one or more resolution-specific "variants", see 60 | # https://flutter.dev/assets-and-images/#resolution-aware 61 | 62 | # For details regarding adding assets from package dependencies, see 63 | # https://flutter.dev/assets-and-images/#from-packages 64 | 65 | # To add custom fonts to your application, add a fonts section here, 66 | # in this "flutter" section. Each entry in this list should have a 67 | # "family" key with the font family name, and a "fonts" key with a 68 | # list giving the asset and other descriptors for the font. For 69 | # example: 70 | # fonts: 71 | # - family: Schyler 72 | # fonts: 73 | # - asset: fonts/Schyler-Regular.ttf 74 | # - asset: fonts/Schyler-Italic.ttf 75 | # style: italic 76 | # - family: Trajan Pro 77 | # fonts: 78 | # - asset: fonts/TrajanPro.ttf 79 | # - asset: fonts/TrajanPro_Bold.ttf 80 | # weight: 700 81 | # 82 | # For details regarding fonts from package dependencies, 83 | # see https://flutter.dev/custom-fonts/#from-packages 84 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:apivideo_player_example/main.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | 12 | void main() { 13 | testWidgets('Verify Platform version', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(const MyApp()); 16 | 17 | // Verify that platform version is retrieved. 18 | expect( 19 | find.byWidgetPredicate( 20 | (Widget widget) => 21 | widget is Text && widget.data!.startsWith('Running on:'), 22 | ), 23 | findsOneWidget, 24 | ); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/example/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | FltPlayer 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FltPlayer", 3 | "short_name": "FltPlayer", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "Demonstrates how to use the apivideo_player plugin.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/ephemeral/ 38 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/ApiVideoPlayerPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface ApiVideoPlayerPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /ios/Classes/ApiVideoPlayerPlugin.m: -------------------------------------------------------------------------------- 1 | #import "ApiVideoPlayerPlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "apivideo_player-Swift.h" 9 | #endif 10 | 11 | @implementation ApiVideoPlayerPlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftApiVideoPlayerPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Classes/FlutterPlayerController.swift: -------------------------------------------------------------------------------- 1 | import ApiVideoPlayer 2 | import Foundation 3 | 4 | // Manages a list of multiple players 5 | class FlutterPlayerController { 6 | private let binaryMessenger: FlutterBinaryMessenger 7 | private let textureRegistry: FlutterTextureRegistry 8 | private var players: [Int64: FlutterPlayerView] = [:] 9 | 10 | init(binaryMessenger: FlutterBinaryMessenger, textureRegistry: FlutterTextureRegistry) { 11 | self.binaryMessenger = binaryMessenger 12 | self.textureRegistry = textureRegistry 13 | } 14 | 15 | func isCreated(textureId: Int64) -> Bool { 16 | return players[textureId] != nil 17 | } 18 | 19 | func initialize(autoplay: Bool) -> Int64 { 20 | let player = FlutterPlayerView(binaryMessenger: binaryMessenger, textureRegistry: textureRegistry, autoplay: autoplay) 21 | 22 | players[player.textureId] = player 23 | 24 | return player.textureId 25 | } 26 | 27 | func dispose(textureId: Int64) { 28 | guard let player = players[textureId] else { 29 | print("Unknown player \(textureId)") 30 | return 31 | } 32 | player.dispose() 33 | players.removeValue(forKey: textureId) 34 | } 35 | 36 | func disposeAll() { 37 | for player in players.values { 38 | player.dispose() 39 | } 40 | players.removeAll() 41 | } 42 | 43 | func isPlaying(textureId: Int64) -> Bool { 44 | guard let player = players[textureId] else { 45 | print("Unknown player \(textureId)") 46 | return false 47 | } 48 | return player.isPlaying 49 | } 50 | 51 | func isLive(textureId: Int64) -> Bool { 52 | guard let player = players[textureId] else { 53 | print("Unknown player \(textureId)") 54 | return false 55 | } 56 | return player.isLive 57 | } 58 | 59 | func getCurrentTime(textureId: Int64) -> Int { 60 | guard let player = players[textureId] else { 61 | print("Unknown player \(textureId)") 62 | return 0 63 | } 64 | return player.currentTime.toMs() 65 | } 66 | 67 | func setCurrentTime(textureId: Int64, currentTime: Int) { 68 | guard let player = players[textureId] else { 69 | print("Unknown player \(textureId)") 70 | return 71 | } 72 | player.currentTime = currentTime.msToCMTime() 73 | } 74 | 75 | func getDuration(textureId: Int64) -> Int { 76 | guard let player = players[textureId] else { 77 | print("Unknown player \(textureId)") 78 | return 0 79 | } 80 | return player.duration.toMs() 81 | } 82 | 83 | func getVideoOptions(textureId: Int64) -> VideoOptions? { 84 | guard let player = players[textureId] else { 85 | print("Unknown player \(textureId)") 86 | return nil 87 | } 88 | return player.videoOptions 89 | } 90 | 91 | func setVideoOptions(textureId: Int64, videoOptions: VideoOptions) { 92 | guard let player = players[textureId] else { 93 | print("Unknown player \(textureId)") 94 | return 95 | } 96 | player.videoOptions = videoOptions 97 | } 98 | 99 | func getAutoplay(textureId: Int64) -> Bool { 100 | guard let player = players[textureId] else { 101 | print("Unknown player \(textureId)") 102 | return false 103 | } 104 | return player.autoplay 105 | } 106 | 107 | func setAutoplay(textureId: Int64, autoplay: Bool) { 108 | guard let player = players[textureId] else { 109 | print("Unknown player \(textureId)") 110 | return 111 | } 112 | player.autoplay = autoplay 113 | } 114 | 115 | func getIsMuted(textureId: Int64) -> Bool { 116 | guard let player = players[textureId] else { 117 | print("Unknown player \(textureId)") 118 | return false 119 | } 120 | return player.isMuted 121 | } 122 | 123 | func setIsMuted(textureId: Int64, isMuted: Bool) { 124 | guard let player = players[textureId] else { 125 | print("Unknown player \(textureId)") 126 | return 127 | } 128 | player.isMuted = isMuted 129 | } 130 | 131 | func getIsLooping(textureId: Int64) -> Bool { 132 | guard let player = players[textureId] else { 133 | print("Unknown player \(textureId)") 134 | return false 135 | } 136 | return player.isLooping 137 | } 138 | 139 | func setIsLooping(textureId: Int64, isLooping: Bool) { 140 | guard let player = players[textureId] else { 141 | print("Unknown player \(textureId)") 142 | return 143 | } 144 | player.isLooping = isLooping 145 | } 146 | 147 | func getVolume(textureId: Int64) -> Float { 148 | guard let player = players[textureId] else { 149 | print("Unknown player \(textureId)") 150 | return 0 151 | } 152 | return player.volume 153 | } 154 | 155 | func setVolume(textureId: Int64, volume: Float) { 156 | guard let player = players[textureId] else { 157 | print("Unknown player \(textureId)") 158 | return 159 | } 160 | player.volume = volume 161 | } 162 | 163 | func getVideoSize(textureId: Int64) -> CGSize? { 164 | guard let player = players[textureId] else { 165 | print("Unknown player \(textureId)") 166 | return nil 167 | } 168 | return player.videoSize 169 | } 170 | 171 | func play(textureId: Int64) { 172 | guard let player = players[textureId] else { 173 | print("Unknown player \(textureId)") 174 | return 175 | } 176 | player.play() 177 | } 178 | 179 | func pause(textureId: Int64) { 180 | guard let player = players[textureId] else { 181 | print("Unknown player \(textureId)") 182 | return 183 | } 184 | player.pause() 185 | } 186 | 187 | func seek(textureId: Int64, offset: Int) { 188 | guard let player = players[textureId] else { 189 | print("Unknown player \(textureId)") 190 | return 191 | } 192 | player.seek(offset: offset.msToCMTime()) 193 | } 194 | 195 | func setPlaybackSpeed(textureId: Int64, speedRate: Double) { 196 | guard let player = players[textureId] else { 197 | print("Unknown player \(textureId)") 198 | return 199 | } 200 | player.speedRate = Float(speedRate) 201 | } 202 | 203 | func getPlaybackSpeed(textureId: Int64) -> Double { 204 | guard let player = players[textureId] else { 205 | print("Unknown player \(textureId)") 206 | return 0 207 | } 208 | return Double(player.speedRate) 209 | } 210 | } 211 | 212 | extension Int { 213 | func msToCMTime() -> CMTime { 214 | return CMTime(value: CMTimeValue(self), timescale: 1000) 215 | } 216 | } 217 | 218 | extension CMTime { 219 | func toMs() -> Int { 220 | let seconds = self.seconds 221 | guard !(seconds.isNaN || seconds.isInfinite) else { 222 | return 0 223 | } 224 | return Int(seconds * 1000) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /ios/Classes/FlutterPlayerView.swift: -------------------------------------------------------------------------------- 1 | import ApiVideoPlayer 2 | import AVFoundation 3 | import AVKit 4 | import Foundation 5 | 6 | class FlutterPlayerView: NSObject, FlutterStreamHandler { 7 | private let playerTexture = PlayerFlutterTexture() 8 | private let textureRegistry: FlutterTextureRegistry 9 | let textureId: Int64! 10 | private let frameUpdater: FrameUpdater 11 | private var displayLink: CADisplayLink! 12 | private let playerController: ApiVideoPlayerController 13 | private let playerLayer = AVPlayerLayer() // Only use to fix bugs according to flutter video_player plugin 14 | 15 | private let eventChannel: FlutterEventChannel 16 | private var eventSink: FlutterEventSink? 17 | 18 | init(binaryMessenger: FlutterBinaryMessenger, 19 | textureRegistry: FlutterTextureRegistry, 20 | videoOptions: VideoOptions? = nil, 21 | autoplay: Bool) 22 | { 23 | self.textureRegistry = textureRegistry 24 | playerController = ApiVideoPlayerController(videoOptions: videoOptions, playerLayer: playerLayer, autoplay: autoplay) 25 | textureId = self.textureRegistry.register(playerTexture) 26 | frameUpdater = FrameUpdater(textureRegistry: self.textureRegistry, textureId: textureId) 27 | displayLink = FlutterPlayerView.createDisplayLink(frameUpdater: frameUpdater) 28 | eventChannel = FlutterEventChannel(name: "video.api.player/events\(String(textureId))", binaryMessenger: binaryMessenger) 29 | super.init() 30 | eventChannel.setStreamHandler(self) 31 | playerController.addDelegate(delegate: self) 32 | } 33 | 34 | static func createVideoOutput() -> AVPlayerItemVideoOutput { 35 | let pixBuffAttributes = [kCVPixelBufferPixelFormatTypeKey: Int(kCVPixelFormatType_32BGRA), 36 | kCVPixelBufferIOSurfacePropertiesKey: [:]] as [String: Any] 37 | return AVPlayerItemVideoOutput(pixelBufferAttributes: pixBuffAttributes) 38 | } 39 | 40 | static func createDisplayLink(frameUpdater: FrameUpdater) -> CADisplayLink { 41 | let displayLink = CADisplayLink(target: frameUpdater, selector: #selector(FrameUpdater.onDisplayLink(_:))) 42 | displayLink.add(to: RunLoop.current, forMode: RunLoop.Mode.common) 43 | displayLink.isPaused = true 44 | return displayLink 45 | } 46 | 47 | var videoOptions: VideoOptions? { 48 | get { 49 | playerController.videoOptions 50 | } 51 | set { 52 | playerController.videoOptions = newValue 53 | } 54 | } 55 | 56 | var isPlaying: Bool { 57 | playerController.isPlaying 58 | } 59 | 60 | var isLive: Bool { 61 | playerController.isLive 62 | } 63 | 64 | var duration: CMTime { 65 | playerController.duration 66 | } 67 | 68 | var currentTime: CMTime { 69 | get { 70 | playerController.currentTime 71 | } 72 | set { 73 | eventSink?(["type": "seekStarted"]) 74 | playerController.seek(to: newValue) 75 | } 76 | } 77 | 78 | var autoplay: Bool { 79 | get { 80 | playerController.autoplay 81 | } 82 | set { 83 | playerController.autoplay = newValue 84 | } 85 | } 86 | 87 | var isMuted: Bool { 88 | get { 89 | playerController.isMuted 90 | } 91 | set { 92 | playerController.isMuted = newValue 93 | } 94 | } 95 | 96 | var volume: Float { 97 | get { 98 | playerController.volume 99 | } 100 | set { 101 | playerController.volume = newValue 102 | } 103 | } 104 | 105 | var isLooping: Bool { 106 | get { 107 | playerController.isLooping 108 | } 109 | set { 110 | playerController.isLooping = newValue 111 | } 112 | } 113 | 114 | var speedRate: Float { 115 | get { 116 | playerController.speedRate 117 | } 118 | set { 119 | playerController.speedRate = newValue 120 | } 121 | } 122 | 123 | var videoSize: CGSize? { 124 | let videoSize = playerController.videoSize 125 | if videoSize.width != 0, videoSize.height != 0 { 126 | return videoSize 127 | } else { 128 | return nil 129 | } 130 | } 131 | 132 | func play() { 133 | playerController.play() 134 | } 135 | 136 | func pause() { 137 | playerController.pause() 138 | } 139 | 140 | func seek(offset: CMTime) { 141 | eventSink?(["type": "seekStarted"]) 142 | playerController.seek(offset: offset) 143 | textureRegistry.textureFrameAvailable(textureId) // render frame of the new scene 144 | } 145 | 146 | func onTextureUnregistered() { 147 | dispose() 148 | } 149 | 150 | func dispose() { 151 | pause() 152 | playerController.removeOutput(output: playerTexture.videoOutput) 153 | displayLink.invalidate() 154 | textureRegistry.unregisterTexture(textureId) 155 | eventSink?(FlutterEndOfEventStream) 156 | } 157 | 158 | func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { 159 | eventSink = events 160 | return nil 161 | } 162 | 163 | func onCancel(withArguments _: Any?) -> FlutterError? { 164 | eventSink = nil 165 | return nil 166 | } 167 | } 168 | 169 | extension FlutterPlayerView: ApiVideoPlayerControllerPlayerDelegate { 170 | func didPrepare() {} 171 | 172 | func didReady() { 173 | playerController.addOutput(output: playerTexture.videoOutput) 174 | // Hack to load the first image. We don't need it in case of autoplay 175 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 176 | self.textureRegistry.textureFrameAvailable(self.textureId) 177 | } 178 | 179 | eventSink?(["type": "ready"]) 180 | } 181 | 182 | func didPause() { 183 | displayLink.isPaused = true 184 | eventSink?(["type": "paused"]) 185 | } 186 | 187 | func didPlay() { 188 | displayLink.isPaused = false 189 | eventSink?(["type": "played"]) 190 | } 191 | 192 | func didReplay() {} 193 | 194 | func didMute() {} 195 | 196 | func didUnMute() {} 197 | 198 | func didLoop() {} 199 | 200 | func didSetVolume(_: Float) {} 201 | 202 | func didSeek(_: CMTime, _: CMTime) { 203 | eventSink?(["type": "seek"]) 204 | } 205 | 206 | func didEnd() { 207 | displayLink.isPaused = true 208 | eventSink?(["type": "ended"]) 209 | } 210 | 211 | func didError(_ error: Error) { 212 | eventSink?(FlutterError(code: "error", message: error.localizedDescription, details: "Stacktrace: \(Thread.callStackSymbols)")) 213 | } 214 | 215 | func didVideoSizeChanged(_: CGSize) {} 216 | } 217 | 218 | class FrameUpdater: NSObject { 219 | private let textureRegistry: FlutterTextureRegistry 220 | private let textureId: Int64 221 | 222 | init(textureRegistry: FlutterTextureRegistry, textureId: Int64) { 223 | self.textureRegistry = textureRegistry 224 | self.textureId = textureId 225 | } 226 | 227 | @objc func onDisplayLink(_: CADisplayLink) { 228 | textureRegistry.textureFrameAvailable(textureId) 229 | } 230 | } 231 | 232 | class PlayerFlutterTexture: NSObject, FlutterTexture { 233 | let videoOutput = FlutterPlayerView.createVideoOutput() 234 | 235 | func copyPixelBuffer() -> Unmanaged? { 236 | let outputItemTime = videoOutput.itemTime(forHostTime: CACurrentMediaTime()) 237 | if videoOutput.hasNewPixelBuffer(forItemTime: outputItemTime) { 238 | guard let pixelBuffer = videoOutput.copyPixelBuffer(forItemTime: outputItemTime, itemTimeForDisplay: nil) else { 239 | return nil 240 | } 241 | return Unmanaged.passRetained(pixelBuffer) 242 | } else { 243 | return nil 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /ios/Classes/SwiftApiVideoPlayerPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | public class SwiftApiVideoPlayerPlugin: NSObject, FlutterPlugin { 5 | private var textureRegistry: FlutterTextureRegistry! 6 | private var messenger: FlutterBinaryMessenger! 7 | private var api: MethodCallHandler! 8 | 9 | public static func register(with registrar: FlutterPluginRegistrar) { 10 | let instance = SwiftApiVideoPlayerPlugin(registrar: registrar) 11 | registrar.publish(instance) 12 | } 13 | 14 | init(registrar: FlutterPluginRegistrar) { 15 | textureRegistry = registrar.textures() 16 | messenger = registrar.messenger() 17 | api = MethodCallHandler(binaryMessenger: messenger, controller: FlutterPlayerController(binaryMessenger: messenger, textureRegistry: textureRegistry)) 18 | } 19 | 20 | public func detachFromEngine(for _: FlutterPluginRegistrar) { 21 | api.dispose() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/apivideo_player.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint apivideo_player.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'apivideo_player' 7 | s.version = '0.0.1' 8 | s.summary = 'A new Flutter plugin project.' 9 | s.description = <<-DESC 10 | A new Flutter plugin project. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'Flutter' 18 | s.dependency 'ApiVideoPlayer', "1.3.0" 19 | s.platform = :ios, '11.0' 20 | 21 | # Flutter.framework does not contain a i386 slice. 22 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 23 | s.swift_version = '5.0' 24 | end 25 | -------------------------------------------------------------------------------- /lib/apivideo_player.dart: -------------------------------------------------------------------------------- 1 | export 'src/apivideo_player_controller.dart'; 2 | export 'src/apivideo_types.dart'; 3 | export 'src/platform/apivideo_mobile_player_platform.dart'; 4 | export 'src/style/apivideo_style.dart'; 5 | export 'src/widgets/apivideo_player.dart'; 6 | export 'src/widgets/apivideo_player_controls_bar.dart'; 7 | export 'src/widgets/apivideo_player_opacity.dart'; 8 | export 'src/widgets/apivideo_player_overlay.dart'; 9 | export 'src/widgets/apivideo_player_settings_bar.dart'; 10 | export 'src/widgets/apivideo_player_time_slider.dart'; 11 | export 'src/widgets/apivideo_player_video.dart'; 12 | -------------------------------------------------------------------------------- /lib/src/apivideo_player_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:apivideo_player/apivideo_player.dart'; 4 | import 'package:apivideo_player/src/apivideo_player_life_cycle_observer.dart'; 5 | import 'package:apivideo_player/src/apivideo_types.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:meta/meta.dart'; 8 | 9 | import 'platform/apivideo_player_platform_interface.dart'; 10 | 11 | ApiVideoPlayerPlatform get _playerPlatform { 12 | return ApiVideoPlayerPlatform.instance; 13 | } 14 | 15 | class ApiVideoPlayerController { 16 | final VideoOptions _initialVideoOptions; 17 | final bool _initialAutoplay; 18 | 19 | static const int kUninitializedTextureId = -1; 20 | int _textureId = kUninitializedTextureId; 21 | 22 | StreamSubscription? _eventSubscription; 23 | final List _eventsListeners = []; 24 | final List _widgetListeners = []; 25 | 26 | PlayerLifeCycleObserver? _lifeCycleObserver; 27 | 28 | /// This is just exposed for testing. Do not use it. 29 | @internal 30 | int get textureId => _textureId; 31 | 32 | /// Creates a new controller where each event callbacks are set explicitly. 33 | ApiVideoPlayerController({ 34 | required VideoOptions videoOptions, 35 | bool autoplay = false, 36 | VoidCallback? onReady, 37 | VoidCallback? onPlay, 38 | VoidCallback? onPause, 39 | VoidCallback? onEnd, 40 | Function(Object)? onError, 41 | }) : _initialAutoplay = autoplay, 42 | _initialVideoOptions = videoOptions { 43 | _eventsListeners.add(ApiVideoPlayerControllerEventsListener( 44 | onReady: onReady, 45 | onPlay: onPlay, 46 | onPause: onPause, 47 | onEnd: onEnd, 48 | onError: onError)); 49 | } 50 | 51 | /// Creates a new controller with a [ApiVideoPlayerControllerEventsListener]. 52 | ApiVideoPlayerController.fromListener( 53 | {required VideoOptions videoOptions, 54 | bool autoplay = false, 55 | ApiVideoPlayerControllerEventsListener? listener}) 56 | : _initialAutoplay = autoplay, 57 | _initialVideoOptions = videoOptions { 58 | if (listener != null) { 59 | _eventsListeners.add(listener); 60 | } 61 | } 62 | 63 | /// Whether the controller has been created. 64 | Future get isCreated => _playerPlatform.isCreated(_textureId); 65 | 66 | /// Whether the video is playing. 67 | Future get isPlaying { 68 | return _playerPlatform.isPlaying(_textureId); 69 | } 70 | 71 | /// Whether the current video is a live. 72 | Future get isLive async { 73 | return await _playerPlatform.isLive(_textureId); 74 | } 75 | 76 | /// The video current time. 77 | Future get currentTime async { 78 | final milliseconds = await _playerPlatform.getCurrentTime(_textureId); 79 | return Duration(milliseconds: milliseconds); 80 | } 81 | 82 | /// Sets the current playback time. 83 | Future setCurrentTime(Duration currentTime) { 84 | return _playerPlatform.setCurrentTime( 85 | _textureId, currentTime.inMilliseconds); 86 | } 87 | 88 | /// The duration of the video. 89 | Future get duration async { 90 | final milliseconds = await _playerPlatform.getDuration(_textureId); 91 | return Duration(milliseconds: milliseconds); 92 | } 93 | 94 | /// The current video options. 95 | Future get videoOptions { 96 | return _playerPlatform.getVideoOptions(_textureId); 97 | } 98 | 99 | /// Sets the video options to play a new video. 100 | Future setVideoOptions(VideoOptions videoOptions) { 101 | return _playerPlatform.setVideoOptions(_textureId, videoOptions); 102 | } 103 | 104 | /// Whether the video will be play automatically. 105 | Future get autoplay { 106 | return _playerPlatform.getAutoplay(_textureId); 107 | } 108 | 109 | /// Sets if the video will be play automatically. 110 | Future setAutoplay(bool autoplay) { 111 | return _playerPlatform.setAutoplay(_textureId, autoplay); 112 | } 113 | 114 | /// Whether the video is muted. 115 | Future get isMuted { 116 | return _playerPlatform.getIsMuted(_textureId); 117 | } 118 | 119 | /// Mutes/unmutes the video. 120 | Future setIsMuted(bool isMuted) { 121 | return _playerPlatform.setIsMuted(_textureId, isMuted); 122 | } 123 | 124 | /// Whether the video is in loop mode. 125 | Future get isLooping { 126 | return _playerPlatform.getIsLooping(_textureId); 127 | } 128 | 129 | /// Sets if the video should be played in loop. 130 | Future setIsLooping(bool isLooping) { 131 | return _playerPlatform.setIsLooping(_textureId, isLooping); 132 | } 133 | 134 | /// The current audio volume 135 | Future get volume { 136 | return _playerPlatform.getVolume(_textureId); 137 | } 138 | 139 | /// Sets the audio volume. 140 | /// 141 | /// From 0 to 1 (0 = muted, 1 = 100%). 142 | Future setVolume(double volume) { 143 | if (volume < 0 || volume > 1) { 144 | throw ArgumentError('Volume must be between 0 and 1'); 145 | } 146 | return _playerPlatform.setVolume(_textureId, volume); 147 | } 148 | 149 | /// The playback speed rate. 150 | Future get speedRate { 151 | return _playerPlatform.getPlaybackRate(_textureId); 152 | } 153 | 154 | /// Sets the playback speed rate. 155 | /// 156 | /// We recommend to set the value from 0.5 to 2 (0.5 = 50%, 2 = 200%). 157 | Future setSpeedRate(double speedRate) { 158 | return _playerPlatform.setPlaybackRate(_textureId, speedRate); 159 | } 160 | 161 | /// The current video size. 162 | Future get videoSize { 163 | return _playerPlatform.getVideoSize(_textureId); 164 | } 165 | 166 | /// Initializes the controller. 167 | Future initialize() async { 168 | _textureId = await _playerPlatform.initialize(_initialAutoplay) ?? 169 | kUninitializedTextureId; 170 | 171 | _lifeCycleObserver = PlayerLifeCycleObserver(this); 172 | _lifeCycleObserver?.initialize(); 173 | 174 | _eventSubscription = _playerPlatform 175 | .playerEventsFor(_textureId) 176 | .listen(_eventListener, onError: _errorListener); 177 | 178 | await _playerPlatform.create(_textureId, _initialVideoOptions); 179 | 180 | for (var listener in [..._widgetListeners]) { 181 | if (listener.onTextureReady != null) { 182 | listener.onTextureReady!(); 183 | } 184 | } 185 | 186 | return; 187 | } 188 | 189 | /// Plays the video. 190 | Future play() { 191 | return _playerPlatform.play(_textureId); 192 | } 193 | 194 | /// Pauses the video. 195 | Future pause() { 196 | return _playerPlatform.pause(_textureId); 197 | } 198 | 199 | /// Disposes the controller. 200 | Future dispose() async { 201 | await _eventSubscription?.cancel(); 202 | _eventsListeners.clear(); 203 | await _playerPlatform.dispose(_textureId); 204 | _lifeCycleObserver?.dispose(); 205 | return; 206 | } 207 | 208 | /// Adds/substracts the given Duration to/from the playback time. 209 | Future seek(Duration offset) { 210 | return _playerPlatform.seek(_textureId, offset.inMilliseconds); 211 | } 212 | 213 | /// Adds an event listener to this controller. 214 | /// 215 | /// ```dart 216 | /// final ApiVideoPlayerControllerEventsListener _eventsListener = 217 | /// ApiVideoPlayerControllerEventsListener(onPlay: () => print('PLAY')); 218 | /// 219 | /// controller.addEventsListener(_eventsListener); 220 | /// ``` 221 | void addListener(ApiVideoPlayerControllerEventsListener listener) { 222 | _eventsListeners.add(listener); 223 | } 224 | 225 | /// Adds an event listener to this controller. 226 | /// 227 | /// ```dart 228 | /// final ApiVideoPlayerControllerEventsListener _eventsListener = 229 | /// ApiVideoPlayerControllerEventsListener(onPlay: () => print('PLAY')); 230 | /// 231 | /// controller.removeEventsListener(_eventsListener); 232 | /// ``` 233 | void removeListener(ApiVideoPlayerControllerEventsListener listener) { 234 | _eventsListeners.remove(listener); 235 | } 236 | 237 | /// Internal use only. Do not use it. 238 | @internal 239 | void addWidgetListener(ApiVideoPlayerControllerWidgetListener listener) { 240 | _widgetListeners.add(listener); 241 | } 242 | 243 | /// Internal use only. Do not use it. 244 | @internal 245 | void removeWidgetListener(ApiVideoPlayerControllerWidgetListener listener) { 246 | _widgetListeners.remove(listener); 247 | } 248 | 249 | void _errorListener(Object obj) { 250 | final PlatformException e = obj as PlatformException; 251 | for (var listener in [..._eventsListeners]) { 252 | if (listener.onError != null) { 253 | listener.onError!(e); 254 | } 255 | } 256 | } 257 | 258 | void _eventListener(PlayerEvent event) { 259 | switch (event.type) { 260 | case PlayerEventType.ready: 261 | for (var listener in [..._eventsListeners]) { 262 | if (listener.onReady != null) { 263 | listener.onReady!(); 264 | } 265 | } 266 | break; 267 | case PlayerEventType.played: 268 | for (var listener in [..._eventsListeners]) { 269 | if (listener.onPlay != null) { 270 | listener.onPlay!(); 271 | } 272 | } 273 | break; 274 | case PlayerEventType.paused: 275 | for (var listener in [..._eventsListeners]) { 276 | if (listener.onPause != null) { 277 | listener.onPause!(); 278 | } 279 | } 280 | break; 281 | case PlayerEventType.seek: 282 | for (var listener in [..._eventsListeners]) { 283 | if (listener.onSeek != null) { 284 | listener.onSeek!(); 285 | } 286 | } 287 | break; 288 | case PlayerEventType.seekStarted: 289 | for (var listener in [..._eventsListeners]) { 290 | if (listener.onSeekStarted != null) { 291 | listener.onSeekStarted!(); 292 | } 293 | } 294 | break; 295 | case PlayerEventType.ended: 296 | for (var listener in [..._eventsListeners]) { 297 | if (listener.onEnd != null) { 298 | listener.onEnd!(); 299 | } 300 | } 301 | break; 302 | case PlayerEventType.unknown: 303 | // Nothing to do 304 | break; 305 | } 306 | } 307 | } 308 | 309 | /// The controller events listener. 310 | /// Use this to listen to the player events. 311 | class ApiVideoPlayerControllerEventsListener { 312 | final VoidCallback? onReady; 313 | final VoidCallback? onPlay; 314 | final VoidCallback? onPause; 315 | final VoidCallback? onSeek; 316 | final VoidCallback? onSeekStarted; 317 | final VoidCallback? onEnd; 318 | final Function(Object)? onError; 319 | 320 | ApiVideoPlayerControllerEventsListener( 321 | {this.onReady, 322 | this.onPlay, 323 | this.onPause, 324 | this.onSeek, 325 | this.onSeekStarted, 326 | this.onEnd, 327 | this.onError}); 328 | } 329 | 330 | /// The internal controller widget listener. 331 | /// Uses by the [ApiVideoPlayerController] to notify the widget when the texture is ready. 332 | /// Only to be used in the Widget that hold the video such as [ApiVideoPlayerVideo]. 333 | class ApiVideoPlayerControllerWidgetListener { 334 | final VoidCallback? onTextureReady; 335 | 336 | ApiVideoPlayerControllerWidgetListener({this.onTextureReady}); 337 | } 338 | -------------------------------------------------------------------------------- /lib/src/apivideo_player_life_cycle_observer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | import '../apivideo_player.dart'; 4 | 5 | /// The player life cycle observer. 6 | /// It pauses the player when the app is paused. 7 | /// It resumes the player when the app is resumed (if the player was playing before). 8 | class PlayerLifeCycleObserver extends Object with WidgetsBindingObserver { 9 | final ApiVideoPlayerController controller; 10 | bool _wasPlayingBeforePause = false; 11 | 12 | PlayerLifeCycleObserver(this.controller); 13 | 14 | void initialize() { 15 | _ambiguate(WidgetsBinding.instance)!.addObserver(this); 16 | } 17 | 18 | @override 19 | void didChangeAppLifecycleState(AppLifecycleState state) async { 20 | switch (state) { 21 | case AppLifecycleState.resumed: 22 | if (_wasPlayingBeforePause) { 23 | controller.play(); 24 | } 25 | break; 26 | case AppLifecycleState.paused: 27 | pause(); 28 | break; 29 | default: 30 | break; 31 | } 32 | } 33 | 34 | void pause() async { 35 | _wasPlayingBeforePause = await controller.isPlaying; 36 | controller.pause(); 37 | } 38 | 39 | void dispose() { 40 | _ambiguate(WidgetsBinding.instance)!.removeObserver(this); 41 | } 42 | } 43 | 44 | T? _ambiguate(T? value) => value; 45 | -------------------------------------------------------------------------------- /lib/src/apivideo_types.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'apivideo_types.g.dart'; 4 | 5 | /// The video types enabled by api.video platform. 6 | enum VideoType { 7 | /// Video on demand. 8 | @JsonValue("vod") 9 | vod, 10 | 11 | /// Live video. 12 | @JsonValue("live") 13 | live 14 | } 15 | 16 | /// The video options that defines a video on api.video platform. 17 | @JsonSerializable() 18 | class VideoOptions { 19 | /// The video id or live stream id from api.video platform. 20 | String videoId; 21 | 22 | /// The video type. Either [VideoType.vod] or [VideoType.live]. 23 | VideoType type; 24 | 25 | /// The private token if the video is private. 26 | String? token; 27 | 28 | /// Creates a [VideoOptions] object from a [videoId] a [type] and a [token]. 29 | /// The [token] could be null. 30 | VideoOptions.raw({required this.videoId, required this.type, this.token}); 31 | 32 | /// Creates a [VideoOptions] object from a [videoId] a [type] and a [token]. 33 | /// If the [type] is null, it will be inferred from the [videoId]: 34 | /// * If the [videoId] starts with "vi", the type will be [VideoType.vod]. 35 | /// * If the [videoId] starts with "li", the type will be [VideoType.live]. 36 | /// The [token] could be null. 37 | factory VideoOptions( 38 | {required String videoId, VideoType? type, String? token}) { 39 | type ??= _inferType(videoId); 40 | return VideoOptions.raw(videoId: videoId, type: type, token: token); 41 | } 42 | 43 | /// Creates a [VideoOptions] from a [json] map. 44 | factory VideoOptions.fromJson(Map json) => 45 | _$VideoOptionsFromJson(json); 46 | 47 | /// Creates a json map from a [VideoOptions]. 48 | Map toJson() => _$VideoOptionsToJson(this); 49 | 50 | /// Infers the [VideoType] from a [videoId]. 51 | /// If the [videoId] starts with "vi", the type will be [VideoType.vod]. 52 | /// If the [videoId] starts with "li", the type will be [VideoType.live]. 53 | static VideoType _inferType(String videoId) { 54 | if (videoId.startsWith("vi")) { 55 | return VideoType.vod; 56 | } else if (videoId.startsWith("li")) { 57 | return VideoType.live; 58 | } else { 59 | throw ArgumentError( 60 | "Failed to infer the video type from the videoId: $videoId"); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/apivideo_types.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'apivideo_types.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | VideoOptions _$VideoOptionsFromJson(Map json) => VideoOptions( 10 | videoId: json['videoId'] as String, 11 | type: $enumDecodeNullable(_$VideoTypeEnumMap, json['type']) ?? 12 | VideoType.vod, 13 | token: json['token'] as String?, 14 | ); 15 | 16 | Map _$VideoOptionsToJson(VideoOptions instance) => 17 | { 18 | 'videoId': instance.videoId, 19 | 'type': _$VideoTypeEnumMap[instance.type]!, 20 | 'token': instance.token, 21 | }; 22 | 23 | const _$VideoTypeEnumMap = { 24 | VideoType.vod: 'vod', 25 | VideoType.live: 'live', 26 | }; 27 | -------------------------------------------------------------------------------- /lib/src/platform/apivideo_mobile_player_platform.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | import '../apivideo_types.dart'; 5 | import 'apivideo_player_platform_interface.dart'; 6 | 7 | /// The implementation of [ApiVideoPlayerPlatform] for mobile (Android and iOS). 8 | class ApiVideoMobilePlayer extends ApiVideoPlayerPlatform { 9 | final MethodChannel _channel = 10 | const MethodChannel('video.api.player/controller'); 11 | 12 | /// Registers this class as the default instance of [PathProviderPlatform]. 13 | static void registerWith() { 14 | ApiVideoPlayerPlatform.instance = ApiVideoMobilePlayer(); 15 | } 16 | 17 | @override 18 | Future isCreated(int textureId) async { 19 | final Map reply = 20 | await _channel.invokeMapMethodWithTexture( 21 | 'isCreated', TextureMessage(textureId: textureId)) as Map; 22 | return reply['isCreated'] as bool; 23 | } 24 | 25 | @override 26 | Future isPlaying(int textureId) async { 27 | final Map reply = 28 | await _channel.invokeMapMethodWithTexture( 29 | 'isPlaying', TextureMessage(textureId: textureId)) as Map; 30 | return reply['isPlaying'] as bool; 31 | } 32 | 33 | @override 34 | Future isLive(int textureId) async { 35 | final Map reply = 36 | await _channel.invokeMapMethodWithTexture( 37 | 'isLive', TextureMessage(textureId: textureId)) as Map; 38 | return reply['isLive'] as bool; 39 | } 40 | 41 | @override 42 | Future getCurrentTime(int textureId) async { 43 | final Map reply = 44 | await _channel.invokeMapMethodWithTexture( 45 | 'getCurrentTime', TextureMessage(textureId: textureId)) as Map; 46 | return reply['currentTime'] as int; 47 | } 48 | 49 | @override 50 | Future setCurrentTime(int textureId, int currentTime) { 51 | final Map params = { 52 | "currentTime": currentTime 53 | }; 54 | return _channel.invokeMapMethodWithTexture( 55 | 'setCurrentTime', TextureMessage(textureId: textureId), params); 56 | } 57 | 58 | @override 59 | Future getDuration(int textureId) async { 60 | final Map reply = 61 | await _channel.invokeMapMethodWithTexture( 62 | 'getDuration', TextureMessage(textureId: textureId)) as Map; 63 | return reply['duration'] as int; 64 | } 65 | 66 | @override 67 | Future getVideoOptions(int textureId) async { 68 | final Map reply = 69 | await _channel.invokeMapMethodWithTexture( 70 | 'getVideoOptions', TextureMessage(textureId: textureId)) 71 | as Map; 72 | return VideoOptions.fromJson(reply); 73 | } 74 | 75 | @override 76 | Future setVideoOptions(int textureId, VideoOptions videoOptions) { 77 | final Map videoParams = { 78 | "videoOptions": videoOptions.toJson() 79 | }; 80 | return _channel.invokeMapMethodWithTexture( 81 | 'setVideoOptions', TextureMessage(textureId: textureId), videoParams); 82 | } 83 | 84 | @override 85 | Future getAutoplay(int textureId) async { 86 | final Map reply = 87 | await _channel.invokeMapMethodWithTexture( 88 | 'getAutoplay', TextureMessage(textureId: textureId)) as Map; 89 | return reply['autoplay'] as bool; 90 | } 91 | 92 | @override 93 | Future setAutoplay(int textureId, bool autoplay) { 94 | final Map params = {"autoplay": autoplay}; 95 | return _channel.invokeMapMethodWithTexture( 96 | 'setAutoplay', TextureMessage(textureId: textureId), params); 97 | } 98 | 99 | @override 100 | Future getIsMuted(int textureId) async { 101 | final Map reply = 102 | await _channel.invokeMapMethodWithTexture( 103 | 'getIsMuted', TextureMessage(textureId: textureId)) as Map; 104 | return reply['isMuted'] as bool; 105 | } 106 | 107 | @override 108 | Future setIsMuted(int textureId, bool isMuted) { 109 | final Map params = {"isMuted": isMuted}; 110 | return _channel.invokeMapMethodWithTexture( 111 | 'setIsMuted', TextureMessage(textureId: textureId), params); 112 | } 113 | 114 | @override 115 | Future getIsLooping(int textureId) async { 116 | final Map reply = 117 | await _channel.invokeMapMethodWithTexture( 118 | 'getIsLooping', TextureMessage(textureId: textureId)) as Map; 119 | return reply['isLooping'] as bool; 120 | } 121 | 122 | @override 123 | Future setIsLooping(int textureId, bool isLooping) { 124 | final Map params = { 125 | "isLooping": isLooping 126 | }; 127 | return _channel.invokeMapMethodWithTexture( 128 | 'setIsLooping', TextureMessage(textureId: textureId), params); 129 | } 130 | 131 | @override 132 | Future getVolume(int textureId) async { 133 | final Map reply = 134 | await _channel.invokeMapMethodWithTexture( 135 | 'getVolume', TextureMessage(textureId: textureId)) as Map; 136 | return reply['volume'] as double; 137 | } 138 | 139 | @override 140 | Future setVolume(int textureId, double volume) { 141 | final Map params = {"volume": volume}; 142 | return _channel.invokeMapMethodWithTexture( 143 | 'setVolume', TextureMessage(textureId: textureId), params); 144 | } 145 | 146 | @override 147 | Future getVideoSize(int textureId) async { 148 | final Map reply = 149 | await _channel.invokeMapMethodWithTexture( 150 | 'getVideoSize', TextureMessage(textureId: textureId)) as Map; 151 | if (reply.containsKey("width") && reply.containsKey("height")) { 152 | return Size(reply["width"] as double, reply["height"] as double); 153 | } 154 | return null; 155 | } 156 | 157 | @override 158 | Future setPlaybackRate(int textureId, double speedRate) { 159 | final Map params = { 160 | "speedRate": speedRate 161 | }; 162 | return _channel.invokeMapMethodWithTexture( 163 | 'setPlaybackSpeed', TextureMessage(textureId: textureId), params); 164 | } 165 | 166 | @override 167 | Future getPlaybackRate(int textureId) async { 168 | final Map reply = 169 | await _channel.invokeMapMethodWithTexture( 170 | 'getPlaybackSpeed', TextureMessage(textureId: textureId)) as Map; 171 | return reply['speedRate'] as double; 172 | } 173 | 174 | @override 175 | Future initialize(bool autoplay) async { 176 | final Map params = {"autoplay": autoplay}; 177 | final Map? reply = 178 | await _channel.invokeMapMethod('initialize', params); 179 | int textureId = reply!['textureId']! as int; 180 | return textureId; 181 | } 182 | 183 | @override 184 | Future create(int textureId, VideoOptions videoOptions) { 185 | return setVideoOptions(textureId, videoOptions); 186 | } 187 | 188 | @override 189 | Future dispose(int textureId) { 190 | return _channel.invokeMapMethodWithTexture( 191 | 'dispose', TextureMessage(textureId: textureId)); 192 | } 193 | 194 | @override 195 | Future play(int textureId) { 196 | return _channel.invokeMapMethodWithTexture( 197 | 'play', TextureMessage(textureId: textureId)); 198 | } 199 | 200 | @override 201 | Future pause(int textureId) { 202 | return _channel.invokeMapMethodWithTexture( 203 | 'pause', TextureMessage(textureId: textureId)); 204 | } 205 | 206 | @override 207 | Future seek(int textureId, int offset) { 208 | final Map params = {"offset": offset}; 209 | return _channel.invokeMapMethodWithTexture( 210 | 'seek', TextureMessage(textureId: textureId), params); 211 | } 212 | 213 | @override 214 | Widget buildView(int textureId) { 215 | return Texture(textureId: textureId); 216 | } 217 | 218 | @override 219 | Stream playerEventsFor(int textureId) { 220 | return _eventChannelFor(textureId) 221 | .receiveBroadcastStream() 222 | .map((dynamic map) { 223 | final Map event = map as Map; 224 | switch (event['type']) { 225 | case 'ready': 226 | return PlayerEvent(type: PlayerEventType.ready); 227 | case 'played': 228 | return PlayerEvent(type: PlayerEventType.played); 229 | case 'paused': 230 | return PlayerEvent(type: PlayerEventType.paused); 231 | case 'seek': 232 | return PlayerEvent(type: PlayerEventType.seek); 233 | case 'seekStarted': 234 | return PlayerEvent(type: PlayerEventType.seekStarted); 235 | case 'ended': 236 | return PlayerEvent(type: PlayerEventType.ended); 237 | default: 238 | return PlayerEvent(type: PlayerEventType.unknown); 239 | } 240 | }); 241 | } 242 | 243 | EventChannel _eventChannelFor(int textureId) { 244 | return EventChannel('video.api.player/events$textureId'); 245 | } 246 | } 247 | 248 | /// Internal extensions on [MethodChannel] to handle [TextureMessage]. 249 | extension MethodChannelExtension on MethodChannel { 250 | Future?> invokeMapMethodWithTexture( 251 | String method, TextureMessage textureMessage, 252 | [dynamic arguments]) async { 253 | final Map params = {}; 254 | params.addAll(textureMessage.encode() as Map); 255 | if (arguments != null) { 256 | params.addAll(arguments); 257 | } 258 | 259 | return invokeMapMethod(method, params); 260 | } 261 | } 262 | 263 | /// Internal message codec for handling texture id for mobile targets. 264 | class TextureMessage { 265 | TextureMessage({ 266 | required this.textureId, 267 | }); 268 | 269 | int textureId; 270 | 271 | Object encode() { 272 | final Map paramMap = {}; 273 | paramMap['textureId'] = textureId; 274 | return paramMap; 275 | } 276 | 277 | static TextureMessage decode(Object message) { 278 | final Map paramMap = message as Map; 279 | return TextureMessage( 280 | textureId: paramMap['textureId']! as int, 281 | ); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /lib/src/platform/apivideo_player_platform_interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 3 | 4 | import '../apivideo_types.dart'; 5 | 6 | /// The interface that each platform must implement. 7 | abstract class ApiVideoPlayerPlatform extends PlatformInterface { 8 | /// Constructs a ApiVideoPlayerPlatform. 9 | ApiVideoPlayerPlatform() : super(token: _token); 10 | 11 | static final Object _token = Object(); 12 | 13 | static ApiVideoPlayerPlatform _instance = _PlatformImplementation(); 14 | 15 | /// The default instance of [ApiVideoPlayerPlatform] to use. 16 | /// 17 | /// Defaults to [_PlatformImplementation]. 18 | static ApiVideoPlayerPlatform get instance => _instance; 19 | 20 | /// Platform-specific implementations should set this with their own 21 | /// platform-specific class that extends [ApiVideoPlayerPlatform] when 22 | /// they register themselves. 23 | static set instance(ApiVideoPlayerPlatform instance) { 24 | PlatformInterface.verifyToken(instance, _token); 25 | _instance = instance; 26 | } 27 | 28 | /// Returns [true] if the platform element has been created. 29 | Future isCreated(int textureId) { 30 | throw UnimplementedError('isCreated() has not been implemented.'); 31 | } 32 | 33 | /// Returns whether the video is playing or not 34 | Future isPlaying(int textureId) { 35 | throw UnimplementedError('isPlaying() has not been implemented.'); 36 | } 37 | 38 | /// Returns whether the video is a live or not 39 | Future isLive(int textureId) { 40 | throw UnimplementedError('isLive() has not been implemented.'); 41 | } 42 | 43 | /// Returns number of milliseconds from the beginning of the video to the 44 | /// current time 45 | Future getCurrentTime(int textureId) { 46 | throw UnimplementedError('getCurrentTime() has not been implemented.'); 47 | } 48 | 49 | /// Sets the video position to a time in milliseconds from the start. 50 | Future setCurrentTime(int textureId, int currentTime) { 51 | throw UnimplementedError('setCurrentTime() has not been implemented.'); 52 | } 53 | 54 | /// Returns the duration of the video 55 | Future getDuration(int textureId) { 56 | throw UnimplementedError('getDuration() has not been implemented.'); 57 | } 58 | 59 | /// Returns the current video 60 | Future getVideoOptions(int textureId) { 61 | throw UnimplementedError('getVideoOptions() has not been implemented.'); 62 | } 63 | 64 | /// Sets the video 65 | Future setVideoOptions(int textureId, VideoOptions videoOptions) { 66 | throw UnimplementedError('setVideoOptions() has not been implemented.'); 67 | } 68 | 69 | /// Gets the autoplay state 70 | Future getAutoplay(int textureId) { 71 | throw UnimplementedError('getAutoplay() has not been implemented.'); 72 | } 73 | 74 | /// Sets the autoplay state 75 | Future setAutoplay(int textureId, bool autoplay) { 76 | throw UnimplementedError('setAutoplay() has not been implemented.'); 77 | } 78 | 79 | /// Gets the muted state 80 | Future getIsMuted(int textureId) { 81 | throw UnimplementedError('getIsMuted() has not been implemented.'); 82 | } 83 | 84 | /// Sets the muted state 85 | Future setIsMuted(int textureId, bool isMuted) { 86 | throw UnimplementedError('setIsMuted() has not been implemented.'); 87 | } 88 | 89 | /// Gets the looping state 90 | Future getIsLooping(int textureId) { 91 | throw UnimplementedError('getIsLooping() has not been implemented.'); 92 | } 93 | 94 | /// Sets the looping state 95 | Future setIsLooping(int textureId, bool isLooping) async { 96 | throw UnimplementedError('setIsLooping() has not been implemented.'); 97 | } 98 | 99 | /// Gets the volume 100 | Future getVolume(int textureId) { 101 | throw UnimplementedError('getVolume() has not been implemented.'); 102 | } 103 | 104 | /// Sets the volume from 0 to 100. 105 | Future setVolume(int textureId, double volume) { 106 | throw UnimplementedError('setVolume() has not been implemented.'); 107 | } 108 | 109 | /// Gets the video size 110 | Future getVideoSize(int textureId) { 111 | throw UnimplementedError('getVideoSize() has not been implemented.'); 112 | } 113 | 114 | /// Creates the texture and registers the native events caller. 115 | /// returns the texture id 116 | Future initialize(bool autoplay) { 117 | throw UnimplementedError('initialize() has not been implemented.'); 118 | } 119 | 120 | /// Creates the native player 121 | Future create(int textureId, VideoOptions videoOptions) { 122 | throw UnimplementedError('create() has not been implemented.'); 123 | } 124 | 125 | /// Releases the video player. 126 | Future dispose(int textureId) { 127 | throw UnimplementedError('dispose() has not been implemented.'); 128 | } 129 | 130 | /// Starts the video playback. 131 | Future play(int textureId) { 132 | throw UnimplementedError('play() has not been implemented.'); 133 | } 134 | 135 | /// Stops the video playback. 136 | Future pause(int textureId) { 137 | throw UnimplementedError('pause() has not been implemented.'); 138 | } 139 | 140 | /// Sets the video position to a time in milliseconds from the current time. 141 | Future seek(int textureId, int offset) { 142 | throw UnimplementedError('seek() has not been implemented.'); 143 | } 144 | 145 | Future setPlaybackRate(int textureId, double speedRate) { 146 | throw UnimplementedError('setPlaybackSpeed() has not been implemented.'); 147 | } 148 | 149 | Future getPlaybackRate(int textureId) { 150 | throw UnimplementedError('getPlaybackSpeed() has not been implemented.'); 151 | } 152 | 153 | /// Returns a widget displaying the video with a given textureID. 154 | Widget buildView(int textureId) { 155 | throw UnimplementedError('buildView() has not been implemented.'); 156 | } 157 | 158 | /// Returns a Stream of [PlayerEvent]s. 159 | Stream playerEventsFor(int textureId) { 160 | throw UnimplementedError('playerEventsFor() has not been implemented.'); 161 | } 162 | } 163 | 164 | class _PlatformImplementation extends ApiVideoPlayerPlatform {} 165 | 166 | class PlayerEvent { 167 | /// Adds optional parameters here if needed 168 | 169 | /// The [PlayerEventType] 170 | final PlayerEventType type; 171 | 172 | PlayerEvent({required this.type}); 173 | } 174 | 175 | enum PlayerEventType { 176 | /// The player is ready. 177 | ready, 178 | 179 | /// The playback just started. 180 | played, 181 | 182 | /// The playback has been paused. 183 | paused, 184 | 185 | /// The video has been seek. 186 | seek, 187 | 188 | /// The video seek has started. 189 | seekStarted, 190 | 191 | /// The playback has been ended. 192 | ended, 193 | 194 | /// An unknown event has been received. 195 | unknown, 196 | } 197 | -------------------------------------------------------------------------------- /lib/src/platform/web/javascript_controller.dart: -------------------------------------------------------------------------------- 1 | @JS('window') 2 | library script.js; 3 | 4 | import 'package:js/js_util.dart' as js; 5 | 6 | import 'package:js/js.dart'; 7 | 8 | @JS('state.getCurrentTime') 9 | external int getCurrentTime(String playerId); 10 | 11 | @JS('state.setCurrentTime') 12 | external void setCurrentTime(String playerId, int currentTimeInSeconds); 13 | 14 | @JS('state.getDuration') 15 | external double getDuration(String playerId); 16 | 17 | @JS('state.getPlaying') 18 | external bool getPlaying(String playerId); 19 | 20 | @JS('state.isLiveStream') 21 | external bool isLiveStream(String playerId); 22 | 23 | @JS('state.getMuted') 24 | external bool getMuted(String playerId); 25 | 26 | @JS('state.getLoop') 27 | external bool getLoop(String playerId); 28 | 29 | @JS('state.getVolume') 30 | external double getVolume(String playerId); 31 | 32 | @JS('state.getVideoSize') 33 | external dynamic getVideoSize(String playerId); 34 | 35 | @JS('state.getPlaybackRate') 36 | external double getPlaybackRate(String playerId); 37 | 38 | @JS('state.loadConfig') 39 | external void loadConfig(String playerId, Object videoOptions); 40 | 41 | dynamic _nested(dynamic val) { 42 | if (val.runtimeType.toString() == 'LegacyJavaScriptObject') { 43 | return jsToMap(val); 44 | } 45 | return val; 46 | } 47 | 48 | /// A workaround to converting an object from JS to a Dart Map. 49 | Map jsToMap(jsObject) { 50 | return Map.fromIterable(_getKeysOfObject(jsObject), value: (key) { 51 | return _nested(js.getProperty(jsObject, key)); 52 | }); 53 | } 54 | 55 | // Both of these interfaces exist to call `Object.keys` from Dart. 56 | // 57 | // But you don't use them directly. Just see `jsToMap`. 58 | @JS('Object.keys') 59 | external List _getKeysOfObject(jsObject); 60 | -------------------------------------------------------------------------------- /lib/src/platform/web/utils/conversion.dart: -------------------------------------------------------------------------------- 1 | import 'dart:js' as js; 2 | import 'dart:js_util'; 3 | 4 | class Utils { 5 | /// Calls a JS object method that returns void only. 6 | static dynamic callJsMethod({ 7 | required int textureId, 8 | required String jsMethodName, 9 | List? args, 10 | }) { 11 | ArgumentError.checkNotNull(js.context['player$textureId'], 'player'); 12 | return js.JsObject.fromBrowserObject(js.context['player$textureId']) 13 | .callMethod( 14 | jsMethodName, 15 | args, 16 | ); 17 | } 18 | 19 | /// Handle a JS [Promise] that returns a value other than void 20 | /// and parse it into a Dart [Future]. 21 | static Future getPromiseFromJs({ 22 | required int textureId, 23 | required Function jsMethod, 24 | }) async { 25 | ArgumentError.checkNotNull(js.context['player$textureId'], 'player'); 26 | ArgumentError.checkNotNull(js.context['state'], 'state'); 27 | return await promiseToFuture( 28 | jsMethod(), 29 | ); 30 | } 31 | 32 | /// Converts seconds into milliseconds. 33 | static int secondsToMilliseconds({required double seconds}) => 34 | int.parse((seconds * 1000).toStringAsFixed(0)); 35 | } 36 | 37 | extension ConvertMapToJsObject on Map { 38 | /// Parse a [Map] to a Javascript object. 39 | Object toJsObject() { 40 | var object = newObject(); 41 | forEach((k, v) { 42 | if (v is Map) { 43 | setProperty(object, k, toJsObject()); 44 | } else { 45 | setProperty(object, k, v); 46 | } 47 | }); 48 | return object; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/platform/web/utils/player_event_type_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:apivideo_player/src/platform/apivideo_player_platform_interface.dart'; 2 | 3 | extension SelectedPlayerEventType on PlayerEventType { 4 | /// Cast the [PlayerEventType] name to his corresponding TypeScript PlayerSdk 5 | /// event 6 | String get displayPlayerSdkName { 7 | switch (this) { 8 | case PlayerEventType.ready: 9 | return 'ready'; 10 | case PlayerEventType.played: 11 | return 'play'; 12 | case PlayerEventType.paused: 13 | return 'pause'; 14 | case PlayerEventType.seek: 15 | return 'seeking'; 16 | case PlayerEventType.ended: 17 | return 'ended'; 18 | default: 19 | return 'unknown'; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/style/apivideo_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ApiVideoColors { 4 | static const MaterialColor orange = MaterialColor( 5 | 0xFFFA5B30, 6 | { 7 | 100: Color(0xFFFBDDD4), 8 | 200: Color(0xFFFFD1C5), 9 | 300: Color(0xFFFFB39E), 10 | 400: Color(0xFFFA5B30), 11 | 500: Color(0xFFE53101), 12 | }, 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/style/apivideo_icons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class ApiVideoIcons { 4 | ApiVideoIcons._(); 5 | 6 | static const _kFontFam = 'ApiVideoIcons'; 7 | static const String _kFontPkg = 'apivideo_player'; 8 | 9 | static const IconData pausePrimary = 10 | IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); 11 | static const IconData playPrimary = 12 | IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); 13 | static const IconData replayPrimary = 14 | IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/style/apivideo_label.dart: -------------------------------------------------------------------------------- 1 | /// The semantic label of a [ApiVideoPlayer]. 2 | class ApiVideoLabels { 3 | static const String play = "Play"; 4 | static const String pause = "Pause"; 5 | static const String forward = "Forward"; 6 | static const String backward = "Backward"; 7 | static const String replay = "Replay"; 8 | static const String playbackRate = "Playback rate"; 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/style/apivideo_style.dart: -------------------------------------------------------------------------------- 1 | import 'package:apivideo_player/src/style/apivideo_colors.dart'; 2 | import 'package:apivideo_player/src/widgets/apivideo_player_controls_bar.dart'; 3 | import 'package:apivideo_player/src/widgets/apivideo_player_settings_bar.dart'; 4 | import 'package:apivideo_player/src/widgets/apivideo_player_time_slider.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | /// Customizable style for the player. 8 | class PlayerStyle { 9 | const PlayerStyle({ 10 | this.settingsBarStyle, 11 | this.controlsBarStyle, 12 | this.timeSliderStyle, 13 | }); 14 | 15 | /// The style of the settings button (volume, playback rate,...). 16 | final SettingsBarStyle? settingsBarStyle; 17 | 18 | /// The style of the control buttons (play, pause, rewind, seek forward and backward). 19 | final ControlsBarStyle? controlsBarStyle; 20 | 21 | /// The style of the time slider. 22 | final TimeSliderStyle? timeSliderStyle; 23 | 24 | /// api.video default style. 25 | static PlayerStyle styleFromApiVideo() { 26 | const textStyle = TextStyle(color: Colors.white); 27 | final buttonStyle = TextButton.styleFrom( 28 | iconColor: Colors.white, 29 | foregroundColor: Colors.white, 30 | side: BorderSide.none, 31 | textStyle: textStyle); 32 | 33 | final settingsBarStyle = SettingsBarStyle( 34 | buttonStyle: buttonStyle, 35 | sliderTheme: SliderThemeData( 36 | activeTrackColor: Colors.white, 37 | thumbColor: Colors.white, 38 | overlayShape: SliderComponentShape.noOverlay, 39 | thumbShape: const RoundSliderThumbShape( 40 | enabledThumbRadius: 6.0, 41 | ))); 42 | 43 | final controlsBarStyle = 44 | ControlsBarStyle.styleFrom(mainControlButtonStyle: buttonStyle); 45 | 46 | final timeSliderStyle = TimeSliderStyle( 47 | sliderTheme: const SliderThemeData( 48 | activeTrackColor: ApiVideoColors.orange, 49 | inactiveTrackColor: Colors.grey, 50 | thumbColor: ApiVideoColors.orange, 51 | valueIndicatorTextStyle: textStyle)); 52 | 53 | return PlayerStyle( 54 | settingsBarStyle: settingsBarStyle, 55 | controlsBarStyle: controlsBarStyle, 56 | timeSliderStyle: timeSliderStyle); 57 | } 58 | 59 | PlayerStyle copyWith({ 60 | SettingsBarStyle? settingsBarStyle, 61 | ControlsBarStyle? controlsBarStyle, 62 | TimeSliderStyle? timeSliderStyle, 63 | }) => 64 | PlayerStyle( 65 | settingsBarStyle: settingsBarStyle ?? this.settingsBarStyle, 66 | controlsBarStyle: controlsBarStyle ?? this.controlsBarStyle, 67 | timeSliderStyle: timeSliderStyle ?? this.timeSliderStyle); 68 | 69 | /// Extracts the style of the player from the [context]. 70 | /// 71 | /// The following themes are retrieved from application theme: 72 | /// - [ThemeData.iconButtonTheme] for the settings button (volume,...). 73 | /// - [ThemeData.iconButtonTheme] for the main control buttons (play, pause, rewind). 74 | /// - [ThemeData.iconButtonTheme] for the side control buttons (seek forward and backward). 75 | /// - [ThemeData.sliderTheme] for the time slider. 76 | /// - [ThemeData.sliderTheme] for the other sliders (except the time slider). 77 | static PlayerStyle of(BuildContext context) => PlayerStyle( 78 | settingsBarStyle: SettingsBarStyle.of(context), 79 | controlsBarStyle: ControlsBarStyle.of(context), 80 | timeSliderStyle: TimeSliderStyle.of(context)); 81 | 82 | /// The default theme of the player. 83 | static PlayerStyle defaultStyle = styleFromApiVideo(); 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/style/fonts/ApiVideoIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-flutter-player/47305c3da9bc7e4e71073cb6a28c82793a048d1b/lib/src/style/fonts/ApiVideoIcons.ttf -------------------------------------------------------------------------------- /lib/src/utils/extensions/duration_extension.dart: -------------------------------------------------------------------------------- 1 | extension DurationDisplay on Duration { 2 | /// Returns a string representation of this [Duration] 3 | String toPlayerString() { 4 | String twoDigits(int n) => n.toString().padLeft(2, "0"); 5 | String twoDigitMinutes = twoDigits(inMinutes.remainder(60)); 6 | String twoDigitSeconds = twoDigits(inSeconds.remainder(60)); 7 | if (inHours > 0) { 8 | return "$inHours:$twoDigitMinutes:$twoDigitSeconds"; 9 | } else { 10 | return "$twoDigitMinutes:$twoDigitSeconds"; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/widgets/apivideo_player.dart: -------------------------------------------------------------------------------- 1 | import 'package:apivideo_player/src/apivideo_player_controller.dart'; 2 | import 'package:apivideo_player/src/style/apivideo_style.dart'; 3 | import 'package:apivideo_player/src/widgets/apivideo_player_opacity.dart'; 4 | import 'package:apivideo_player/src/widgets/apivideo_player_overlay.dart'; 5 | import 'package:apivideo_player/src/widgets/apivideo_player_video.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | /// The main player widget. 9 | /// It displays a [Stack] containing the video and a [child] widget on top. 10 | /// By default, the [child] widget is the [PlayerOverlay] inside a [TimedOpacity]. 11 | /// 12 | /// Use the [child] to display a custom overlay on top of the video. 13 | /// 14 | /// ```dart 15 | /// Widget build(BuildContext context) { 16 | /// return ApiVideoPlayer( 17 | /// controller: controller, 18 | /// ) 19 | /// } 20 | /// ``` 21 | class ApiVideoPlayer extends StatefulWidget { 22 | const ApiVideoPlayer( 23 | {super.key, 24 | required this.controller, 25 | this.fit = BoxFit.contain, 26 | this.controlsVisibilityDuration = const Duration(seconds: 4), 27 | this.style, 28 | this.child}); 29 | 30 | /// Creates a player with api.video style. 31 | factory ApiVideoPlayer.styleFromApiVideo( 32 | {Key? key, 33 | required ApiVideoPlayerController controller, 34 | Duration controlsVisibilityDuration = const Duration(seconds: 4), 35 | Widget? child}) { 36 | return ApiVideoPlayer( 37 | key: key, 38 | controller: controller, 39 | controlsVisibilityDuration: controlsVisibilityDuration, 40 | style: PlayerStyle.defaultStyle, 41 | child: child); 42 | } 43 | 44 | /// Creates a player without controls. 45 | factory ApiVideoPlayer.noControls( 46 | {Key? key, required ApiVideoPlayerController controller}) { 47 | return ApiVideoPlayer(key: key, controller: controller, child: Container()); 48 | } 49 | 50 | /// The controller for the player. 51 | final ApiVideoPlayerController controller; 52 | 53 | /// The duration to wait before hiding the controls. 54 | final Duration controlsVisibilityDuration; 55 | 56 | /// The style of the player. 57 | final PlayerStyle? style; 58 | 59 | /// The fit for the video. The overlay is not affected. 60 | /// See [BoxFit] for more details. 61 | final BoxFit fit; 62 | 63 | /// The child widget to display as an overlay on top of the video. 64 | final Widget? child; 65 | 66 | @override 67 | State createState() => _ApiVideoPlayerState(); 68 | } 69 | 70 | class _ApiVideoPlayerState extends State { 71 | late final _opacityController = 72 | TimedOpacityController(duration: widget.controlsVisibilityDuration); 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return PlayerVideo( 77 | controller: widget.controller, 78 | fit: widget.fit, 79 | child: widget.child ?? 80 | TimedOpacity( 81 | controller: _opacityController, 82 | child: PlayerOverlay( 83 | controller: widget.controller, 84 | style: widget.style ?? PlayerStyle.defaultStyle, 85 | onItemPress: () { 86 | _opacityController.showForDuration(); 87 | }))); 88 | } 89 | 90 | @override 91 | void dispose() { 92 | _opacityController.dispose(); 93 | super.dispose(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/widgets/apivideo_player_controls_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:apivideo_player/src/style/apivideo_icons.dart'; 2 | import 'package:apivideo_player/src/style/apivideo_label.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class ControlsBarController extends ChangeNotifier { 6 | ControlsBarState _state = ControlsBarState.paused; 7 | 8 | ControlsBarState get state => _state; 9 | 10 | set state(ControlsBarState state) { 11 | _state = state; 12 | notifyListeners(); 13 | } 14 | } 15 | 16 | enum ControlsBarState { 17 | playing, 18 | paused, 19 | ended; 20 | 21 | bool get isPlaying => this == ControlsBarState.playing; 22 | 23 | bool get isPaused => this == ControlsBarState.paused; 24 | 25 | bool get didEnd => this == ControlsBarState.ended; 26 | } 27 | 28 | class ControlsBar extends StatefulWidget { 29 | const ControlsBar( 30 | {super.key, 31 | required this.controller, 32 | required this.onPlay, 33 | required this.onPause, 34 | required this.onReplay, 35 | required this.onForward, 36 | required this.onBackward, 37 | this.playIcon = ApiVideoIcons.playPrimary, 38 | this.pauseIcon = ApiVideoIcons.pausePrimary, 39 | this.rewindIcon = ApiVideoIcons.replayPrimary, 40 | this.forwardIcon = Icons.forward_10_rounded, 41 | this.backwardIcon = Icons.replay_10_rounded, 42 | this.style}); 43 | 44 | final ControlsBarController controller; 45 | 46 | final VoidCallback onPlay; 47 | final VoidCallback onPause; 48 | final VoidCallback onReplay; 49 | final VoidCallback onForward; 50 | final VoidCallback onBackward; 51 | 52 | /// The main icons to display. 53 | final IconData playIcon; 54 | final IconData pauseIcon; 55 | final IconData rewindIcon; 56 | 57 | final IconData? forwardIcon; 58 | final IconData? backwardIcon; 59 | 60 | final ControlsBarStyle? style; 61 | 62 | @override 63 | State createState() => _ControlsBarState(); 64 | } 65 | 66 | class _ControlsBarState extends State { 67 | bool _isPlaying = false; 68 | bool _didEnd = false; 69 | 70 | @override 71 | void initState() { 72 | super.initState(); 73 | widget.controller.addListener(_didChangeStateValue); 74 | if (mounted) { 75 | setState(() { 76 | _isPlaying = widget.controller._state.isPlaying; 77 | _didEnd = widget.controller._state.didEnd; 78 | }); 79 | } 80 | } 81 | 82 | @override 83 | void didUpdateWidget(ControlsBar oldWidget) { 84 | super.didUpdateWidget(oldWidget); 85 | oldWidget.controller.removeListener(_didChangeStateValue); 86 | widget.controller.addListener(_didChangeStateValue); 87 | } 88 | 89 | @override 90 | void dispose() { 91 | widget.controller.removeListener(_didChangeStateValue); 92 | super.dispose(); 93 | } 94 | 95 | @override 96 | Widget build(BuildContext context) { 97 | return Center( 98 | child: Row( 99 | mainAxisSize: MainAxisSize.min, 100 | children: [ 101 | IconButton( 102 | onPressed: () { 103 | widget.onBackward(); 104 | }, 105 | iconSize: 30, 106 | icon: Icon(widget.backwardIcon, 107 | semanticLabel: ApiVideoLabels.backward), 108 | style: widget.style?.seekBackwardControlButtonStyle), 109 | buildBtnVideoControl(), 110 | IconButton( 111 | onPressed: () { 112 | widget.onForward(); 113 | }, 114 | iconSize: 30, 115 | icon: Icon(widget.forwardIcon, 116 | semanticLabel: ApiVideoLabels.forward), 117 | style: widget.style?.seekForwardControlButtonStyle), 118 | ], 119 | ), 120 | ); 121 | } 122 | 123 | Widget buildBtnVideoControl() { 124 | return _didEnd 125 | ? buildBtnReplay() 126 | : _isPlaying 127 | ? buildBtnPause() 128 | : buildBtnPlay(); 129 | } 130 | 131 | Widget buildBtnPlay() => IconButton( 132 | onPressed: () { 133 | widget.onPlay(); 134 | }, 135 | iconSize: 60, 136 | icon: Icon( 137 | widget.playIcon, 138 | semanticLabel: ApiVideoLabels.play, 139 | ), 140 | style: widget.style?.mainControlButtonStyle); 141 | 142 | Widget buildBtnPause() => IconButton( 143 | onPressed: () { 144 | widget.onPause(); 145 | }, 146 | iconSize: 60, 147 | icon: Icon( 148 | widget.pauseIcon, 149 | semanticLabel: ApiVideoLabels.pause, 150 | ), 151 | style: widget.style?.mainControlButtonStyle); 152 | 153 | Widget buildBtnReplay() => IconButton( 154 | onPressed: () { 155 | widget.onReplay(); 156 | }, 157 | iconSize: 60, 158 | icon: Icon( 159 | widget.rewindIcon, 160 | semanticLabel: ApiVideoLabels.replay, 161 | ), 162 | style: widget.style?.mainControlButtonStyle); 163 | 164 | _didChangeStateValue() { 165 | if (_isPlaying == widget.controller._state.isPlaying && 166 | _didEnd == widget.controller._state.didEnd) { 167 | return; 168 | } 169 | if (mounted) { 170 | setState(() { 171 | _isPlaying = widget.controller._state.isPlaying; 172 | _didEnd = widget.controller._state.didEnd; 173 | }); 174 | } 175 | } 176 | } 177 | 178 | class ControlsBarStyle { 179 | const ControlsBarStyle( 180 | {this.mainControlButtonStyle, 181 | this.seekForwardControlButtonStyle, 182 | this.seekBackwardControlButtonStyle}); 183 | 184 | final ButtonStyle? mainControlButtonStyle; 185 | final ButtonStyle? seekForwardControlButtonStyle; 186 | final ButtonStyle? seekBackwardControlButtonStyle; 187 | 188 | /// Applies the style to all buttons 189 | /// 190 | /// If [sideControlButtonStyle] is null, it will be the same as [mainControlButtonStyle]. 191 | static ControlsBarStyle styleFrom( 192 | {ButtonStyle? mainControlButtonStyle, 193 | ButtonStyle? sideControlButtonStyle}) { 194 | sideControlButtonStyle ??= mainControlButtonStyle; 195 | 196 | return ControlsBarStyle( 197 | mainControlButtonStyle: mainControlButtonStyle, 198 | seekForwardControlButtonStyle: mainControlButtonStyle, 199 | seekBackwardControlButtonStyle: mainControlButtonStyle); 200 | } 201 | 202 | ControlsBarStyle copyWith({ 203 | ButtonStyle? mainControlButtonStyle, 204 | ButtonStyle? seekForwardControlButtonStyle, 205 | ButtonStyle? seekBackwardControlButtonStyle, 206 | }) { 207 | return ControlsBarStyle( 208 | mainControlButtonStyle: 209 | mainControlButtonStyle ?? this.mainControlButtonStyle, 210 | seekForwardControlButtonStyle: 211 | seekForwardControlButtonStyle ?? this.seekForwardControlButtonStyle, 212 | seekBackwardControlButtonStyle: seekBackwardControlButtonStyle ?? 213 | this.seekBackwardControlButtonStyle); 214 | } 215 | 216 | /// Applies the [Theme.iconButtonTheme] to all buttons 217 | static ControlsBarStyle of(BuildContext context) { 218 | final iconButtonTheme = Theme.of(context).iconButtonTheme; 219 | 220 | return styleFrom(mainControlButtonStyle: iconButtonTheme.style); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lib/src/widgets/apivideo_player_opacity.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | 5 | class TimedOpacityController extends ChangeNotifier { 6 | TimedOpacityController({ 7 | this.duration = const Duration(seconds: 4), 8 | }); 9 | 10 | /// The duration to wait before hiding the overlay. 11 | final Duration duration; 12 | bool _isVisible = false; 13 | 14 | Timer? _opacityTimer; 15 | 16 | @override 17 | void dispose() { 18 | _opacityTimer?.cancel(); 19 | _opacityTimer = null; 20 | super.dispose(); 21 | } 22 | 23 | void showForDuration() { 24 | if (!_isVisible) { 25 | show(); 26 | } 27 | _opacityTimer?.cancel(); 28 | _opacityTimer = Timer(duration, hide); 29 | } 30 | 31 | void show() { 32 | _isVisible = true; 33 | notifyListeners(); 34 | } 35 | 36 | void hide() { 37 | _opacityTimer?.cancel(); 38 | _opacityTimer = null; 39 | 40 | _isVisible = false; 41 | notifyListeners(); 42 | } 43 | } 44 | 45 | /// A widget that automatically hides its [child] after a given duration. 46 | class TimedOpacity extends StatefulWidget { 47 | const TimedOpacity( 48 | {super.key, required this.controller, required this.child}); 49 | 50 | /// The controller for the opacity. 51 | final TimedOpacityController controller; 52 | 53 | /// The child widget 54 | final Widget child; 55 | 56 | @override 57 | State createState() => _TimedOpacityState(); 58 | } 59 | 60 | class _TimedOpacityState extends State { 61 | bool _isVisible = false; 62 | 63 | @override 64 | initState() { 65 | super.initState(); 66 | if (mounted) { 67 | setState(() { 68 | _isVisible = widget.controller._isVisible; 69 | }); 70 | } 71 | widget.controller.addListener(_didChangeOpacityValue); 72 | } 73 | 74 | @override 75 | void dispose() { 76 | widget.controller.removeListener(_didChangeOpacityValue); 77 | super.dispose(); 78 | } 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | return AnimatedOpacity( 83 | // If the widget is visible, animate to 0.0 (invisible). 84 | // If the widget is hidden, animate to 1.0 (fully visible). 85 | opacity: _isVisible ? 1.0 : 0.0, 86 | duration: const Duration(milliseconds: 500), 87 | child: MouseRegion( 88 | onEnter: (_) => widget.controller.show(), 89 | onExit: (_) => widget.controller.showForDuration(), 90 | child: GestureDetector( 91 | behavior: HitTestBehavior.opaque, 92 | onTap: () { 93 | widget.controller.showForDuration(); 94 | }, 95 | child: _isVisible ? widget.child : Container(), 96 | ), 97 | )); 98 | } 99 | 100 | void _didChangeOpacityValue() { 101 | if (_isVisible == widget.controller._isVisible) { 102 | return; 103 | } 104 | if (mounted) { 105 | setState(() { 106 | _isVisible = widget.controller._isVisible; 107 | }); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/src/widgets/apivideo_player_overlay.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:apivideo_player/apivideo_player.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:pointer_interceptor/pointer_interceptor.dart'; 6 | 7 | /// The overlay of the video player. 8 | /// It displays the controls, time slider and the action bar. 9 | class PlayerOverlay extends StatefulWidget { 10 | const PlayerOverlay( 11 | {super.key, required this.controller, this.style, this.onItemPress}); 12 | 13 | /// The controller for the player. 14 | final ApiVideoPlayerController controller; 15 | 16 | /// The style of the player. 17 | final PlayerStyle? style; 18 | 19 | /// The callback to be called when an item (play, pause,...) is clicked (used to show the overlay). 20 | final VoidCallback? onItemPress; 21 | 22 | @override 23 | State createState() => _PlayerOverlayState(); 24 | } 25 | 26 | class _PlayerOverlayState extends State 27 | with TickerProviderStateMixin { 28 | final _timeSliderController = TimeSliderController(); 29 | final _controlsBarController = ControlsBarController(); 30 | final _settingsBarController = SettingsBarController(); 31 | 32 | Timer? _timeSliderTimer; 33 | 34 | late final ApiVideoPlayerControllerEventsListener _listener = 35 | ApiVideoPlayerControllerEventsListener( 36 | onReady: () async { 37 | _updateTimes(); 38 | }, 39 | onPlay: () async { 40 | _onPlay(); 41 | }, 42 | onPause: () async { 43 | _onPause(); 44 | }, 45 | onSeek: () async { 46 | _updateCurrentTime(); 47 | }, 48 | onSeekStarted: () async { 49 | if (_controlsBarController.state.didEnd) { 50 | _controlsBarController.state = ControlsBarState.paused; 51 | } 52 | }, 53 | onEnd: () async { 54 | _stopRemainingTimeUpdates(); 55 | _controlsBarController.state = ControlsBarState.ended; 56 | }, 57 | ); 58 | 59 | @override 60 | void initState() { 61 | super.initState(); 62 | widget.controller.addListener(_listener); 63 | // In case controller is already created 64 | widget.controller.isCreated.then((bool isCreated) async => { 65 | if (isCreated) 66 | { 67 | _updateTimes(), 68 | _updateVolume(), 69 | _updateMuted(), 70 | widget.controller.isPlaying.then((isPlaying) => { 71 | if (isPlaying) {_onPlay()} 72 | }) 73 | } 74 | }); 75 | } 76 | 77 | @override 78 | void didUpdateWidget(PlayerOverlay oldWidget) { 79 | super.didUpdateWidget(oldWidget); 80 | oldWidget.controller.removeListener(_listener); 81 | widget.controller.addListener(_listener); 82 | } 83 | 84 | @override 85 | void dispose() { 86 | _stopRemainingTimeUpdates(); 87 | widget.controller.removeListener(_listener); 88 | 89 | _timeSliderController.dispose(); 90 | _controlsBarController.dispose(); 91 | _settingsBarController.dispose(); 92 | 93 | super.dispose(); 94 | } 95 | 96 | @override 97 | Widget build(BuildContext context) => PointerInterceptor( 98 | child: buildOverlay(), 99 | ); 100 | 101 | Widget buildOverlay() => Stack( 102 | children: [ 103 | Positioned( 104 | top: 0, 105 | right: 0, 106 | child: SettingsBar( 107 | controller: _settingsBarController, 108 | onVolumeChanged: (volume) { 109 | widget.controller.setVolume(volume); 110 | if (widget.onItemPress != null) { 111 | widget.onItemPress!(); 112 | } 113 | }, 114 | onToggleMute: () async { 115 | final isMuted = await widget.controller.isMuted; 116 | widget.controller.setIsMuted(!isMuted); 117 | if (widget.onItemPress != null) { 118 | widget.onItemPress!(); 119 | } 120 | }, 121 | onSpeedRateChanged: (speed) { 122 | widget.controller.setSpeedRate(speed); 123 | if (widget.onItemPress != null) { 124 | widget.onItemPress!(); 125 | } 126 | }, 127 | style: widget.style?.settingsBarStyle)), 128 | Center( 129 | child: ControlsBar( 130 | controller: _controlsBarController, 131 | onBackward: () { 132 | widget.controller.seek(const Duration(seconds: -10)); 133 | if (widget.onItemPress != null) { 134 | widget.onItemPress!(); 135 | } 136 | }, 137 | onForward: () { 138 | widget.controller.seek(const Duration(seconds: 10)); 139 | if (widget.onItemPress != null) { 140 | widget.onItemPress!(); 141 | } 142 | }, 143 | onPause: () { 144 | widget.controller.pause(); 145 | _onPause(); 146 | if (widget.onItemPress != null) { 147 | widget.onItemPress!(); 148 | } 149 | }, 150 | onPlay: () { 151 | widget.controller.play(); 152 | _onPlay(); 153 | if (widget.onItemPress != null) { 154 | widget.onItemPress!(); 155 | } 156 | }, 157 | onReplay: () { 158 | widget.controller.setCurrentTime(const Duration(seconds: 0)); 159 | widget.controller.play(); 160 | if (widget.onItemPress != null) { 161 | widget.onItemPress!(); 162 | } 163 | }, 164 | style: widget.style?.controlsBarStyle, 165 | ), 166 | ), 167 | Positioned( 168 | bottom: 0, 169 | right: 0, 170 | left: 0, 171 | child: TimeSlider( 172 | controller: _timeSliderController, 173 | style: widget.style?.timeSliderStyle, 174 | onChanged: (Duration value) { 175 | widget.controller.setCurrentTime(value); 176 | if (widget.onItemPress != null) { 177 | widget.onItemPress!(); 178 | } 179 | }, 180 | )), 181 | ], 182 | ); 183 | 184 | void _onPlay() { 185 | _startRemainingTimeUpdates(); 186 | if (mounted) { 187 | _controlsBarController.state = ControlsBarState.playing; 188 | } 189 | } 190 | 191 | void _onPause() { 192 | _stopRemainingTimeUpdates(); 193 | if (mounted) { 194 | _controlsBarController.state = ControlsBarState.paused; 195 | } 196 | } 197 | 198 | void _startRemainingTimeUpdates() { 199 | _timeSliderTimer?.cancel(); 200 | _timeSliderTimer = Timer.periodic( 201 | const Duration(milliseconds: 100), 202 | (timer) async { 203 | final isLive = await widget.controller.isLive; 204 | if (isLive) { 205 | _updateTimes(); 206 | } else { 207 | _updateCurrentTime(); 208 | } 209 | }, 210 | ); 211 | } 212 | 213 | void _stopRemainingTimeUpdates() { 214 | _timeSliderTimer?.cancel(); 215 | _timeSliderTimer = null; 216 | } 217 | 218 | void _updateTimes() async { 219 | Duration currentTime = await widget.controller.currentTime; 220 | Duration duration = await widget.controller.duration; 221 | if (mounted) { 222 | _timeSliderController.setTime(currentTime, duration); 223 | } 224 | } 225 | 226 | void _updateCurrentTime() async { 227 | Duration currentTime = await widget.controller.currentTime; 228 | if (mounted) { 229 | _timeSliderController.currentTime = currentTime; 230 | } 231 | } 232 | 233 | void _updateVolume() async { 234 | double volume = await widget.controller.volume; 235 | if (mounted) { 236 | _settingsBarController.volume = volume; 237 | } 238 | } 239 | 240 | void _updateMuted() async { 241 | bool isMuted = await widget.controller.isMuted; 242 | if (mounted) { 243 | _settingsBarController.isMuted = isMuted; 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /lib/src/widgets/apivideo_player_settings_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:apivideo_player/src/widgets/common/apivideo_player_multi_text_button.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'apivideo_player_volume_slider.dart'; 6 | 7 | class SettingsBarController { 8 | final volumeController = VolumeSliderController(); 9 | 10 | double get volume => volumeController.volume; 11 | 12 | set volume(double newVolume) { 13 | volumeController.volume = newVolume; 14 | } 15 | 16 | bool get isMuted => volumeController.isMuted; 17 | 18 | set isMuted(bool newIsMuted) { 19 | volumeController.isMuted = newIsMuted; 20 | } 21 | 22 | void dispose() { 23 | volumeController.dispose(); 24 | } 25 | } 26 | 27 | class SettingsBar extends StatefulWidget { 28 | const SettingsBar( 29 | {super.key, 30 | required this.controller, 31 | this.onToggleMute, 32 | this.onVolumeChanged, 33 | this.onSpeedRateChanged, 34 | this.style}); 35 | 36 | final SettingsBarController controller; 37 | 38 | final VoidCallback? onToggleMute; 39 | final ValueChanged? onVolumeChanged; 40 | 41 | final ValueChanged? onSpeedRateChanged; 42 | 43 | final SettingsBarStyle? style; 44 | 45 | @override 46 | State createState() => _SettingsBarState(); 47 | } 48 | 49 | class _SettingsBarState extends State { 50 | @override 51 | Widget build(BuildContext context) { 52 | return Row( 53 | mainAxisAlignment: MainAxisAlignment.end, 54 | crossAxisAlignment: CrossAxisAlignment.center, 55 | children: [ 56 | kIsWeb 57 | ? VolumeSlider( 58 | controller: widget.controller.volumeController, 59 | onVolumeChanged: (volume) { 60 | if (widget.onVolumeChanged != null) { 61 | widget.onVolumeChanged!(volume); 62 | } 63 | }, 64 | onToggleMute: () { 65 | if (widget.onToggleMute != null) { 66 | widget.onToggleMute!(); 67 | } 68 | }, 69 | style: VolumeSliderStyle( 70 | buttonStyle: widget.style?.buttonStyle, 71 | sliderTheme: widget.style?.sliderTheme, 72 | )) 73 | : Container(), 74 | MultiTextButton( 75 | keysValues: const { 76 | '0.5x': 0.5, 77 | '1.0x': 1.0, 78 | '1.2x': 1.2, 79 | '1.5x': 1.5, 80 | '2.0x': 2.0, 81 | }, 82 | defaultKey: "1.0x", 83 | onValueChanged: (speed) { 84 | if (widget.onSpeedRateChanged != null) { 85 | widget.onSpeedRateChanged!(speed); 86 | } 87 | }, 88 | size: 17, 89 | style: widget.style?.buttonStyle, 90 | ) 91 | ]); 92 | } 93 | } 94 | 95 | class SettingsBarStyle { 96 | const SettingsBarStyle.raw({this.buttonStyle, required this.sliderTheme}); 97 | 98 | final ButtonStyle? buttonStyle; 99 | final SliderThemeData sliderTheme; 100 | 101 | factory SettingsBarStyle({ 102 | ButtonStyle? buttonStyle, 103 | SliderThemeData? sliderTheme, 104 | }) { 105 | sliderTheme ??= const SliderThemeData(); 106 | 107 | return SettingsBarStyle.raw( 108 | buttonStyle: buttonStyle, sliderTheme: sliderTheme); 109 | } 110 | 111 | SettingsBarStyle copyWith({ 112 | ButtonStyle? buttonStyle, 113 | SliderThemeData? sliderTheme, 114 | }) { 115 | return SettingsBarStyle.raw( 116 | buttonStyle: buttonStyle ?? this.buttonStyle, 117 | sliderTheme: sliderTheme ?? this.sliderTheme); 118 | } 119 | 120 | /// Applies the [Theme.iconButtonTheme] to all buttons and the 121 | /// [Theme.sliderTheme] to the slider. 122 | static SettingsBarStyle of(BuildContext context) { 123 | final iconButtonTheme = Theme.of(context).iconButtonTheme; 124 | final sliderTheme = Theme.of(context).sliderTheme; 125 | 126 | return SettingsBarStyle( 127 | buttonStyle: iconButtonTheme.style, sliderTheme: sliderTheme); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/src/widgets/apivideo_player_time_slider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:apivideo_player/src/utils/extensions/duration_extension.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class TimeSliderValue { 7 | final Duration currentTime; 8 | final Duration duration; 9 | 10 | const TimeSliderValue({ 11 | this.currentTime = Duration.zero, 12 | this.duration = Duration.zero, 13 | }); 14 | 15 | /// Creates a copy of this value but with the given fields replaced with the new values. 16 | TimeSliderValue copyWith({Duration? currentTime, Duration? duration}) { 17 | return TimeSliderValue( 18 | currentTime: currentTime ?? this.currentTime, 19 | duration: duration ?? this.duration); 20 | } 21 | } 22 | 23 | class TimeSliderController extends ValueNotifier { 24 | TimeSliderController() : super(const TimeSliderValue()); 25 | 26 | Duration get currentTime => value.currentTime; 27 | 28 | set currentTime(Duration newCurrentTime) { 29 | value = value.copyWith( 30 | currentTime: newCurrentTime, 31 | ); 32 | } 33 | 34 | Duration get duration => value.duration; 35 | 36 | set duration(Duration newDuration) { 37 | value = value.copyWith( 38 | duration: newDuration, 39 | ); 40 | } 41 | 42 | setTime(Duration newCurrentTime, Duration newDuration) { 43 | value = TimeSliderValue(currentTime: newCurrentTime, duration: newDuration); 44 | } 45 | } 46 | 47 | class TimeSlider extends StatefulWidget { 48 | const TimeSlider.raw( 49 | {super.key, 50 | required this.controller, 51 | required this.style, 52 | this.onChanged}); 53 | 54 | final TimeSliderController controller; 55 | 56 | final TimeSliderStyle style; 57 | 58 | final ValueChanged? onChanged; 59 | 60 | factory TimeSlider({ 61 | required TimeSliderController controller, 62 | TimeSliderStyle? style, 63 | ValueChanged? onChanged, 64 | }) { 65 | style ??= TimeSliderStyle(); 66 | return TimeSlider.raw( 67 | controller: controller, 68 | style: style, 69 | onChanged: onChanged, 70 | ); 71 | } 72 | 73 | @override 74 | State createState() => _TimeSliderState(); 75 | } 76 | 77 | class _TimeSliderState extends State { 78 | Duration _currentTime = Duration.zero; 79 | Duration _duration = Duration.zero; 80 | 81 | @override 82 | void initState() { 83 | super.initState(); 84 | widget.controller.addListener(_didChangeTimeSliderValue); 85 | if (mounted) { 86 | setState(() { 87 | _currentTime = widget.controller.currentTime; 88 | _duration = widget.controller.duration; 89 | }); 90 | } 91 | } 92 | 93 | @override 94 | void dispose() { 95 | widget.controller.removeListener(_didChangeTimeSliderValue); 96 | super.dispose(); 97 | } 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | return Row( 102 | mainAxisAlignment: MainAxisAlignment.center, 103 | crossAxisAlignment: CrossAxisAlignment.center, 104 | children: [ 105 | Expanded( 106 | flex: 4, 107 | child: SliderTheme( 108 | data: widget.style.sliderTheme, 109 | child: Slider( 110 | value: max( 111 | 0, 112 | min( 113 | widget.controller.currentTime.inMilliseconds, 114 | widget.controller.duration.inMilliseconds, 115 | )).toDouble(), 116 | // Ensure that the slider doesn't go over the duration or under 0.0 117 | min: 0.0, 118 | max: widget.controller.duration.inMilliseconds.toDouble(), 119 | onChanged: (value) { 120 | final Duration currentTime = 121 | Duration(milliseconds: value.toInt()); 122 | if (currentTime == widget.controller.currentTime) { 123 | return; 124 | } 125 | widget.controller.currentTime = currentTime; 126 | if (widget.onChanged != null) { 127 | widget.onChanged!(currentTime); 128 | } 129 | }, 130 | ))), 131 | Expanded( 132 | flex: 1, 133 | child: Text(_getRemainingTime().toPlayerString(), 134 | maxLines: 1, 135 | softWrap: false, 136 | textAlign: TextAlign.center, 137 | style: widget.style.sliderTheme.valueIndicatorTextStyle)), 138 | ], 139 | ); 140 | } 141 | 142 | Duration _getRemainingTime() { 143 | final remainingTime = _duration - _currentTime; 144 | if (remainingTime.isNegative) { 145 | return Duration.zero; 146 | } else { 147 | return remainingTime; 148 | } 149 | } 150 | 151 | void _didChangeTimeSliderValue() { 152 | if (mounted) { 153 | setState(() { 154 | _currentTime = widget.controller.currentTime; 155 | _duration = widget.controller.duration; 156 | }); 157 | } 158 | } 159 | } 160 | 161 | class TimeSliderStyle { 162 | const TimeSliderStyle.raw({required this.sliderTheme}); 163 | 164 | final SliderThemeData sliderTheme; 165 | 166 | factory TimeSliderStyle({SliderThemeData? sliderTheme}) { 167 | sliderTheme ??= const SliderThemeData(); 168 | return TimeSliderStyle.raw(sliderTheme: sliderTheme); 169 | } 170 | 171 | static TimeSliderStyle of(BuildContext context) { 172 | final SliderThemeData sliderTheme = SliderTheme.of(context); 173 | return TimeSliderStyle(sliderTheme: sliderTheme); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /lib/src/widgets/apivideo_player_video.dart: -------------------------------------------------------------------------------- 1 | import 'package:apivideo_player/src/apivideo_player_controller.dart'; 2 | import 'package:apivideo_player/src/platform/apivideo_player_platform_interface.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | ApiVideoPlayerPlatform get _playerPlatform { 6 | return ApiVideoPlayerPlatform.instance; 7 | } 8 | 9 | /// The [Stack] containing the video and a [child] on top of the video. 10 | class PlayerVideo extends StatefulWidget { 11 | const PlayerVideo( 12 | {super.key, 13 | required this.controller, 14 | this.fit = BoxFit.contain, 15 | this.child}); 16 | 17 | /// The controller for the player. 18 | final ApiVideoPlayerController controller; 19 | 20 | /// The [BoxFit] for the video. The [child] is scale to the video box. 21 | final BoxFit fit; 22 | 23 | /// The child widget is displayed on top of the video. 24 | /// The purpose of this widget is to display an overlay on top of the video. 25 | /// It is scaled to the video size. 26 | /// By default, the child is an empty [Container]. 27 | final Widget? child; 28 | 29 | @override 30 | State createState() => _PlayerVideoState(); 31 | } 32 | 33 | class _PlayerVideoState extends State { 34 | _PlayerVideoState() { 35 | _widgetListener = 36 | ApiVideoPlayerControllerWidgetListener(onTextureReady: () async { 37 | _updateTextureId(); 38 | }); 39 | _eventsListener = ApiVideoPlayerControllerEventsListener(onReady: () async { 40 | _updateAspectRatio(); 41 | }); 42 | } 43 | 44 | late ApiVideoPlayerControllerEventsListener _eventsListener; 45 | late ApiVideoPlayerControllerWidgetListener _widgetListener; 46 | late int _textureId = widget.controller.textureId; 47 | 48 | double _aspectRatio = 1.77; 49 | Size _size = const Size(1280, 720); 50 | 51 | @override 52 | void initState() { 53 | super.initState(); 54 | widget.controller.addWidgetListener(_widgetListener); 55 | widget.controller.addListener(_eventsListener); 56 | // In case controller is already created 57 | widget.controller.isCreated.then((bool isCreated) => { 58 | if (isCreated) {_updateAspectRatio()} 59 | }); 60 | } 61 | 62 | @override 63 | void didUpdateWidget(PlayerVideo oldWidget) { 64 | super.didUpdateWidget(oldWidget); 65 | oldWidget.controller.removeWidgetListener(_widgetListener); 66 | oldWidget.controller.removeListener(_eventsListener); 67 | widget.controller.addWidgetListener(_widgetListener); 68 | widget.controller.addListener(_eventsListener); 69 | } 70 | 71 | @override 72 | void dispose() { 73 | widget.controller.removeWidgetListener(_widgetListener); 74 | widget.controller.removeListener(_eventsListener); 75 | super.dispose(); 76 | } 77 | 78 | @override 79 | Widget build(BuildContext context) { 80 | return _textureId == ApiVideoPlayerController.kUninitializedTextureId 81 | ? Container() 82 | : _buildPlayer(); 83 | } 84 | 85 | Widget _buildPlayer() => LayoutBuilder( 86 | builder: (BuildContext context, BoxConstraints constraints) { 87 | return Stack(alignment: Alignment.center, children: [ 88 | // See https://github.com/flutter/flutter/issues/17287 89 | SizedBox( 90 | width: constraints.maxWidth, 91 | height: constraints.maxHeight, 92 | child: FittedBox( 93 | fit: widget.fit, 94 | clipBehavior: Clip.hardEdge, 95 | child: Center( 96 | child: SizedBox( 97 | width: _size.width, 98 | height: _size.height, 99 | child: _playerPlatform.buildView(_textureId))))), 100 | _buildFittedPlayerOverlay(constraints) 101 | ]); 102 | }); 103 | 104 | Widget _buildFittedPlayerOverlay(BoxConstraints constraints) { 105 | final fittedSize = applyBoxFit(widget.fit, _size, constraints.biggest); 106 | return SizedBox( 107 | width: fittedSize.destination.width, 108 | height: fittedSize.destination.height, 109 | child: widget.child ?? Container()); 110 | } 111 | 112 | void _updateAspectRatio() async { 113 | final newSize = await widget.controller.videoSize ?? const Size(1280, 720); 114 | final double newAspectRatio = newSize.aspectRatio; 115 | if ((newAspectRatio != _aspectRatio) || (newSize != _size)) { 116 | if (mounted) { 117 | setState(() { 118 | _size = newSize; 119 | _aspectRatio = newAspectRatio; 120 | }); 121 | } 122 | } 123 | } 124 | 125 | void _updateTextureId() async { 126 | final int newTextureId = widget.controller.textureId; 127 | if (newTextureId != _textureId) { 128 | if (mounted) { 129 | setState(() { 130 | _textureId = newTextureId; 131 | }); 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/src/widgets/apivideo_player_volume_slider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class VolumeSliderController extends ChangeNotifier { 4 | double _volume = 1.0; 5 | 6 | double get volume => _volume; 7 | 8 | set volume(double newVolume) { 9 | _volume = newVolume; 10 | notifyListeners(); 11 | } 12 | 13 | bool _isMuted = false; 14 | 15 | bool get isMuted => _isMuted; 16 | 17 | set isMuted(bool newIsMuted) { 18 | _isMuted = newIsMuted; 19 | notifyListeners(); 20 | } 21 | } 22 | 23 | class VolumeSlider extends StatefulWidget { 24 | const VolumeSlider.raw({ 25 | super.key, 26 | required this.controller, 27 | required this.onVolumeChanged, 28 | required this.onToggleMute, 29 | required this.style, 30 | }); 31 | 32 | final VolumeSliderController controller; 33 | 34 | final Function(double) onVolumeChanged; 35 | final VoidCallback onToggleMute; 36 | 37 | final VolumeSliderStyle style; 38 | 39 | factory VolumeSlider({ 40 | required VolumeSliderController controller, 41 | required Function(double) onVolumeChanged, 42 | required VoidCallback onToggleMute, 43 | VolumeSliderStyle? style, 44 | }) { 45 | style ??= VolumeSliderStyle(); 46 | 47 | return VolumeSlider.raw( 48 | controller: controller, 49 | onVolumeChanged: onVolumeChanged, 50 | onToggleMute: onToggleMute, 51 | style: style, 52 | ); 53 | } 54 | 55 | @override 56 | State createState() => _VolumeSliderState(); 57 | } 58 | 59 | class _VolumeSliderState extends State 60 | with TickerProviderStateMixin { 61 | late AnimationController expandController; 62 | late Animation animation; 63 | 64 | double _volume = 1.0; 65 | bool _isMuted = false; 66 | 67 | void _animateExpand({required bool open}) { 68 | if (open) { 69 | expandController.forward(); 70 | } else { 71 | expandController.animateBack(0, duration: const Duration(seconds: 1)); 72 | } 73 | } 74 | 75 | @override 76 | void initState() { 77 | super.initState(); 78 | widget.controller.addListener(_didChangeVolumeSliderValue); 79 | if (mounted) { 80 | setState(() { 81 | _isMuted = widget.controller.isMuted; 82 | _volume = widget.controller.volume; 83 | }); 84 | } 85 | expandController = AnimationController( 86 | duration: const Duration(seconds: 1), 87 | vsync: this, 88 | ); 89 | animation = CurvedAnimation( 90 | parent: expandController, 91 | curve: Curves.fastLinearToSlowEaseIn, 92 | ); 93 | } 94 | 95 | @override 96 | void didUpdateWidget(VolumeSlider oldWidget) { 97 | super.didUpdateWidget(oldWidget); 98 | oldWidget.controller.removeListener(_didChangeVolumeSliderValue); 99 | widget.controller.addListener(_didChangeVolumeSliderValue); 100 | } 101 | 102 | @override 103 | void dispose() { 104 | widget.controller.removeListener(_didChangeVolumeSliderValue); 105 | expandController.dispose(); 106 | super.dispose(); 107 | } 108 | 109 | @override 110 | Widget build(BuildContext context) { 111 | return MouseRegion( 112 | onEnter: (_) => _animateExpand(open: true), 113 | onExit: (_) => _animateExpand(open: false), 114 | child: Row( 115 | mainAxisAlignment: MainAxisAlignment.end, 116 | children: [ 117 | IconButton( 118 | style: widget.style.buttonStyle, 119 | icon: Icon( 120 | _volume <= 0 || _isMuted ? Icons.volume_off : Icons.volume_up, 121 | size: 18, 122 | ), 123 | onPressed: () { 124 | widget.controller.isMuted = !_isMuted; 125 | widget.onToggleMute(); 126 | }), 127 | SizeTransition( 128 | sizeFactor: animation, 129 | axis: Axis.horizontal, 130 | child: SizedBox( 131 | height: 30.0, 132 | width: 80.0, 133 | child: SliderTheme( 134 | data: widget.style.sliderTheme.copyWith(trackHeight: 2.0), 135 | child: Slider( 136 | value: _isMuted ? 0 : _volume, 137 | onChanged: (value) { 138 | if (_isMuted) { 139 | widget.controller.isMuted = false; 140 | } 141 | widget.controller.volume = value; 142 | widget.onVolumeChanged(value); 143 | }, 144 | ), 145 | ), 146 | ), 147 | ), 148 | ], 149 | ), 150 | ); 151 | } 152 | 153 | void _didChangeVolumeSliderValue() { 154 | if (_isMuted != widget.controller.isMuted || 155 | _volume != widget.controller.volume) { 156 | setState(() { 157 | _isMuted = widget.controller.isMuted; 158 | _volume = widget.controller.volume; 159 | }); 160 | } 161 | } 162 | } 163 | 164 | class VolumeSliderStyle { 165 | const VolumeSliderStyle.raw({ 166 | this.buttonStyle, 167 | required this.sliderTheme, 168 | }); 169 | 170 | final ButtonStyle? buttonStyle; 171 | final SliderThemeData sliderTheme; 172 | 173 | factory VolumeSliderStyle({ 174 | ButtonStyle? buttonStyle, 175 | SliderThemeData? sliderTheme, 176 | }) { 177 | sliderTheme ??= const SliderThemeData(); 178 | 179 | return VolumeSliderStyle.raw( 180 | buttonStyle: buttonStyle, 181 | sliderTheme: sliderTheme, 182 | ); 183 | } 184 | 185 | VolumeSliderStyle copyWith({ 186 | ButtonStyle? buttonStyle, 187 | SliderThemeData? sliderTheme, 188 | }) => 189 | VolumeSliderStyle.raw( 190 | buttonStyle: buttonStyle ?? this.buttonStyle, 191 | sliderTheme: sliderTheme ?? this.sliderTheme, 192 | ); 193 | } 194 | -------------------------------------------------------------------------------- /lib/src/widgets/common/apivideo_player_multi_text_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MultiTextButton extends StatefulWidget { 4 | MultiTextButton({ 5 | super.key, 6 | required this.keysValues, 7 | required this.onValueChanged, 8 | this.defaultKey, 9 | this.size = 15, 10 | this.style, 11 | }) : assert(keysValues.isNotEmpty), 12 | assert(defaultKey == null || keysValues.containsKey(defaultKey)); 13 | 14 | final Map keysValues; 15 | 16 | final String? defaultKey; 17 | 18 | final ValueChanged onValueChanged; 19 | 20 | final double size; 21 | 22 | final ButtonStyle? style; 23 | 24 | @override 25 | State createState() => _MultiTextButtonState(); 26 | } 27 | 28 | class _MultiTextButtonState extends State { 29 | late String _selectedKey = widget.defaultKey ?? widget.keysValues.keys.first; 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return TextButton.icon( 34 | style: widget.style, 35 | onPressed: () { 36 | final position = 37 | widget.keysValues.keys.toList().indexOf(_selectedKey); 38 | final newKey = widget.keysValues.keys 39 | .elementAt((position + 1) % widget.keysValues.keys.length); 40 | if (mounted) { 41 | setState(() { 42 | _selectedKey = newKey; 43 | }); 44 | } 45 | widget.onValueChanged(widget.keysValues[newKey]); 46 | }, 47 | icon: Icon(Icons.speed, size: widget.size), 48 | label: Text(_selectedKey)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: apivideo_player 2 | description: The official Flutter player for api.video for iOS, Android and Web. 3 | version: 1.4.0 4 | repository: https://github.com/apivideo/api.video-flutter-player 5 | issue_tracker: https://github.com/apivideo/api.video-flutter-player/issues 6 | homepage: https://api.video 7 | 8 | environment: 9 | sdk: "^3.0.0" 10 | flutter: ">=2.5.0" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | flutter_web_plugins: 16 | sdk: flutter 17 | js: ^0.7.0 18 | json_annotation: ^4.8.1 19 | plugin_platform_interface: ^2.1.5 20 | pointer_interceptor: ^0.10.1+1 21 | meta: ^1.12.0 22 | 23 | dev_dependencies: 24 | build_runner: ^2.4.11 25 | flutter_lints: ^4.0.0 26 | flutter_test: 27 | sdk: flutter 28 | json_serializable: ^6.7.1 29 | 30 | # For information on the generic Dart part of this file, see the 31 | # following page: https://dart.dev/tools/pub/pubspec 32 | # The following section is specific to Flutter packages. 33 | flutter: 34 | # This section identifies this Flutter project as a plugin project. 35 | # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) 36 | # which should be registered in the plugin registry. This is required for 37 | # using method channels. 38 | # The Android 'package' specifies package in which the registered class is. 39 | # This is required for using method channels on Android. 40 | # The 'ffiPlugin' specifies that native code should be built and bundled. 41 | # This is required for using `dart:ffi`. 42 | # All these are used by the tooling to maintain consistency when 43 | # adding or updating assets for this project. 44 | plugin: 45 | implements: apivideo_player 46 | platforms: 47 | android: 48 | dartPluginClass: ApiVideoMobilePlayer 49 | package: video.api.flutter.player 50 | pluginClass: ApiVideoPlayerPlugin 51 | ios: 52 | dartPluginClass: ApiVideoMobilePlayer 53 | pluginClass: ApiVideoPlayerPlugin 54 | web: 55 | pluginClass: ApiVideoPlayerPlugin 56 | fileName: src/platform/apivideo_player_web.dart 57 | fonts: 58 | - family: ApiVideoIcons 59 | fonts: 60 | - asset: lib/src/style/fonts/ApiVideoIcons.ttf 61 | # To add assets to your plugin package, add an assets section, like this: 62 | # assets: 63 | # - images/a_dot_burr.jpeg 64 | # - images/a_dot_ham.jpeg 65 | # 66 | # For details regarding assets in packages, see 67 | # https://flutter.dev/assets-and-images/#from-packages 68 | # 69 | # An image asset can refer to one or more resolution-specific "variants", see 70 | # https://flutter.dev/assets-and-images/#resolution-aware 71 | # To add custom fonts to your plugin package, add a fonts section here, 72 | # in this "flutter" section. Each entry in this list should have a 73 | # "family" key with the font family name, and a "fonts" key with a 74 | # list giving the asset and other descriptors for the font. For 75 | # example: 76 | # fonts: 77 | # - family: Schyler 78 | # fonts: 79 | # - asset: fonts/Schyler-Regular.ttf 80 | # - asset: fonts/Schyler-Italic.ttf 81 | # style: italic 82 | # - family: Trajan Pro 83 | # fonts: 84 | # - asset: fonts/TrajanPro.ttf 85 | # - asset: fonts/TrajanPro_Bold.ttf 86 | # weight: 700 87 | # 88 | # For details regarding fonts in packages, see 89 | # https://flutter.dev/custom-fonts/#from-packages 90 | -------------------------------------------------------------------------------- /test/apivideo_player_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:apivideo_player/apivideo_player.dart'; 4 | import 'package:apivideo_player/src/platform/apivideo_player_platform_interface.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | void main() { 8 | TestWidgetsFlutterBinding.ensureInitialized(); 9 | 10 | test('plugin initialized', () async { 11 | TestWidgetsFlutterBinding.ensureInitialized(); 12 | final FakeApiVideoPlayerPlatform fakeVideoPlayerPlatform = 13 | FakeApiVideoPlayerPlatform(); 14 | ApiVideoPlayerPlatform.instance = fakeVideoPlayerPlatform; 15 | 16 | final ApiVideoPlayerController controller = ApiVideoPlayerController( 17 | videoOptions: VideoOptions(videoId: "test", type: VideoType.vod)); 18 | await controller.initialize(); 19 | expect(fakeVideoPlayerPlatform.calls.first, 'initialize'); 20 | expect(fakeVideoPlayerPlatform.calls[1], 'create'); 21 | }); 22 | } 23 | 24 | class FakeApiVideoPlayerPlatform extends ApiVideoPlayerPlatform { 25 | Completer initialized = Completer(); 26 | List calls = []; 27 | 28 | @override 29 | Future initialize(bool autoplay) { 30 | calls.add('initialize'); 31 | initialized.complete(true); 32 | return Future.value(0); 33 | } 34 | 35 | @override 36 | Future create(int textureId, VideoOptions videoOptions) { 37 | calls.add('create'); 38 | return Future.value(); 39 | } 40 | 41 | @override 42 | Stream playerEventsFor(int textureId) { 43 | return StreamController().stream; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/apivideo_types.dart: -------------------------------------------------------------------------------- 1 | import 'package:apivideo_player/apivideo_player.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | test('Assert no inference when type is explicit', () { 6 | final videoOptions = 7 | VideoOptions(videoId: "vi77Dgk0F8eLwaFOtC5870yn", type: VideoType.live); 8 | expect(videoOptions.videoId, "vi77Dgk0F8eLwaFOtC5870yn"); 9 | expect(videoOptions.type, VideoType.live); 10 | expect(videoOptions.token, null); 11 | }); 12 | 13 | test('Assert inferred type for vod', () { 14 | final videoOptions = VideoOptions(videoId: "vi77Dgk0F8eLwaFOtC5870yn"); 15 | expect(videoOptions.videoId, "vi77Dgk0F8eLwaFOtC5870yn"); 16 | expect(videoOptions.type, VideoType.vod); 17 | expect(videoOptions.token, null); 18 | }); 19 | 20 | test('Assert inferred type for live', () { 21 | final videoOptions = VideoOptions(videoId: "li77Dgk0F8eLwaFOtC5870yn"); 22 | expect(videoOptions.videoId, "li77Dgk0F8eLwaFOtC5870yn"); 23 | expect(videoOptions.type, VideoType.live); 24 | expect(videoOptions.token, null); 25 | }); 26 | } 27 | --------------------------------------------------------------------------------