├── .editorconfig ├── .gitattributes ├── .github └── pull-request-template.md ├── .gitignore ├── .metadata ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── res.properties ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── media │ │ └── bcc │ │ └── bccm_player │ │ └── pigeon │ │ ├── ChromecastControllerPigeon.java │ │ ├── DownloaderApi.java │ │ └── PlaybackPlatformApi.java │ ├── kotlin │ └── media │ │ └── bcc │ │ └── bccm_player │ │ ├── BccmPlayerPlugin.kt │ │ ├── BccmPlayerPluginEvent.kt │ │ ├── BccmPlayerPluginSingleton.kt │ │ ├── DownloadService.kt │ │ ├── Downloader.kt │ │ ├── DownloaderApiImpl.kt │ │ ├── MediaInfoFetcher.kt │ │ ├── PlaybackApiImpl.kt │ │ ├── PlaybackService.kt │ │ ├── players │ │ ├── PlayerController.kt │ │ ├── PlayerListener.kt │ │ ├── chromecast │ │ │ ├── CastExpandedControlsActivity.kt │ │ │ ├── CastMediaItemConverter.kt │ │ │ ├── CastOptionsProvider.kt │ │ │ ├── CastPlayerController.kt │ │ │ ├── CastPlayerData.kt │ │ │ └── CastPlayerWithTrackSelection.kt │ │ └── exoplayer │ │ │ ├── BccmForwardingPlayer.kt │ │ │ ├── BccmPlayerViewController.kt │ │ │ └── ExoPlayerController.kt │ │ ├── utils │ │ ├── DevicePerformanceManager.kt │ │ ├── EmptySurfaceView.kt │ │ ├── LanguageUtils.kt │ │ ├── NoOpVoidResult.kt │ │ ├── PigeonExtensions.kt │ │ ├── SwipeTouchListener.kt │ │ ├── SystemGestureExcludedLinearLayout.kt │ │ └── TrackUtils.kt │ │ └── views │ │ ├── FlutterCastButton.kt │ │ ├── FlutterCastPlayerView.kt │ │ ├── FlutterEmptyView.kt │ │ ├── FlutterExoPlayerView.kt │ │ └── FullscreenPlayerView.kt │ └── res │ ├── anim │ └── swipe_up.xml │ ├── drawable │ ├── exit.png │ ├── icon_play.png │ ├── live_gradient.xml │ └── pip_icon.png │ ├── layout │ ├── activity_picture_in_picture.xml │ ├── player_controls.xml │ ├── player_fullscreen_view.xml │ ├── player_view.xml │ └── surface_player_view.xml │ ├── menu │ ├── cast.xml │ └── cast_expanded_controller.xml │ ├── values-night │ └── themes.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── strings.xml │ ├── styles.xml │ ├── themes.xml │ └── values.xml ├── assets └── .gitkeep ├── devtools_options.yaml ├── doc ├── advanced-setup │ ├── app-config.md │ ├── chromecast.md │ └── plugins.md ├── advanced-usage │ ├── airplay.md │ ├── custom-controls.md │ ├── hdr-content.md │ ├── offline.md │ ├── orientations.md │ ├── player-state.md │ ├── primary-player.md │ └── styling.md ├── contributing │ ├── architecture.md │ ├── basics.md │ ├── example.md │ ├── ios.md │ └── project_hierarchy.png ├── demo │ ├── casting.jpg │ ├── casting_fullscreen.jpg │ ├── controls.jpg │ ├── demo.gif │ └── demo.mp4 ├── index.md └── usage.md ├── example ├── .gitignore ├── .vscode │ └── launch.json ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── media │ │ │ │ │ └── bcc │ │ │ │ │ └── bccm_player_example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── 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 │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── example_videos.dart │ ├── examples │ │ ├── custom_controls.dart │ │ ├── downloader.dart │ │ ├── list_of_players.dart │ │ ├── native_controls.dart │ │ ├── playground.dart │ │ ├── queue.dart │ │ └── simple_player.dart │ ├── generated_plugin_registrant.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml └── 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 │ └── Group.png ├── Classes │ ├── BccmPlayerPlugin.h │ ├── BccmPlayerPlugin.m │ ├── Downloader │ │ ├── Downloader.swift │ │ ├── DownloaderState.swift │ │ └── MediaInfoFetcher.swift │ ├── DownloaderApiImpl.swift │ ├── MediaQueueController.swift │ ├── Pigeon │ │ ├── ChromecastPigeon.h │ │ ├── ChromecastPigeon.m │ │ ├── DownloaderApi.swift │ │ ├── PlaybackPlatformApi.h │ │ └── PlaybackPlatformApi.m │ ├── PlaybackApiImpl.swift │ ├── Players │ │ ├── AVQueuePlayerController.swift │ │ ├── CastPlayerController.swift │ │ └── PlayerController.swift │ ├── SwiftBccmPlayerPlugin.swift │ ├── Utils │ │ ├── AsyncUtils.swift │ │ ├── CastSetup.swift │ │ ├── Colors.swift │ │ ├── Errors.swift │ │ ├── LandscapeAVPlayerViewController.swift │ │ ├── MediaItemMapper.swift │ │ ├── MetadataUtils.swift │ │ ├── PeakBitrateController.swift │ │ ├── PigeonExtensions.swift │ │ ├── PlayerMetadataConstants.swift │ │ ├── SimpleGCKRequestDelegate.swift │ │ └── TrackUtils.swift │ ├── Views │ │ ├── BccmPlayerView.swift │ │ ├── CastButtonView.swift │ │ └── CastPlayerView.swift │ └── bccm_player.h └── bccm_player.podspec ├── lib ├── bccm_player.dart ├── bccm_player_native.dart ├── bccm_player_web.dart ├── controls.dart ├── plugins │ ├── bcc_media.dart │ └── riverpod.dart └── src │ ├── downloader_platform_interface.dart │ ├── model │ └── player_view_config.dart │ ├── native │ ├── chromecast_events.dart │ ├── chromecast_pigeon_listener.dart │ ├── root_pigeon_playback_listener.dart │ └── root_queue_manager_pigeon.dart │ ├── pigeon │ ├── chromecast_pigeon.g.dart │ ├── downloader_pigeon.g.dart │ ├── pigeon_extensions.dart │ └── playback_platform_pigeon.g.dart │ ├── playback_platform_interface.dart │ ├── plugins │ ├── bcc_media │ │ └── bccm_playback_listener.dart │ └── riverpod │ │ ├── providers.dart │ │ └── providers │ │ ├── chromecast_provider.dart │ │ ├── player_event_stream_provider.dart │ │ ├── player_provider.dart │ │ └── plugin_state_provider.dart │ ├── queue │ ├── default_queue_controller.dart │ └── queue_controller.dart │ ├── state │ ├── inherited_player_view_controller.dart │ ├── player_controller.dart │ ├── player_state_notifier.dart │ ├── player_state_notifier.freezed.dart │ ├── player_view_controller.dart │ ├── plugin_state_notifier.dart │ ├── plugin_state_notifier.freezed.dart │ ├── state_playback_listener.dart │ └── texture.dart │ ├── theme │ ├── bccm_player_theme.dart │ ├── controls_theme_data.dart │ ├── mini_player_theme_data.dart │ └── player_theme.dart │ ├── utils │ ├── debouncer.dart │ ├── extensions.dart │ ├── num.dart │ ├── svg_icons.dart │ ├── time.dart │ ├── timeline.dart │ ├── transparent_image.dart │ └── use_wakelock_while_palying.dart │ ├── web │ ├── js │ │ └── bccm_video_player.dart │ └── video_js_player.dart │ └── widgets │ ├── cast │ ├── cast_button.dart │ └── cast_player.dart │ ├── controls │ ├── control_fade_out.dart │ ├── controls_wrapper.dart │ ├── default │ │ ├── fullscreen_button.dart │ │ ├── play_pause_button.dart │ │ ├── settings.dart │ │ ├── settings_option_list.dart │ │ └── time_skip_button.dart │ ├── default_controls.dart │ ├── play_next_button.dart │ ├── smooth_video_progress.dart │ └── tv │ │ └── tv_controls.dart │ ├── mini_player │ ├── loading_indicator.dart │ └── mini_player.dart │ ├── utils │ ├── bccm_player_plugin_state_builder.dart │ └── bccm_player_state_builder.dart │ └── video │ ├── controlled_player_view.dart │ ├── native_player_view.dart │ ├── player_view.dart │ └── video_platform_view.dart ├── mkdocs.yml ├── pigeons ├── README.md ├── chromecast_pigeon.dart ├── downloader_pigeon.dart └── playback_platform_pigeon.dart ├── pubspec.yaml └── test ├── controller_test.dart └── utils ├── mocks.dart └── mocks.mocks.dart /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.dart] 4 | max_line_length = 150 5 | 6 | [*.kt] 7 | max_line_length = 150 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ios/Frameworks/** binary -------------------------------------------------------------------------------- /.github/pull-request-template.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | ## About the changes 14 | 15 | 21 | ### Important files 22 | 23 | 27 | ## Discussion points 28 | -------------------------------------------------------------------------------- /.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 | 32 | **/dgph 33 | 34 | # Mkdocs (has own branch) 35 | site -------------------------------------------------------------------------------- /.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: 2d771d4966b6a8562bd026807fccbbddecf42e2e 8 | channel: master 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 2d771d4966b6a8562bd026807fccbbddecf42e2e 17 | base_revision: 2d771d4966b6a8562bd026807fccbbddecf42e2e 18 | - platform: android 19 | create_revision: 2d771d4966b6a8562bd026807fccbbddecf42e2e 20 | base_revision: 2d771d4966b6a8562bd026807fccbbddecf42e2e 21 | - platform: ios 22 | create_revision: 2d771d4966b6a8562bd026807fccbbddecf42e2e 23 | base_revision: 2d771d4966b6a8562bd026807fccbbddecf42e2e 24 | - platform: web 25 | create_revision: 2d771d4966b6a8562bd026807fccbbddecf42e2e 26 | base_revision: 2d771d4966b6a8562bd026807fccbbddecf42e2e 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 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "example", 9 | "cwd": "example", 10 | "request": "launch", 11 | "type": "dart" 12 | }, 13 | { 14 | "name": "example (profile mode)", 15 | "cwd": "example", 16 | "request": "launch", 17 | "type": "dart", 18 | "flutterMode": "profile" 19 | }, 20 | { 21 | "name": "example (release mode)", 22 | "cwd": "example", 23 | "request": "launch", 24 | "type": "dart", 25 | "flutterMode": "release" 26 | }, 27 | { 28 | "name": "example (attach)", 29 | "cwd": "example", 30 | "request": "attach", 31 | "type": "dart" 32 | }, 33 | { 34 | "name": "Debug build_runner", 35 | "cwd": "${workspaceFolder}", 36 | "request": "launch", 37 | "program": ".dart_tool/build/entrypoint/build.dart", 38 | "type": "dart", 39 | "args": ["build", "--delete-conflicting-outputs"] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.lineLength": 150, 3 | "[dart]": { 4 | "editor.rulers": [150] 5 | }, 6 | "git.autoRepositoryDetection": "openEditors", 7 | "git.detectSubmodules": false, 8 | "files.associations": { 9 | "*.dart.template": "dart" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) (2024) BCC Media Sti 2 | 3 | All rights reserved. 4 | 5 | The material contained in this project is the property of BCC Media Sti. No permission is granted to reproduce, distribute, or create derivative works from the content of this project other than for purposes of testing, development, and review. 6 | 7 | Any form of commercial use, distribution, or reproduction is explicitly forbidden without prior written consent from BCC Media Sti. 8 | 9 | Notwithstanding the above, the presence of a more specific license in a file or folder within this project overrides the terms of this license for that specific file or folder. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: publish pigeons help 2 | 3 | BUILD_NUMBER=$(shell grep -i -e "version: " pubspec.yaml | cut -d " " -f 2) 4 | 5 | # From https://stackoverflow.com/a/64996042 6 | help: 7 | @egrep -h '\s##\s' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-30s\033[0m %s\n", $$1, $$2}' 8 | 9 | 10 | publish: ## Publish the package to pub.dev 11 | read -p "Release v${BUILD_NUMBER}? (CTRL+C to abort)" 12 | git tag v${BUILD_NUMBER} 13 | git push origin v${BUILD_NUMBER} 14 | dart pub publish 15 | mkdocs gh-deploy 16 | 17 | pigeons: ## Generate pigeon files 18 | for f in pigeons/*.dart; do dart run pigeon --input $$f; done -------------------------------------------------------------------------------- /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 | 6 | analyzer: 7 | errors: 8 | body_might_complete_normally_nullable: ignore 9 | exclude: [lib/**.freezed.dart, lib/**.g.dart] 10 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | gradle-wrapper.jar 3 | /.gradle 4 | /captures/ 5 | /gradlew 6 | /gradlew.bat 7 | /local.properties 8 | /.idea/workspace.xml 9 | /.idea/libraries 10 | .DS_Store 11 | /build 12 | /captures 13 | .cxx 14 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '2.1.0' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | maven { url "https://npaw.jfrog.io/artifactory/youbora/" } 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:8.10.0' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | 14 | } 15 | plugins { 16 | id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version" 17 | } 18 | 19 | group 'media.bcc.bccm_player' 20 | version '1.0-SNAPSHOT' 21 | 22 | rootProject.allprojects { 23 | repositories { 24 | google() 25 | mavenCentral() 26 | maven { url "https://npaw.jfrog.io/artifactory/youbora/" } 27 | } 28 | } 29 | 30 | apply plugin: 'com.android.library' 31 | apply plugin: 'kotlin-android' 32 | apply plugin: 'kotlin-parcelize' 33 | 34 | android { 35 | namespace "media.bcc.bccm_player" 36 | compileSdk 35 37 | 38 | compileOptions { 39 | sourceCompatibility JavaVersion.VERSION_11 40 | targetCompatibility JavaVersion.VERSION_11 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = '11' 45 | } 46 | 47 | sourceSets { 48 | main.java.srcDirs += 'src/main/kotlin' 49 | } 50 | 51 | defaultConfig { 52 | minSdkVersion 19 53 | multiDexEnabled true 54 | } 55 | buildFeatures { 56 | viewBinding true 57 | } 58 | } 59 | 60 | dependencies { 61 | def youboraVersion = "6.8.1" 62 | implementation "com.nicepeopleatwork:youboralib:$youboraVersion" 63 | implementation "com.nicepeopleatwork:media3-adapter:$youboraVersion" 64 | implementation 'androidx.constraintlayout:constraintlayout:2.2.1' 65 | 66 | def media3_version = "1.7.1" 67 | implementation "androidx.media3:media3-exoplayer:$media3_version" 68 | implementation "androidx.media3:media3-exoplayer-hls:$media3_version" 69 | implementation "androidx.media3:media3-cast:$media3_version" 70 | implementation "androidx.media3:media3-test-utils:$media3_version" 71 | implementation "androidx.media3:media3-session:$media3_version" 72 | implementation "androidx.media3:media3-ui:$media3_version" 73 | implementation "androidx.media3:media3-datasource:$media3_version" 74 | 75 | implementation 'androidx.appcompat:appcompat:1.7.0' 76 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0" 77 | implementation 'androidx.core:core-ktx:1.16.0' 78 | implementation 'androidx.core:core-performance:1.0.0' 79 | implementation 'androidx.core:core-performance-play-services:1.0.0' 80 | implementation 'com.google.android.gms:play-services-cast-framework:22.1.0' 81 | } 82 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | ## For more details on how to configure your build environment visit 2 | # http://www.gradle.org/docs/current/userguide/build_environment.html 3 | # 4 | # Specifies the JVM arguments used for the daemon process. 5 | # The setting is particularly useful for tweaking memory settings. 6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m 7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 8 | # 9 | # When configured, Gradle will run in incubating parallel mode. 10 | # This option should only be used with decoupled projects. For more details, visit 11 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 12 | # org.gradle.parallel=true 13 | #Tue May 27 11:16:55 CEST 2025 14 | android.enableJetifier=true 15 | android.useAndroidX=true 16 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /android/res.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/android/res.properties -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'bccm_player' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/BccmPlayerPluginEvent.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player 2 | 3 | import android.app.Activity 4 | import androidx.lifecycle.Lifecycle 5 | 6 | interface BccmPlayerPluginEvent 7 | 8 | class AttachedToActivityEvent(val activity: Activity) : BccmPlayerPluginEvent 9 | class DetachedFromActivityEvent : BccmPlayerPluginEvent 10 | class SetPlayerViewVisibilityEvent(val viewId: Long, val visible: Boolean) : 11 | BccmPlayerPluginEvent 12 | 13 | class PictureInPictureModeChangedEvent( 14 | val isInPictureInPictureMode: Boolean, 15 | val lifecycleState: Lifecycle.State 16 | ) : 17 | BccmPlayerPluginEvent 18 | -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/BccmPlayerPluginSingleton.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player 2 | 3 | import android.app.Activity 4 | import android.util.Log 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.Job 8 | import kotlinx.coroutines.flow.MutableSharedFlow 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.filter 11 | import kotlinx.coroutines.flow.update 12 | import kotlinx.coroutines.launch 13 | import media.bcc.bccm_player.pigeon.PlaybackPlatformApi 14 | 15 | object BccmPlayerPluginSingleton { 16 | 17 | val activityState = MutableStateFlow(null) 18 | val npawConfigState = MutableStateFlow(null) 19 | val appConfigState = MutableStateFlow(null) 20 | val eventBus = MutableSharedFlow() 21 | private val mainScope = CoroutineScope(Dispatchers.Main + Job()) 22 | 23 | init { 24 | Log.d("bccm", "bccmdebug: created BccmPlayerPluginSingleton") 25 | mainScope.launch { keepTrackOfActivity() } 26 | } 27 | 28 | private suspend fun keepTrackOfActivity() { 29 | eventBus.filter { event -> event is AttachedToActivityEvent }.collect { event -> 30 | activityState.update { (event as AttachedToActivityEvent).activity } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/DownloadService.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player 2 | 3 | import android.app.Notification 4 | import androidx.annotation.OptIn 5 | import androidx.media3.common.util.UnstableApi 6 | import androidx.media3.exoplayer.offline.Download 7 | import androidx.media3.exoplayer.offline.DownloadManager 8 | import androidx.media3.exoplayer.offline.DownloadNotificationHelper 9 | import androidx.media3.exoplayer.offline.DownloadService 10 | import androidx.media3.exoplayer.scheduler.Scheduler 11 | 12 | 13 | private const val JOB_ID = 1 14 | private const val FOREGROUND_NOTIFICATION_ID = 1 15 | const val DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel" 16 | 17 | @OptIn(UnstableApi::class) 18 | class DownloadService : DownloadService( 19 | FOREGROUND_NOTIFICATION_ID, 20 | DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, 21 | DOWNLOAD_NOTIFICATION_CHANNEL_ID, 22 | androidx.media3.exoplayer.R.string.exo_download_notification_channel_name, 23 | 0 24 | ) { 25 | override fun getDownloadManager(): DownloadManager { 26 | return Downloader.getOrCreateDownloadManager(applicationContext) 27 | } 28 | 29 | override fun getScheduler(): Scheduler? { 30 | return null 31 | } 32 | 33 | override fun getForegroundNotification( 34 | downloads: MutableList, 35 | notMetRequirements: Int 36 | ): Notification { 37 | return DownloadNotificationHelper( 38 | this, 39 | DOWNLOAD_NOTIFICATION_CHANNEL_ID 40 | ).buildProgressNotification( 41 | this, 42 | android.R.drawable.stat_sys_download_done, 43 | null, // TODO: accept custom message? 44 | null, 45 | downloads, 46 | notMetRequirements, 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/DownloaderApiImpl.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player 2 | 3 | import android.os.Environment 4 | import android.os.StatFs 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.launch 8 | import media.bcc.bccm_player.pigeon.DownloaderApi 9 | 10 | class DownloaderApiImpl(private val downloader: Downloader) : DownloaderApi.DownloaderPigeon { 11 | private val scope = CoroutineScope(Dispatchers.Main) 12 | 13 | override fun startDownload( 14 | downloadConfig: DownloaderApi.DownloadConfig, 15 | result: DownloaderApi.Result 16 | ) { 17 | scope.launch { 18 | try { 19 | result.success(downloader.startDownload(downloadConfig)) 20 | } catch (e: Exception) { 21 | result.error(e) 22 | } 23 | } 24 | } 25 | 26 | override fun getDownloadStatus(downloadKey: String, result: DownloaderApi.Result) { 27 | result.success(downloader.getDownloadStatus(downloadKey)) 28 | } 29 | 30 | override fun getDownloads(result: DownloaderApi.Result>) { 31 | result.success(downloader.getDownloads().toMutableList()) 32 | } 33 | 34 | override fun getDownload(downloadKey: String, result: DownloaderApi.NullableResult) { 35 | } 36 | 37 | override fun removeDownload(downloadKey: String, result: DownloaderApi.VoidResult) { 38 | downloader.removeDownload(downloadKey) 39 | result.success() 40 | } 41 | 42 | override fun getFreeDiskSpace(result: DownloaderApi.Result) { 43 | try { 44 | val stat = StatFs(Environment.getExternalStorageDirectory().path) 45 | result.success((stat.blockSizeLong * stat.availableBlocksLong).toDouble()); 46 | } catch (err: Error) { 47 | result.error(err); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/MediaInfoFetcher.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player 2 | 3 | import android.content.Context 4 | import androidx.media3.common.C.TRACK_TYPE_AUDIO 5 | import androidx.media3.common.C.TRACK_TYPE_TEXT 6 | import androidx.media3.common.C.TRACK_TYPE_VIDEO 7 | import androidx.media3.common.MediaItem 8 | import androidx.media3.common.PlaybackException 9 | import androidx.media3.common.Player 10 | import androidx.media3.common.Tracks 11 | import androidx.media3.common.util.UnstableApi 12 | import androidx.media3.exoplayer.ExoPlayer 13 | import androidx.media3.exoplayer.trackselection.DefaultTrackSelector 14 | import kotlinx.coroutines.suspendCancellableCoroutine 15 | import kotlinx.coroutines.withTimeout 16 | import media.bcc.bccm_player.pigeon.PlaybackPlatformApi.MediaInfo 17 | import media.bcc.bccm_player.utils.TrackUtils 18 | 19 | object MediaInfoFetcher { 20 | @UnstableApi 21 | suspend fun fetchMediaInfo(context: Context, url: String, mimeType: String?): MediaInfo { 22 | val trackSelector = DefaultTrackSelector(context); 23 | trackSelector.setParameters( 24 | trackSelector.buildUponParameters().setMaxVideoBitrate(1).setMaxAudioBitrate(1) 25 | .setTrackTypeDisabled(TRACK_TYPE_AUDIO, true) 26 | .setTrackTypeDisabled(TRACK_TYPE_VIDEO, true) 27 | .setTrackTypeDisabled(TRACK_TYPE_TEXT, true) 28 | ) 29 | val player = ExoPlayer.Builder(context) 30 | .setTrackSelector(trackSelector).build() 31 | 32 | player.setMediaItem(MediaItem.Builder().setMimeType(mimeType).setUri(url).build()) 33 | player.prepare() 34 | 35 | withTimeout(10000) { 36 | suspendCancellableCoroutine { cont -> 37 | player.addListener(object : Player.Listener { 38 | override fun onTracksChanged(tracks: Tracks) { 39 | cont.resumeWith(Result.success(Unit)) 40 | } 41 | 42 | override fun onPlayerError(error: PlaybackException) { 43 | cont.resumeWith(Result.failure(error)) 44 | } 45 | }) 46 | } 47 | } 48 | 49 | val audioTracks = TrackUtils.getAudioTracksForPlayer(player) 50 | val videoTracks = TrackUtils.getVideoTracksForPlayer(player) 51 | val textTracks = TrackUtils.getTextTracksForPlayer(player) 52 | 53 | return MediaInfo.Builder() 54 | .setAudioTracks(audioTracks) 55 | .setVideoTracks(videoTracks) 56 | .setTextTracks(textTracks) 57 | .build() 58 | } 59 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/players/chromecast/CastExpandedControlsActivity.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.players.chromecast 2 | 3 | import android.view.Menu 4 | import com.google.android.gms.cast.framework.CastButtonFactory 5 | import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity 6 | import media.bcc.bccm_player.R 7 | 8 | class CastExpandedControlsActivity : ExpandedControllerActivity() { 9 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 10 | super.onCreateOptionsMenu(menu) 11 | menuInflater.inflate(R.menu.cast_expanded_controller, menu) 12 | CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item) 13 | return true 14 | } 15 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/players/chromecast/CastOptionsProvider.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.players.chromecast 2 | 3 | import android.content.Context 4 | import android.content.pm.ApplicationInfo 5 | import android.content.pm.PackageManager 6 | import com.google.android.gms.cast.framework.CastOptions 7 | import com.google.android.gms.cast.framework.OptionsProvider 8 | import com.google.android.gms.cast.framework.SessionProvider 9 | import com.google.android.gms.cast.framework.media.CastMediaOptions 10 | 11 | 12 | @Suppress("unused") 13 | class CastOptionsProvider : OptionsProvider { 14 | override fun getCastOptions(context: Context): CastOptions { 15 | val mediaOptions = CastMediaOptions.Builder() 16 | .setExpandedControllerActivityClassName(CastExpandedControlsActivity::class.java.name) 17 | .build() 18 | val metaData = context.packageManager.getApplicationInfo( 19 | context.packageName, 20 | PackageManager.GET_META_DATA 21 | ).metaData 22 | val appId = metaData?.getString("cast_app_id") 23 | 24 | val builder = CastOptions.Builder(); 25 | if (appId != null) { 26 | builder.setReceiverApplicationId(appId); 27 | } 28 | builder.setCastMediaOptions(mediaOptions) 29 | return builder.build() 30 | } 31 | 32 | override fun getAdditionalSessionProviders(context: Context): List? { 33 | return null 34 | } 35 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/players/chromecast/CastPlayerData.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.players.chromecast 2 | 3 | class CastPlayerData { 4 | var isLive: Boolean? = null 5 | var isOffline: Boolean? = null 6 | var mimeType: String? = null 7 | var lastKnownAudioLanguage: String? = null 8 | var lastKnownSubtitleLanguage: String? = null 9 | 10 | companion object { 11 | fun from(extras: Map?): CastPlayerData? { 12 | if (extras == null) return null 13 | var playerData: CastPlayerData? = null 14 | // Example: media.bcc.player.is_live 15 | for (kv in extras.filter { it.key.contains(CastMediaItemConverter.BCCM_PLAYER_DATA) }) { 16 | if (playerData == null) playerData = CastPlayerData() 17 | if (kv.key == CastMediaItemConverter.PLAYER_DATA_IS_LIVE) { 18 | playerData.isLive = extras[kv.key] == "true" 19 | } 20 | if (kv.key == CastMediaItemConverter.PLAYER_DATA_IS_OFFLINE) { 21 | playerData.isOffline = extras[kv.key] == "true" 22 | } 23 | if (kv.key == CastMediaItemConverter.PLAYER_DATA_MIME_TYPE) { 24 | playerData.mimeType = extras[kv.key] as? String 25 | } 26 | if (kv.key == CastMediaItemConverter.PLAYER_DATA_LAST_KNOWN_AUDIO_LANGUAGE) { 27 | playerData.lastKnownAudioLanguage = extras[kv.key] as? String 28 | } 29 | if (kv.key == CastMediaItemConverter.PLAYER_DATA_LAST_KNOWN_SUBTITLE_LANGUAGE) { 30 | playerData.lastKnownSubtitleLanguage = extras[kv.key] as? String 31 | } 32 | } 33 | return playerData 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/players/exoplayer/BccmPlayerViewController.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.players.exoplayer 2 | 3 | import androidx.annotation.RequiresApi 4 | 5 | interface BccmPlayerViewController { 6 | val isFullscreen: Boolean 7 | val shouldPipAutomatically: Boolean 8 | fun onOwnershipLost() {} 9 | fun exitFullscreen() {} 10 | fun enterFullscreen() {} 11 | 12 | @RequiresApi(value = 26) 13 | fun enterPictureInPicture() 14 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/utils/DevicePerformanceManager.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.utils 2 | 3 | import android.content.Context 4 | import androidx.core.performance.DevicePerformance 5 | import androidx.core.performance.play.services.PlayServicesDevicePerformance 6 | 7 | object DevicePerformanceManager { 8 | @Volatile 9 | private var instance: DevicePerformance? = null 10 | 11 | fun getInstance(context: Context): DevicePerformance? { 12 | return instance ?: synchronized(this) { 13 | instance ?: run { 14 | try { 15 | PlayServicesDevicePerformance(context).also { instance = it } 16 | } catch (e: Exception) { 17 | null 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/utils/EmptySurfaceView.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.utils 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.os.Build 7 | import android.view.SurfaceHolder 8 | import android.view.SurfaceView 9 | import androidx.annotation.RequiresApi 10 | 11 | class EmptySurfaceView(context: Context?) : SurfaceView(context), SurfaceHolder.Callback { 12 | override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {} 13 | 14 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 15 | override fun surfaceCreated(holder: SurfaceHolder) { 16 | var canvas: Canvas? = null 17 | try { 18 | canvas = holder.lockCanvas(null) 19 | synchronized(holder) { 20 | canvas?.drawColor(Color.RED) 21 | } 22 | } catch (e: Exception) { 23 | e.printStackTrace() 24 | } finally { 25 | if (canvas != null) { 26 | holder.unlockCanvasAndPost(canvas) 27 | } 28 | } 29 | } 30 | 31 | override fun surfaceDestroyed(holder: SurfaceHolder) { 32 | // TODO Auto-generated method stub 33 | } 34 | 35 | init { 36 | holder.addCallback(this) 37 | } 38 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/utils/LanguageUtils.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.utils 2 | 3 | object LanguageUtils { 4 | fun toThreeLetterLanguageCode(languageCode: String?): String? { 5 | return when (languageCode) { 6 | // Norwegian 7 | "no", "nb", "nb-NO", "nor", "nob", "no-nob" -> "nor" 8 | 9 | // English 10 | "en", "eng", "en-US", "en-GB" -> "eng" 11 | 12 | // French 13 | "fr", "fra", "fr-FR" -> "fra" 14 | 15 | // German 16 | "de", "deu", "de-DE" -> "deu" 17 | 18 | // Hungarian 19 | "hu", "hun", "hu-HU" -> "hun" 20 | 21 | // Spanish 22 | "es", "spa", "es-ES" -> "spa" 23 | 24 | // Italian 25 | "it", "ita", "it-IT" -> "ita" 26 | 27 | // Polish 28 | "pl", "pol", "pl-PL" -> "pol" 29 | 30 | // Romanian 31 | "ro", "ron", "ro-RO" -> "ron" 32 | 33 | // Russian 34 | "ru", "rus", "ru-RU" -> "rus" 35 | 36 | // Slovenian 37 | "sl", "slv", "sl-SI" -> "slv" 38 | 39 | // Turkish 40 | "tr", "tur", "tr-TR" -> "tur" 41 | 42 | // Chinese 43 | "zh", "zho", "cmn", "zh-cmn", "zh-CN" -> "zho" 44 | 45 | // Cantonese 46 | "zh-HK", "yue", "yue-HK" -> "yue" 47 | 48 | // Tamil 49 | "ta", "tam", "ta-IN" -> "tam" 50 | 51 | // Bulgarian 52 | "bg", "bul", "bg-BG" -> "bul" 53 | 54 | // Dutch 55 | "nl", "nld", "nl-NL" -> "nld" 56 | 57 | // Danish 58 | "da", "dan", "da-DK" -> "dan" 59 | 60 | // Finnish 61 | "fi", "fin", "fi-FI" -> "fin" 62 | 63 | // Portuguese 64 | "pt", "por", "pt-PT" -> "por" 65 | 66 | // Khasi 67 | "kha", "kha-IN" -> "kha" 68 | 69 | // Croatian 70 | "hr", "hrv", "hbs-hrv", "hr-HR" -> "hrv" 71 | 72 | else -> languageCode 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/utils/NoOpVoidResult.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.utils 2 | 3 | import media.bcc.bccm_player.pigeon.ChromecastControllerPigeon 4 | import media.bcc.bccm_player.pigeon.DownloaderApi 5 | import media.bcc.bccm_player.pigeon.PlaybackPlatformApi 6 | 7 | /** 8 | * Response handler for calls to Dart that don't require any error handling, such as event 9 | * notifications where if the Dart side has been torn down, silently dropping the message is the 10 | * desired behavior. 11 | * 12 | * 13 | * Longer term, any call using this is likely a good candidate to migrate to event channels. 14 | */ 15 | class NoOpVoidResult : PlaybackPlatformApi.VoidResult { 16 | override fun success() {} 17 | override fun error(error: Throwable) {} 18 | } 19 | 20 | class DownloaderApiNoOpVoidResult : DownloaderApi.VoidResult { 21 | override fun success() {} 22 | override fun error(error: Throwable) {} 23 | } 24 | 25 | class ChromecastNoOpVoidResult : ChromecastControllerPigeon.VoidResult { 26 | override fun success() {} 27 | override fun error(error: Throwable) {} 28 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/utils/PigeonExtensions.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.utils 2 | 3 | import androidx.media3.common.C 4 | import media.bcc.bccm_player.pigeon.PlaybackPlatformApi 5 | 6 | fun PlaybackPlatformApi.TrackType.toMedia3Type(): Int { 7 | if (this == PlaybackPlatformApi.TrackType.TEXT) { 8 | return C.TRACK_TYPE_TEXT; 9 | } else if (this == PlaybackPlatformApi.TrackType.AUDIO) { 10 | return C.TRACK_TYPE_AUDIO; 11 | } else if (this == PlaybackPlatformApi.TrackType.VIDEO) { 12 | return C.TRACK_TYPE_VIDEO; 13 | } 14 | return C.TRACK_TYPE_UNKNOWN; 15 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/utils/SwipeTouchListener.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.utils 2 | 3 | import android.view.MotionEvent 4 | import android.view.View 5 | import kotlin.math.abs 6 | 7 | 8 | class SwipeTouchListener(private val minimumDistance: Double, private val listener: Listener) : 9 | View.OnTouchListener { 10 | private var downY = 0f 11 | private var upY = 0f 12 | 13 | interface Listener { 14 | fun onTopToBottomSwipe() {} 15 | } 16 | 17 | override fun onTouch(v: View?, event: MotionEvent): Boolean { 18 | when (event.action) { 19 | MotionEvent.ACTION_DOWN -> { 20 | downY = event.y 21 | return true 22 | } 23 | 24 | MotionEvent.ACTION_UP -> { 25 | upY = event.y 26 | val deltaY = downY - upY 27 | v?.performClick() 28 | 29 | if (abs(deltaY) > minimumDistance) { 30 | if (deltaY < 0) { 31 | listener.onTopToBottomSwipe() 32 | return true 33 | } 34 | } 35 | } 36 | } 37 | return true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/utils/SystemGestureExcludedLinearLayout.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.utils 2 | 3 | import android.content.Context 4 | import android.graphics.Paint 5 | import android.graphics.Rect 6 | import android.os.Build 7 | import android.util.AttributeSet 8 | import android.widget.LinearLayout 9 | 10 | 11 | class SystemGestureExcludedLinearLayout : LinearLayout { 12 | private var exclusionRect: Rect = Rect() 13 | private var exclusionRects: ArrayList = ArrayList() 14 | private var excludeEnabled: Boolean = true 15 | 16 | constructor(context: Context?) : super(context) 17 | 18 | constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) 19 | 20 | constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( 21 | context, 22 | attrs, 23 | defStyleAttr 24 | ) 25 | 26 | fun setExclusionEnabled(enabled: Boolean) { 27 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 28 | if (!enabled) { 29 | excludeEnabled = false 30 | exclusionRects.clear() 31 | systemGestureExclusionRects = exclusionRects 32 | } else { 33 | excludeEnabled = true 34 | forceLayout() 35 | } 36 | } 37 | } 38 | 39 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 40 | super.onLayout(changed, left, top, right, bottom) 41 | 42 | if (!excludeEnabled) { 43 | return 44 | } 45 | 46 | // Set the system gesture exclusion rects for the LinearLayout 47 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 48 | exclusionRect.set(left, top, right, bottom); 49 | exclusionRects.clear(); 50 | exclusionRects.add(exclusionRect); 51 | systemGestureExclusionRects = exclusionRects 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/views/FlutterCastButton.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.views 2 | 3 | import android.content.Context 4 | import android.graphics.BlendMode 5 | import android.graphics.BlendModeColorFilter 6 | import android.graphics.Color 7 | import android.os.Build 8 | import android.view.ContextThemeWrapper 9 | import android.view.View 10 | import androidx.core.graphics.drawable.DrawableCompat 11 | import androidx.mediarouter.app.MediaRouteButton 12 | import com.google.android.gms.cast.framework.CastButtonFactory 13 | import io.flutter.plugin.common.StandardMessageCodec 14 | import io.flutter.plugin.platform.PlatformView 15 | import io.flutter.plugin.platform.PlatformViewFactory 16 | import media.bcc.bccm_player.R 17 | 18 | internal class FlutterCastButton( 19 | context: Context, 20 | color: Int? 21 | ) : 22 | PlatformView { 23 | private val _view: View 24 | 25 | class Factory : PlatformViewFactory(StandardMessageCodec.INSTANCE) { 26 | override fun create(context: Context, viewId: Int, args: Any?): PlatformView { 27 | val params = args as? Map<*, *>?; 28 | val color = (params?.get("color") as? Long?)?.toInt(); 29 | return FlutterCastButton(context, color) 30 | } 31 | } 32 | 33 | override fun getView(): View { 34 | return _view 35 | } 36 | 37 | override fun dispose() {} 38 | 39 | init { 40 | val targetColor = color ?: Color.WHITE 41 | val wrappedContext = ContextThemeWrapper(context, androidx.appcompat.R.style.Theme_AppCompat_NoActionBar) 42 | val btn = MediaRouteButton(wrappedContext) 43 | 44 | val castTheme = 45 | ContextThemeWrapper(wrappedContext, androidx.mediarouter.R.style.Theme_MediaRouter) 46 | val attrs = castTheme.obtainStyledAttributes( 47 | null, 48 | androidx.mediarouter.R.styleable.MediaRouteButton, 49 | androidx.mediarouter.R.attr.mediaRouteButtonStyle, 50 | 0 51 | ) 52 | val drawable = 53 | attrs.getDrawable(androidx.mediarouter.R.styleable.MediaRouteButton_externalRouteEnabledDrawable) 54 | attrs.recycle() 55 | if (drawable != null) { 56 | DrawableCompat.setTint(drawable, targetColor) 57 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 58 | drawable.colorFilter = BlendModeColorFilter(targetColor, BlendMode.SRC_ATOP) 59 | }; 60 | drawable.state = btn.drawableState 61 | btn.setRemoteIndicatorDrawable(drawable) 62 | } 63 | _view = btn 64 | CastButtonFactory.setUpMediaRouteButton(wrappedContext, btn) 65 | } 66 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/views/FlutterCastPlayerView.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.views 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.widget.LinearLayout 7 | import androidx.media3.ui.PlayerView 8 | import io.flutter.plugin.common.StandardMessageCodec 9 | import io.flutter.plugin.platform.PlatformView 10 | import io.flutter.plugin.platform.PlatformViewFactory 11 | import media.bcc.bccm_player.BccmPlayerPlugin 12 | import media.bcc.bccm_player.R 13 | import media.bcc.bccm_player.players.chromecast.CastPlayerController 14 | import media.bcc.bccm_player.utils.SystemGestureExcludedLinearLayout 15 | 16 | class FlutterCastPlayerView( 17 | private val context: Context, private val controller: CastPlayerController 18 | ) : PlatformView { 19 | private val _v = SystemGestureExcludedLinearLayout(context) 20 | private var _playerView: PlayerView? = null 21 | 22 | class Factory(private val plugin: BccmPlayerPlugin) : 23 | PlatformViewFactory(StandardMessageCodec.INSTANCE) { 24 | override fun create(context: Context, id: Int, args: Any?): PlatformView { 25 | val controller = plugin.getPlaybackService()?.getChromecastController() 26 | return if (controller != null) 27 | FlutterCastPlayerView(context, controller) 28 | else 29 | FlutterEmptyView(context) 30 | } 31 | } 32 | 33 | init { 34 | setup() 35 | } 36 | 37 | override fun getView(): View { 38 | return _v 39 | } 40 | 41 | override fun dispose() { 42 | 43 | } 44 | 45 | private fun setup() { 46 | LayoutInflater.from(context).inflate(R.layout.player_view, _v, true) 47 | 48 | val playerView = _v.findViewById(R.id.brunstad_player).also { _playerView = it } 49 | 50 | playerView.player = controller.castPlayer 51 | } 52 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/media/bcc/bccm_player/views/FlutterEmptyView.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player.views 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import io.flutter.plugin.common.StandardMessageCodec 6 | import io.flutter.plugin.platform.PlatformView 7 | import io.flutter.plugin.platform.PlatformViewFactory 8 | 9 | 10 | class FlutterEmptyView(context: Context) : PlatformView { 11 | 12 | private val _view: View = View(context) 13 | 14 | class Factory : PlatformViewFactory(StandardMessageCodec.INSTANCE) { 15 | override fun create(context: Context, viewId: Int, args: Any?): PlatformView { 16 | return FlutterEmptyView(context) 17 | } 18 | 19 | } 20 | 21 | override fun getView(): View { 22 | return _view 23 | } 24 | 25 | override fun dispose() {} 26 | } 27 | -------------------------------------------------------------------------------- /android/src/main/res/anim/swipe_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 12 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/android/src/main/res/drawable/exit.png -------------------------------------------------------------------------------- /android/src/main/res/drawable/icon_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/android/src/main/res/drawable/icon_play.png -------------------------------------------------------------------------------- /android/src/main/res/drawable/live_gradient.xml: -------------------------------------------------------------------------------- 1 |  2 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/pip_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/android/src/main/res/drawable/pip_icon.png -------------------------------------------------------------------------------- /android/src/main/res/layout/activity_picture_in_picture.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /android/src/main/res/layout/player_fullscreen_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 26 | 27 | 41 | 42 | 43 | 52 | 53 | 63 | -------------------------------------------------------------------------------- /android/src/main/res/menu/cast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /android/src/main/res/menu/cast_expanded_controller.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /android/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /android/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #FF039BE5 3 | #FF01579B 4 | #FF40C4FF 5 | #FF00B0FF 6 | #66000000 7 | -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PictureInPicture 3 | Dummy Button 4 | DUMMY\nCONTENT 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /android/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /android/src/main/res/values/values.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cast 4 | Failed to get Cast context. Try updating Google Play Services and restart the app. 5 | -------------------------------------------------------------------------------- /assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/assets/.gitkeep -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /doc/advanced-setup/app-config.md: -------------------------------------------------------------------------------- 1 | # App config / default languages 2 | 3 | Use the app config to control global settings like default languages. 4 | 5 | ```dart 6 | BccmPlayerInterface.instance.setAppConfig( 7 | AppConfig( 8 | appLanguage: appLanguage.languageCode, // 2-letter IETF BCP 47 code 9 | audioLanguage: audioLanguage, // 2-letter IETF BCP 47 code 10 | subtitleLanguage: subtitleLanguage, // 2-letter IETF BCP 47 code 11 | analyticsId: analyticsId, // Can be used by analytics services like NPAW 12 | sessionId: sessionId, // Can be used by analytics services like NPAW 13 | ), 14 | ) 15 | ``` 16 | -------------------------------------------------------------------------------- /doc/advanced-setup/chromecast.md: -------------------------------------------------------------------------------- 1 | ### Chromecast 2 | 3 | Casting requires some extra steps to setup. 4 | 5 | You need a [receiver app ID](https://developers.google.com/cast/docs/overview). Recommend starting with the receiver code at https://github.com/bcc-code/bccm-player-chromecast, as it includes support for default languages (see "App config" docs). That receiver is also available at this appId for testing: `519C9F80`. 6 | 7 | ## Android 8 | 9 | 1. Change your android FlutterActivity to be a FlutterFragmentActivity (required for the native chromecast views): 10 | 11 | ```diff 12 | // android/app/src/main/kotlin/your/bundle/name/MainActivity.kt 13 | - class MainActivity : FlutterActivity() { 14 | + class MainActivity : FlutterFragmentActivity() { 15 | ``` 16 | 17 | 2. Add a cast_app_id to your AndroidManifest.xml 18 | 19 | ```xml 20 | 21 | 24 | ``` 25 | 26 | 3. Update `NormalTheme` in your `styles.xml` to use an AppCompat theme and have a `colorPrimary`. This is a requirement from the Cast SDK. 27 | 28 | ```xml 29 | 33 | ``` 34 | 35 | 4. Update `NormalTheme` in your `night/styles.xml` too: 36 | 37 | ```xml 38 | 42 | ``` 43 | 44 | ## iOS 45 | 46 | 1. Follow the cast sdk documentation on how to add the "NSBonjourServices" and "NSLocalNetworkUsageDescription" plist values: [https://developers.google.com/cast/docs/ios_sender#ios_14](https://developers.google.com/cast/docs/ios_sender#ios_14) 47 | 2. Add your receiver id to your Info.plist: 48 | 49 | ```xml 50 | cast_app_id 51 | ABCD1234 52 | ``` 53 | 54 | 3. Example Info.plist for step 4 and 5: 55 | 56 | ```xml 57 | cast_app_id 58 | 519C9F80 59 | NSLocalNetworkUsageDescription 60 | ${PRODUCT_NAME} uses the local network to discover Cast-enabled devices on your WiFi 61 | network. 62 | NSBonjourServices 63 | 64 | _519C9F80._googlecast._tcp 65 | _googlecast._tcp 66 | 67 | ``` 68 | -------------------------------------------------------------------------------- /doc/advanced-setup/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | ## Riverpod 4 | 5 | The riverpod providers are there to simplify usage of the StateNotifiers and event streams. See [./lib/src/plugins/riverpod/providers](./lib/src/plugins/riverpod/providers) to find available providers. 6 | 7 | ```dart 8 | final String? currentMediaItemEpisodeId = ref.watch( 9 | primaryPlayerProvider.select( 10 | (player) => player?.currentMediaItem?.metadata?.extras?['id']?.asOrNull(), 11 | ), 12 | ); 13 | 14 | final String? currentMediaItemEpisodeId = ref.watch( 15 | playerProviderFor(playerId).select( 16 | (player) => player?.currentMediaItem?.metadata?.extras?['id']?.asOrNull(), 17 | ), 18 | ); 19 | ``` 20 | 21 | ## Npaw / Youbora 22 | 23 | NPAW can be enabled with "setNpawConfig()": 24 | 25 | ```dart 26 | BccmPlayerInterface.instance.setNpawConfig( 27 | NpawConfig( 28 | accountCode: '', 29 | appName: '', 30 | ), 31 | ) 32 | ``` 33 | 34 | It uses title etc from your MediaMetadata by default, but you can customize it via `extras`. 35 | Currently limited to the following properties: 36 | 37 | ```dart 38 | MediaMetadata( 39 | extras: { 40 | 'npaw.content.id': '123', 41 | 'npaw.content.title': 'Live', 42 | 'npaw.content.tvShow': 'Show', 43 | 'npaw.content.season': 'Season', 44 | 'npaw.content.episodeTitle': 'Livestream', 45 | 'npaw.content.isLive': 'true', 46 | 'npaw.isOffline': 'true', 47 | 'npaw.content.type': 'video', 48 | 'npaw.content.customDimension1': 'customDimension1', 49 | 'npaw.content.customDimension2': 'customDimension2', 50 | }, 51 | ); 52 | ``` 53 | 54 | ## For BCC Media apps 55 | 56 | Add the BCC Media playback listener (sends episode progress to API and that kind of stuff). 57 | Add it in main.dart. 58 | 59 | ```dart 60 | BccmPlayerInterface.instance.addPlaybackListener( 61 | BccmPlaybackListener(ref: ref, apiProvider: apiProvider), 62 | ) 63 | ``` 64 | -------------------------------------------------------------------------------- /doc/advanced-usage/airplay.md: -------------------------------------------------------------------------------- 1 | ### Airplay 2 | 3 | An airplay button is not included out-of-the-box because bccm_player uses AVPlayerViewController, AVAudioSession, etc, under-the-hood so it should integrate quite seamlessly with other flutter airplay packages. 4 | 5 | One such package is [flutter_to_airplay](https://pub.dev/packages/flutter_to_airplay): 6 | 7 | ```bash 8 | flutter pub add flutter_to_airplay 9 | ``` 10 | 11 | ````dart 12 | import 'package:bccm_player/bccm_player.dart'; 13 | import 'package:flutter/material.dart'; 14 | import 'package:flutter_to_airplay/flutter_to_airplay.dart'; 15 | 16 | class MyPlayer extends StatelessWidget { 17 | const MyPlayer({super.key}); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return BccmPlayerView( 22 | BccmPlayerController.primary, 23 | config: BccmPlayerViewConfig( 24 | controlsConfig: BccmPlayerControlsConfig( 25 | additionalActionsBuilder: (context) => [ 26 | if (Platform.isIOS) 27 | Padding( 28 | padding: const EdgeInsets.only(right: 4), 29 | child: Transform.scale( 30 | scale: 0.85, 31 | child: const AirPlayRoutePickerView( 32 | width: 20, 33 | height: 34, 34 | prioritizesVideoDevices: true, 35 | tintColor: Colors.white, 36 | activeTintColor: Colors.white, 37 | backgroundColor: Colors.transparent, 38 | ), 39 | ), 40 | ) 41 | ], 42 | ), 43 | ), 44 | ); 45 | } 46 | }``` 47 | ```` 48 | -------------------------------------------------------------------------------- /doc/advanced-usage/hdr-content.md: -------------------------------------------------------------------------------- 1 | # HDR content / SurfaceView 2 | 3 | HDR should work fine on iOS. 4 | 5 | On Android, you need to enable SurfaceViews. However, the reason this isn't enabled by default is because flutter has a bug with surface views. 6 | Check out [this issue on the flutter repo](https://github.com/flutter/flutter/issues/89558). 7 | 8 | You can opt-in to using surfaceViews via the `useSurfaceView` property on BccmPlayerView or VideoPlatformView: 9 | 10 | ```dart 11 | BccmPlayerView( 12 | controller, 13 | config: const BccmPlayerViewConfig( 14 | useSurfaceView: true, 15 | ), 16 | ), 17 | ``` 18 | -------------------------------------------------------------------------------- /doc/advanced-usage/offline.md: -------------------------------------------------------------------------------- 1 | # Offline 2 | 3 | To support offlining, you need to add the DATA_SYNC permission to your AndroidManifest: 4 | 5 | ```xml 6 | 7 | ``` 8 | 9 | Explore the APIs and examples to understand how to use it. 10 | -------------------------------------------------------------------------------- /doc/advanced-usage/orientations.md: -------------------------------------------------------------------------------- 1 | ### Orientations 2 | 3 | The default when entering fullscreen is to force landscape for landscape videos, portrait for portrait videos and allow all if undetermined. 4 | When exiting, the default is to allow all orientations. 5 | 6 | To control orientations, use the `deviceOrientationsNormal`/`deviceOrientationsFullscreen` on BccmPlayerViewConfig. 7 | These callbacks receives the relevant `viewController` as arguments, and should return either a specific list of orientations, or `null` get the default behavior. 8 | 9 | This gives a lot of flexibility for you to determine how to handle the specific scenario. 10 | 11 | #### Example: Force portrait when exiting fullscreen 12 | 13 | If you want to use the default adaptive behavior in fullscreen, but need to force portrait mode in your app, use `deviceOrientationsNormal`: 14 | 15 | ```dart 16 | BccmPlayerViewConfig( 17 | deviceOrientationsNormal: (_) => [DeviceOrientations.portraitUp], 18 | ) 19 | ``` 20 | 21 | #### Example: Assume landscape for uninitialized/square videos 22 | 23 | If you want to force landscape if the video is square or uninitialized, use `deviceOrientationsFullscreen`: 24 | 25 | ```dart 26 | BccmPlayerViewConfig( 27 | deviceOrientationsFullscreen: (viewController) { 28 | final videoSize = viewController.playerController.value.videoSize; 29 | if (videoSize == null || videoSize.aspectRatio == 1) { 30 | return [DeviceOrientation.landscapeLeft]; 31 | } 32 | return null; // return null to get default behavior 33 | }, 34 | ) 35 | ``` 36 | -------------------------------------------------------------------------------- /doc/advanced-usage/player-state.md: -------------------------------------------------------------------------------- 1 | ### Player state 2 | 3 | You can get player state from your controller's "value". 4 | Also, it's a ValueNotifier so you can listen to BccmPlayerController, exactly like other plugins (video_player, etc.). 5 | 6 | Example: 7 | 8 | ```dart 9 | final controller = BccmPlayerController.primary; 10 | controller.value.videoSize.aspectRatio; 11 | ``` 12 | -------------------------------------------------------------------------------- /doc/advanced-usage/primary-player.md: -------------------------------------------------------------------------------- 1 | ### Primary player 2 | 3 | As this plugin is designed for a VOD-type application, the plugin has the concept of a "primary" player. The plugin guarantees a primary player during initialization. 4 | This makes it easy for flutter to know which player to use by default across your app. 5 | The primary player also has some extra superpowers: 6 | 7 | - always available: cant be disposed and is initialized on startup 8 | - it controls what's shown in the notification center 9 | - it automatically transfers the current video to chromecasts when you start a session ([technical details here](#chromecast-technical-details)). 10 | - cast sessions will automatically claim the primaryPlayer (so you don't need extra logic for handling the cast sessions) 11 | 12 | ```dart 13 | // The primary player is automatically initialized on startup 14 | // It's accessible statically via BccmPlayerController.primary: 15 | final controller = BccmPlayerController.primary; 16 | 17 | // Change video with replaceCurrentMediaItem 18 | await controller.replaceCurrentMediaItem( 19 | MediaItem( 20 | url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', 21 | mimeType: 'video/mp4', 22 | metadata: MediaMetadata(title: 'Apple advanced (HLS/HDR)'), 23 | ), 24 | ); 25 | 26 | // Display as usual 27 | final widget = BccmPlayerView(controller); 28 | 29 | // You don't need to (and actually can't) dispose the primary player. 30 | if (!controller.isPrimary) controller.dispose(); 31 | ``` 32 | -------------------------------------------------------------------------------- /doc/advanced-usage/styling.md: -------------------------------------------------------------------------------- 1 | ### Styling / theming 2 | 3 | #### Theme 4 | 5 | You can set a theme to customize some colors and text, similarly to how you set a Material theme. 6 | Wrap with BccmPlayerTheme somewhere high in the hierarchy. 7 | 8 | Example: 9 | 10 | ```dart 11 | BccmPlayerTheme( 12 | playerTheme: BccmPlayerThemeData( 13 | controls: BccmControlsThemeData( 14 | primaryColor: Colors.lightGreen, 15 | durationTextStyle: const TextStyle(color: Colors.green), 16 | ), 17 | ), 18 | child: BccmPlayerView(BccmPlayerController.primary), 19 | ) 20 | ``` 21 | -------------------------------------------------------------------------------- /doc/contributing/basics.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Thank you for the interest in contributing! 4 | We want to improve the codebase so that it's usable for others too, so we are very open for PRs and issues. 5 | The docs has a page about architecture details to help you understand the codebase. 6 | 7 | Before starting on a bigger change it might be a good idea to create an issue about your ideas so that we can help you out and become aligned. 8 | 9 | ### Getting started 10 | 11 | Thank you for wanting to contribute. Here's a few details on how to get started. 12 | 13 | 1. Clone the repo locally. 14 | 2. In a terminal, run `dart run build_runner --watch` and keep it open while developing. 15 | 16 | #### Pigeons (Important) 17 | 18 | Pigeon is used to generate type-safe code for communicating between flutter and the native host (iOS/Android). 19 | Pigeon doesn't use build_runner, so the commands below need to be re-run whenever you change the dart pigeon files. 20 | 21 | ```sh 22 | # When you change playback_platform_pigeon.dart, run: 23 | dart run pigeon --input pigeons/playback_platform_pigeon.dart 24 | 25 | # When you change chromecast_pigeon.dart, run: 26 | dart run pigeon --input pigeons/chromecast_pigeon.dart 27 | ``` 28 | 29 | You will likely need to add things to the pigeons if you are building new features that require writing native code in swift/kotlin. 30 | -------------------------------------------------------------------------------- /doc/contributing/ios.md: -------------------------------------------------------------------------------- 1 | 2 | # IOS setup 3 | 4 | ## Xcode formatting 5 | I recommend using SwiftFormat plus a keyboard shortcut on CMD+S to automatically format your files: https://luisramos.dev/xcode-format-and-save 6 | Xcode 16 adds a builtin formatter though so you can also wait for that. 7 | 8 | -------------------------------------------------------------------------------- /doc/contributing/project_hierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/doc/contributing/project_hierarchy.png -------------------------------------------------------------------------------- /doc/demo/casting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/doc/demo/casting.jpg -------------------------------------------------------------------------------- /doc/demo/casting_fullscreen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/doc/demo/casting_fullscreen.jpg -------------------------------------------------------------------------------- /doc/demo/controls.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/doc/demo/controls.jpg -------------------------------------------------------------------------------- /doc/demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/doc/demo/demo.gif -------------------------------------------------------------------------------- /doc/demo/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/doc/demo/demo.mp4 -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | - Package: [https://pub.dev/packages/bccm_player](https://pub.dev/packages/bccm_player) 4 | 5 | - Github: [https://github.com/bcc-code/bccm-player](https://github.com/bcc-code/bccm-player) 6 | 7 | --- 8 | 9 | 1. Add the dependency 10 | 11 | ```bash 12 | flutter pub add bccm_player 13 | ``` 14 | 15 | 2. (Android) For native fullscreen and picture-in-picture to work correctly, you need to override these 2 methods on your MainActivity: 16 | 17 | ```kotlin 18 | class MainActivity : FlutterFragmentActivity() { 19 | @SuppressLint("MissingSuperCall") 20 | override fun onPictureInPictureModeChanged( 21 | isInPictureInPictureMode: Boolean, 22 | newConfig: Configuration? 23 | ) { 24 | super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) 25 | // This is important for PiP to behave correctly (e.g. pause video when exiting PiP). 26 | val bccmPlayer = 27 | flutterEngine?.plugins?.get(BccmPlayerPlugin::class.javaObjectType) as BccmPlayerPlugin? 28 | bccmPlayer?.handleOnPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) 29 | } 30 | 31 | override fun onBackPressed() { 32 | // This makes the back button work correctly in the native fullscreen player. 33 | // Returns true if the event was handled. 34 | if (!BccmPlayerPlugin.handleOnBackPressed(this)) { 35 | super.onBackPressed() 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | 3. (Android) Add `supportsPictureInPicture="true"` to your AndroidManifest for MainActivity: 42 | 43 | ```xml 44 | 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterRoot = localProperties.getProperty('flutter.sdk') 16 | if (flutterRoot == null) { 17 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 18 | } 19 | 20 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 21 | if (flutterVersionCode == null) { 22 | flutterVersionCode = '1' 23 | } 24 | 25 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 26 | if (flutterVersionName == null) { 27 | flutterVersionName = '1.0' 28 | } 29 | 30 | android { 31 | namespace "media.bcc.bccm_player_example" 32 | compileSdkVersion flutter.compileSdkVersion 33 | 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_11 36 | targetCompatibility JavaVersion.VERSION_11 37 | coreLibraryDesugaringEnabled true 38 | } 39 | 40 | kotlinOptions { 41 | jvmTarget = '11' 42 | } 43 | 44 | sourceSets { 45 | main.java.srcDirs += 'src/main/kotlin' 46 | } 47 | 48 | defaultConfig { 49 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 50 | applicationId "media.bcc.bccm_player_example" 51 | // You can update the following values to match your application needs. 52 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 53 | minSdkVersion 29 54 | targetSdkVersion flutter.targetSdkVersion 55 | versionCode flutterVersionCode.toInteger() 56 | versionName flutterVersionName 57 | } 58 | 59 | buildTypes { 60 | release { 61 | // TODO: Add your own signing config for the release build. 62 | // Signing with the debug keys for now, so `flutter run --release` works. 63 | signingConfig signingConfigs.debug 64 | } 65 | } 66 | } 67 | 68 | flutter { 69 | source '../..' 70 | } 71 | 72 | dependencies { 73 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 74 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' 75 | } 76 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 8 | 9 | 14 | 23 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 41 | 44 | 45 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/media/bcc/bccm_player_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package media.bcc.bccm_player_example 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.res.Configuration 5 | import io.flutter.embedding.android.FlutterFragmentActivity 6 | import media.bcc.bccm_player.BccmPlayerPlugin 7 | 8 | class MainActivity : FlutterFragmentActivity() { 9 | @SuppressLint("MissingSuperCall") 10 | override fun onPictureInPictureModeChanged( 11 | isInPictureInPictureMode: Boolean, 12 | newConfig: Configuration 13 | ) { 14 | super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) 15 | val bccmPlayer = 16 | flutterEngine?.plugins?.get(BccmPlayerPlugin::class.javaObjectType) as BccmPlayerPlugin? 17 | bccmPlayer?.handleOnPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) 18 | } 19 | 20 | override fun onBackPressed() { 21 | if (!BccmPlayerPlugin.handleOnBackPressed(this)) { 22 | super.onBackPressed() 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /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/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 20 | 21 | 25 | 26 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '2.1.0' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:8.10.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4608m 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | kotlin.code.style=official -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue May 27 13:41:35 CEST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 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.10.0" apply false 22 | id "org.jetbrains.kotlin.android" version "2.1.0" 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 | 13.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, '15.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 | - bccm_player (0.0.1): 3 | - Flutter 4 | - google-cast-sdk (= 4.8.3) 5 | - Protobuf (~> 3.13) 6 | - YouboraAVPlayerAdapter (= 6.6.8) 7 | - YouboraLib (= 6.6.22) 8 | - connectivity_plus (0.0.1): 9 | - Flutter 10 | - FlutterMacOS 11 | - Flutter (1.0.0) 12 | - google-cast-sdk (4.8.3): 13 | - Protobuf (~> 3.13) 14 | - package_info_plus (0.4.5): 15 | - Flutter 16 | - Protobuf (3.28.2) 17 | - wakelock_plus (0.0.1): 18 | - Flutter 19 | - YouboraAVPlayerAdapter (6.6.8): 20 | - YouboraAVPlayerAdapter/Default (= 6.6.8) 21 | - YouboraLib (~> 6.5) 22 | - YouboraAVPlayerAdapter/Default (6.6.8): 23 | - YouboraLib (~> 6.5) 24 | - YouboraLib (6.6.22) 25 | 26 | DEPENDENCIES: 27 | - bccm_player (from `.symlinks/plugins/bccm_player/ios`) 28 | - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) 29 | - Flutter (from `Flutter`) 30 | - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 31 | - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) 32 | 33 | SPEC REPOS: 34 | trunk: 35 | - google-cast-sdk 36 | - Protobuf 37 | - YouboraAVPlayerAdapter 38 | - YouboraLib 39 | 40 | EXTERNAL SOURCES: 41 | bccm_player: 42 | :path: ".symlinks/plugins/bccm_player/ios" 43 | connectivity_plus: 44 | :path: ".symlinks/plugins/connectivity_plus/darwin" 45 | Flutter: 46 | :path: Flutter 47 | package_info_plus: 48 | :path: ".symlinks/plugins/package_info_plus/ios" 49 | wakelock_plus: 50 | :path: ".symlinks/plugins/wakelock_plus/ios" 51 | 52 | SPEC CHECKSUMS: 53 | bccm_player: 508fbd6f79e90a6d834da4d2fde56058d9060742 54 | connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db 55 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 56 | google-cast-sdk: 1fb6724e94cc5ff23b359176e0cf6360586bb97a 57 | package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c 58 | Protobuf: 28c89b24435762f60244e691544ed80f50d82701 59 | wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 60 | YouboraAVPlayerAdapter: bc898a61e09c667202a77491b677a42dfa0adc04 61 | YouboraLib: fc65b030ac93499f8beb3f5daa10deb3879fb2d2 62 | 63 | PODFILE CHECKSUM: 57c8aed26fba39d3ec9424816221f294a07c58eb 64 | 65 | COCOAPODS: 1.15.2 66 | -------------------------------------------------------------------------------- /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.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 UIKit 2 | import Flutter 3 | 4 | @main 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/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /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/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/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 | cast_app_id 6 | 519C9F80 7 | NSLocalNetworkUsageDescription 8 | ${PRODUCT_NAME} uses the local network to discover Cast-enabled devices on your WiFi 9 | network. 10 | NSBonjourServices 11 | 12 | _519C9F80._googlecast._tcp 13 | _googlecast._tcp 14 | 15 | CFBundleDevelopmentRegion 16 | $(DEVELOPMENT_LANGUAGE) 17 | CFBundleDisplayName 18 | Bccm Player 19 | CFBundleExecutable 20 | $(EXECUTABLE_NAME) 21 | CFBundleIdentifier 22 | $(PRODUCT_BUNDLE_IDENTIFIER) 23 | CFBundleInfoDictionaryVersion 24 | 6.0 25 | CFBundleName 26 | bccm_player_example 27 | CFBundlePackageType 28 | APPL 29 | CFBundleShortVersionString 30 | $(FLUTTER_BUILD_NAME) 31 | CFBundleSignature 32 | ???? 33 | CFBundleVersion 34 | $(FLUTTER_BUILD_NUMBER) 35 | LSRequiresIPhoneOS 36 | 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIMainStoryboardFile 40 | Main 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | CADisableMinimumFrameDurationOnPhone 57 | 58 | UIApplicationSupportsIndirectInputEvents 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/example_videos.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | 3 | final exampleVideos = [ 4 | MediaItem( 5 | url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', 6 | mimeType: 'video/mp4', 7 | metadata: MediaMetadata( 8 | title: 'Big Buck Bunny (MP4)', 9 | artist: 'Blender Foundation', 10 | artworkUri: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg', 11 | ), 12 | ), 13 | MediaItem( 14 | url: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8', 15 | mimeType: 'application/x-mpegURL', 16 | metadata: MediaMetadata(title: 'Apple BipBop fMP4 (HLS)'), 17 | ), 18 | MediaItem( 19 | url: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8', 20 | mimeType: 'application/x-mpegURL', 21 | metadata: MediaMetadata( 22 | title: 'Apple advanced (HLS/HDR)', 23 | artist: 'Apple Inc.', 24 | artworkUri: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg', 25 | ), 26 | ), 27 | ]; 28 | -------------------------------------------------------------------------------- /example/lib/examples/list_of_players.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | import 'package:bccm_player_example/example_videos.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class ListOfPlayers extends StatefulWidget { 6 | const ListOfPlayers({super.key}); 7 | 8 | @override 9 | State createState() => _ListOfPlayersState(); 10 | } 11 | 12 | class _ListOfPlayersState extends State { 13 | late List controllers; 14 | 15 | @override 16 | void initState() { 17 | controllers = [ 18 | BccmPlayerController.empty(), 19 | ...exampleVideos.map( 20 | (e) => BccmPlayerController(e), 21 | ), 22 | BccmPlayerController.primary, 23 | ]; 24 | for (final controller in controllers) { 25 | controller.initialize().then((_) => controller.setMixWithOthers(true)); 26 | } 27 | super.initState(); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | for (var element in controllers) { 33 | if (!element.isPrimary) element.dispose(); 34 | } 35 | super.dispose(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return ListView( 41 | children: [ 42 | ...controllers.map( 43 | (controller) => Column( 44 | children: [ 45 | BccmPlayerView(controller), 46 | ElevatedButton( 47 | onPressed: () { 48 | controller.setPrimary(); 49 | }, 50 | child: const Text('Make primary')), 51 | ], 52 | ), 53 | ) 54 | ], 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/lib/examples/native_controls.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class NativeControls extends StatefulWidget { 5 | const NativeControls({super.key}); 6 | 7 | @override 8 | State createState() => _NativeControlsState(); 9 | } 10 | 11 | class _NativeControlsState extends State { 12 | @override 13 | Widget build(BuildContext context) { 14 | return ListView( 15 | children: [ 16 | Column( 17 | children: [ 18 | VideoPlatformView( 19 | playerController: BccmPlayerController.primary, 20 | showControls: true, 21 | ), 22 | ], 23 | ), 24 | ], 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/lib/examples/simple_player.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class SimplePlayer extends StatefulWidget { 5 | const SimplePlayer({super.key}); 6 | 7 | @override 8 | State createState() => _SimplePlayerState(); 9 | } 10 | 11 | class _SimplePlayerState extends State { 12 | late BccmPlayerController playerController; 13 | 14 | @override 15 | void initState() { 16 | playerController = BccmPlayerController( 17 | MediaItem( 18 | url: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8', 19 | mimeType: 'application/x-mpegURL', 20 | metadata: MediaMetadata(title: 'Apple advanced (HLS/HDR)'), 21 | ), 22 | ); 23 | playerController.initialize().then((_) => playerController.setMixWithOthers(true)); // if you want to play together with other videos 24 | super.initState(); 25 | } 26 | 27 | @override 28 | void dispose() { 29 | playerController.dispose(); 30 | super.dispose(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return ListView( 36 | children: [ 37 | Column( 38 | children: [ 39 | BccmPlayerView(playerController), 40 | ElevatedButton( 41 | onPressed: () { 42 | playerController.setPrimary(); 43 | }, 44 | child: const Text('Make primary'), 45 | ), 46 | ElevatedButton( 47 | onPressed: () { 48 | final currentMs = playerController.value.playbackPositionMs; 49 | if (currentMs != null) { 50 | playerController.seekTo(Duration(milliseconds: currentMs + 20000)); 51 | } 52 | }, 53 | child: const Text('Skip 20 seconds'), 54 | ), 55 | ], 56 | ), 57 | ], 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /example/lib/generated_plugin_registrant.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // ignore_for_file: directives_ordering 6 | // ignore_for_file: lines_longer_than_80_chars 7 | // ignore_for_file: depend_on_referenced_packages 8 | 9 | import 'package:bccm_player/bccm_player_web.dart'; 10 | 11 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 12 | 13 | // ignore: public_member_api_docs 14 | void registerPlugins(Registrar registrar) { 15 | BccmPlayerWeb.registerWith(registrar); 16 | registrar.registerMessageHandler(); 17 | } 18 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bccm_player/bccm_player.dart'; 4 | import 'package:bccm_player_example/examples/queue.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | 8 | import 'examples/list_of_players.dart'; 9 | import 'examples/playground.dart'; 10 | import 'examples/native_controls.dart'; 11 | import 'examples/custom_controls.dart'; 12 | import 'examples/simple_player.dart'; 13 | import 'examples/downloader.dart'; 14 | 15 | Future main() async { 16 | await BccmPlayerInterface.instance.setup(); 17 | 18 | // FocusDebugger.instance.activate(); 19 | 20 | runApp(const MyApp()); 21 | } 22 | 23 | class MyApp extends StatefulWidget { 24 | const MyApp({super.key}); 25 | 26 | @override 27 | State createState() => _MyAppState(); 28 | } 29 | 30 | class _MyAppState extends State { 31 | @override 32 | Widget build(BuildContext context) { 33 | return MediaQuery( 34 | data: MediaQuery.of(context).copyWith(navigationMode: NavigationMode.directional), 35 | child: Shortcuts( 36 | shortcuts: { 37 | LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), 38 | }, 39 | child: DefaultTabController( 40 | length: 7, 41 | child: MaterialApp( 42 | home: Scaffold( 43 | appBar: AppBar( 44 | title: const Text('Plugin example app'), 45 | actions: const [ 46 | Padding( 47 | padding: EdgeInsets.only(right: 16), 48 | child: CastButton(color: Colors.white), 49 | ), 50 | ], 51 | bottom: const TabBar(tabs: [ 52 | Tab(text: 'Queue'), 53 | Tab(text: 'Playground'), 54 | Tab(text: 'List Of Players'), 55 | Tab(text: 'Simple player'), 56 | Tab(text: 'Custom controls'), 57 | Tab(text: 'Native controls'), 58 | Tab(text: 'Downloader') 59 | ]), 60 | ), 61 | // tabs with Playground #1 then a new "ListOfPlayers" tab at #2 and controls to navigate between the tabs 62 | body: const TabBarView( 63 | children: [ 64 | QueueExample(), 65 | Playground(), 66 | ListOfPlayers(), 67 | SimplePlayer(), 68 | CustomControls(), 69 | NativeControls(), 70 | Downloader(), 71 | ], 72 | ), 73 | ), 74 | ), 75 | ), 76 | ), 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/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 | bccm_player_example 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bccm_player_example", 3 | "short_name": "bccm_player_example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "Demonstrates how to use the bccm_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/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Assets/Group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcc-code/bccm-player/6c400ab87eb1959ffbf49d91142b8c4c12f4f759/ios/Assets/Group.png -------------------------------------------------------------------------------- /ios/Classes/BccmPlayerPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface BccmPlayerPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /ios/Classes/BccmPlayerPlugin.m: -------------------------------------------------------------------------------- 1 | #import "BccmPlayerPlugin.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 "bccm_player-Swift.h" 9 | #endif 10 | 11 | @implementation BccmPlayerPlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftBccmPlayerPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Classes/Downloader/DownloaderState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloaderState.swift 3 | // bccm_player 4 | // 5 | // Created by Coen Jan Wessels on 05/09/2023. 6 | // 7 | 8 | public struct DownloaderState: Codable { 9 | public struct Track: Codable { 10 | public let id: String 11 | public let label: String? 12 | public let language: String? 13 | public let frameRate: Double? 14 | public let bitrate: Int? 15 | public let width: Int? 16 | public let height: Int? 17 | public let isSelected: Bool 18 | } 19 | 20 | public struct TaskInput: Codable { 21 | public let url: URL 22 | public let mimeType: String 23 | public let title: String 24 | public let audioTrackIds: [String] 25 | public let videoTrackIds: [String] 26 | public let additionalData: [String: String] 27 | } 28 | 29 | public struct TaskState: Codable { 30 | public let key: UUID 31 | public let input: TaskInput 32 | public var tempOfflineUrl: URL? = nil 33 | public var bookmark: Data? = nil 34 | public var statusCode: Int = DownloadStatus.paused.rawValue 35 | public var error: String? = nil 36 | public var progress: Double = 0.0 37 | } 38 | 39 | var tasks: [String: TaskState] 40 | 41 | mutating func updateTask(task: TaskState) -> DownloaderState { 42 | tasks[task.key.uuidString] = task 43 | return self 44 | } 45 | } 46 | 47 | extension DownloaderState.TaskInput { 48 | var downloadConfig: DownloadConfig { 49 | DownloadConfig(url: url.absoluteString, 50 | mimeType: mimeType, 51 | title: title, 52 | audioTrackIds: audioTrackIds, 53 | videoTrackIds: videoTrackIds, 54 | additionalData: additionalData) 55 | } 56 | } 57 | 58 | extension DownloaderState.TaskState { 59 | func getUrlFromBookmark() -> URL? { 60 | return bookmark.flatMap { 61 | var staleBookmark = false 62 | let url = try? URL(resolvingBookmarkData: $0, bookmarkDataIsStale: &staleBookmark) 63 | return url 64 | } 65 | } 66 | 67 | func toDownloadModel() -> Download { 68 | let status = DownloadStatus(rawValue: statusCode) ?? .failed 69 | 70 | return Download(key: key.uuidString, 71 | config: input.downloadConfig, 72 | offlineUrl: getUrlFromBookmark()?.absoluteString, 73 | fractionDownloaded: progress, 74 | status: status, 75 | error: error) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ios/Classes/Downloader/MediaInfoFetcher.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | 3 | enum MediaInfoFetcher { 4 | static func fetchInfo(for url: URL, mimeType: String?) async throws -> MediaInfo { 5 | let asset = AVURLAsset(url: url, 6 | options: mimeType != nil ? ["AVURLAssetOutOfBandMIMETypeKey": mimeType!] : nil) 7 | 8 | try await withCheckedThrowingContinuation { continuation in 9 | asset.loadValuesAsynchronously(forKeys: ["tracks", "variants"]) { 10 | var error: NSError? = nil 11 | switch asset.statusOfValue(forKey: "tracks", error: &error) { 12 | case .loaded: 13 | continuation.resume() 14 | case .failed: 15 | 16 | continuation.resume(throwing: error ?? NSError()) 17 | case .cancelled: 18 | continuation.resume(throwing: error ?? NSError()) 19 | default: 20 | continuation.resume() 21 | } 22 | } 23 | } 24 | 25 | let audioTracks: [Track] = TrackUtils.getAudioTracksForAsset(asset, playerItem: nil) 26 | let textTracks: [Track] = TrackUtils.getTextTracksForAsset(asset, playerItem: nil) 27 | let videoTracks: [Track] = TrackUtils.getVideoTracksForAsset(asset, playerItem: nil) 28 | 29 | return MediaInfo.make(withAudioTracks: audioTracks, textTracks: textTracks, videoTracks: videoTracks) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ios/Classes/DownloaderApiImpl.swift: -------------------------------------------------------------------------------- 1 | 2 | class DownloaderApiImpl: NSObject, DownloaderPigeon { 3 | public let downloader: Downloader 4 | 5 | init(downloader: Downloader) { 6 | self.downloader = downloader 7 | } 8 | 9 | func startDownload(downloadConfig: DownloadConfig, completion: @escaping (Result) -> Void) { 10 | Task { 11 | do { 12 | let download = try await downloader.startDownload(config: downloadConfig) 13 | completion(.success(download)) 14 | } catch { 15 | completion(.failure(error)) 16 | } 17 | } 18 | } 19 | 20 | func getDownloadStatus(downloadKey: String, completion: @escaping (Result) -> Void) { 21 | Task { 22 | do { 23 | let progress = try await downloader.progress(forKey: downloadKey) 24 | completion(.success(progress)) 25 | } catch { 26 | completion(.failure(error)) 27 | } 28 | } 29 | } 30 | 31 | func getDownloads(completion: @escaping (Result<[Download], Error>) -> Void) { 32 | completion(Result(catching: { 33 | downloader.getAll() 34 | })) 35 | } 36 | 37 | func getDownload(downloadKey: String, completion: @escaping (Result) -> Void) { 38 | completion(Result(catching: { 39 | downloader.get(forKey: downloadKey) 40 | })) 41 | } 42 | 43 | func removeDownload(downloadKey: String, completion: @escaping (Result) -> Void) { 44 | completion(Result(catching: { 45 | try downloader.remove(download: downloadKey) 46 | })) 47 | } 48 | 49 | func getFreeDiskSpace(completion: @escaping (Result) -> Void) { 50 | completion(Result(catching: { 51 | try _getFreeDiskSpace() 52 | })) 53 | } 54 | 55 | private func _getFreeDiskSpace() throws -> Double { 56 | let fileURL = URL(fileURLWithPath: NSHomeDirectory() as String) 57 | do { 58 | let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey]) 59 | if let capacity = values.volumeAvailableCapacityForImportantUsage { 60 | return Double(capacity) 61 | } 62 | throw FlutterError(code: "diskspace", message: "Failed getting disk space", details: nil) 63 | } catch { 64 | print("Error retrieving capacity: \(error.localizedDescription)") 65 | throw FlutterError(code: "diskspace", message: error.localizedDescription, details: nil) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ios/Classes/Pigeon/ChromecastPigeon.h: -------------------------------------------------------------------------------- 1 | // Autogenerated from Pigeon (v22.7.4), do not edit directly. 2 | // See also: https://pub.dev/packages/pigeon 3 | 4 | #import 5 | 6 | @protocol FlutterBinaryMessenger; 7 | @protocol FlutterMessageCodec; 8 | @class FlutterError; 9 | @class FlutterStandardTypedData; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @class CastSessionUnavailableEvent; 14 | 15 | @interface CastSessionUnavailableEvent : NSObject 16 | + (instancetype)makeWithPlaybackPositionMs:(nullable NSNumber *)playbackPositionMs; 17 | @property(nonatomic, strong, nullable) NSNumber * playbackPositionMs; 18 | @end 19 | 20 | /// The codec used by all APIs. 21 | NSObject *nullGetChromecastPigeonCodec(void); 22 | 23 | /// An API called by the native side to notify about chromecast changes 24 | @interface ChromecastPigeon : NSObject 25 | - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; 26 | - (instancetype)initWithBinaryMessenger:(id)binaryMessenger messageChannelSuffix:(nullable NSString *)messageChannelSuffix; 27 | - (void)onSessionEnded:(void (^)(FlutterError *_Nullable))completion; 28 | - (void)onSessionEnding:(void (^)(FlutterError *_Nullable))completion; 29 | - (void)onSessionResumeFailed:(void (^)(FlutterError *_Nullable))completion; 30 | - (void)onSessionResumed:(void (^)(FlutterError *_Nullable))completion; 31 | - (void)onSessionResuming:(void (^)(FlutterError *_Nullable))completion; 32 | - (void)onSessionStartFailed:(void (^)(FlutterError *_Nullable))completion; 33 | - (void)onSessionStarted:(void (^)(FlutterError *_Nullable))completion; 34 | - (void)onSessionStarting:(void (^)(FlutterError *_Nullable))completion; 35 | - (void)onSessionSuspended:(void (^)(FlutterError *_Nullable))completion; 36 | - (void)onCastSessionAvailable:(void (^)(FlutterError *_Nullable))completion; 37 | - (void)onCastSessionUnavailable:(CastSessionUnavailableEvent *)event completion:(void (^)(FlutterError *_Nullable))completion; 38 | @end 39 | 40 | NS_ASSUME_NONNULL_END 41 | -------------------------------------------------------------------------------- /ios/Classes/Players/PlayerController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerController.swift 3 | // bccm_player 4 | // 5 | // Created by Andreas Gangsø on 19/09/2022. 6 | // 7 | 8 | import AVFoundation 9 | import Foundation 10 | 11 | public protocol PlayerController { 12 | var id: String { get } 13 | var mixWithOthers: Bool { get set } 14 | var manuallySelectedAudioLanguage: String? { get set } 15 | func setNpawConfig(npawConfig: NpawConfig?) 16 | func updateAppConfig(appConfig: AppConfig?) 17 | func getCurrentItem() -> MediaItem? 18 | func getPlayerTracksSnapshot() -> PlayerTracksSnapshot 19 | func setSelectedTrack(type: TrackType, trackId: String?) 20 | func setPlaybackSpeed(_ speed: Float) 21 | func setVolume(_ speed: Float) 22 | func setRepeatMode(_ repeatMode: RepeatMode) 23 | func getPlayerStateSnapshot() -> PlayerStateSnapshot 24 | func replaceCurrentMediaItem(_ mediaItem: MediaItem, autoplay: NSNumber?, completion: ((FlutterError?) -> Void)?) 25 | func play() 26 | func seekTo(_ positionMs: Int64, _ completion: @escaping (Bool) -> Void) 27 | func pause() 28 | func stop(reset: Bool) 29 | func exitFullscreen() 30 | func enterFullscreen() 31 | func hasBecomePrimary() 32 | } 33 | -------------------------------------------------------------------------------- /ios/Classes/SwiftBccmPlayerPlugin.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | import Combine 3 | import Flutter 4 | import GoogleCast 5 | import UIKit 6 | 7 | public class SwiftBccmPlayerPlugin: NSObject, FlutterPlugin { 8 | static var cancellables: [AnyCancellable] = [] 9 | 10 | public static func register(with registrar: FlutterPluginRegistrar) { 11 | setupCast() 12 | let messenger = registrar.messenger() 13 | let channel = FlutterMethodChannel(name: "bccm_player", binaryMessenger: messenger) 14 | let instance = SwiftBccmPlayerPlugin() 15 | registrar.addMethodCallDelegate(instance, channel: channel) 16 | registrar.addApplicationDelegate(instance) 17 | 18 | let playbackListener = PlaybackListenerPigeon(binaryMessenger: messenger) 19 | let queueManagerPigeon = QueueManagerPigeon(binaryMessenger: messenger) 20 | 21 | let chromecastPigeon = ChromecastPigeon(binaryMessenger: messenger) 22 | let playbackApi = PlaybackApiImpl(chromecastPigeon: chromecastPigeon, playbackListener: playbackListener, queueManagerPigeon: queueManagerPigeon) 23 | 24 | let downloaderListener = DownloaderListenerPigeon(binaryMessenger: messenger) 25 | let downloader = Downloader() 26 | cancellables.append(contentsOf: [ 27 | downloader.changeEvents.sink { event in 28 | downloaderListener.onDownloadStatusChanged(event: event) { _ in } 29 | }, 30 | downloader.removeEvents.sink { event in 31 | downloaderListener.onDownloadRemoved(event: event) { _ in } 32 | }, 33 | downloader.failEvents.sink { event in 34 | downloaderListener.onDownloadFailed(event: event) { _ in } 35 | } 36 | ]) 37 | 38 | registrar.register( 39 | BccmPlayerFactory(messenger: messenger, playbackApi: playbackApi), 40 | withId: "bccm-player") 41 | registrar.register( 42 | CastPlayerViewFactory(messenger: messenger, playbackApi: playbackApi), 43 | withId: "bccm-cast-player") 44 | registrar.register( 45 | CastButtonFactory(messenger: messenger, playbackApi: playbackApi), 46 | withId: "bccm_player/cast_button") 47 | 48 | SetUpPlaybackPlatformPigeon(registrar.messenger(), playbackApi) 49 | DownloaderPigeonSetup.setUp(binaryMessenger: registrar.messenger(), api: DownloaderApiImpl(downloader: downloader)) 50 | 51 | UIApplication.shared.beginReceivingRemoteControlEvents() 52 | } 53 | 54 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 55 | result("iOS " + UIDevice.current.systemVersion) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ios/Classes/Utils/AsyncUtils.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> ()) { 5 | URLSession.shared.dataTask(with: url, completionHandler: completion).resume() 6 | } 7 | 8 | func getData(from url: URL) async throws -> Data? { 9 | return try await withUnsafeThrowingContinuation { continuation in 10 | getData(from: url) { data, response, error in 11 | if let error = error { 12 | continuation.resume(throwing: error) 13 | } else { 14 | continuation.resume(returning: data) 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ios/Classes/Utils/CastSetup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CastSetup.swift 3 | // bccm_player 4 | // 5 | // Created by Andreas Gangsø on 27/04/2023. 6 | // 7 | 8 | import Foundation 9 | import GoogleCast 10 | 11 | func setupCast() { 12 | guard let appId = Bundle.main.object(forInfoDictionaryKey: "cast_app_id") as? String else { 13 | return 14 | } 15 | let criteria = GCKDiscoveryCriteria(applicationID: appId) 16 | let options = GCKCastOptions(discoveryCriteria: criteria) 17 | GCKCastContext.setSharedInstanceWith(options) 18 | let styler = GCKUIStyle.sharedInstance() 19 | styler.castViews.mediaControl.expandedController.backgroundImageContentMode = UIImageView.ContentMode.scaleAspectFit.rawValue as NSNumber 20 | styler.castViews.mediaControl.expandedController.backgroundColor = UIColor(red: CGFloat(13/255.0), green: CGFloat(22/255.0), blue: CGFloat(55/255.0), alpha: CGFloat(1.0)) 21 | GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = false 22 | GCKCastContext.sharedInstance().discoveryManager.startDiscovery() 23 | } 24 | -------------------------------------------------------------------------------- /ios/Classes/Utils/Colors.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func uiColorFromHex(hexValue: Int) -> UIColor { 4 | let red = CGFloat((hexValue & 0x00FF0000) >> 16) 5 | let green = CGFloat((hexValue & 0x0000FF00) >> 8) 6 | let blue = CGFloat(hexValue & 0x000000FF) 7 | let alpha = CGFloat((hexValue & 0xFF000000) >> 24) 8 | debugPrint("red: \(red), green: \(green), blue: \(blue), alpha: \(alpha)") 9 | return UIColor(red: red / 255, green: green / 255, blue: blue / 255, alpha: alpha / 255) 10 | } 11 | -------------------------------------------------------------------------------- /ios/Classes/Utils/Errors.swift: -------------------------------------------------------------------------------- 1 | enum BccmPlayerError: Error { 2 | case runtimeError(String) 3 | } 4 | -------------------------------------------------------------------------------- /ios/Classes/Utils/LandscapeAVPlayerViewController.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | 3 | class LandscapeAVPlayerViewController: AVPlayerViewController { 4 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask { 5 | return .landscape 6 | } 7 | 8 | override var shouldAutorotate: Bool { 9 | return true 10 | } 11 | 12 | override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { 13 | return .landscapeRight 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ios/Classes/Utils/MediaItemMapper.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import AVKit 3 | import Foundation 4 | import MediaPlayer 5 | 6 | class MediaItemMapper { 7 | static func mapPlayerItem(_ playerItem: AVPlayerItem?) -> MediaItem? { 8 | guard let playerItem = playerItem else { 9 | return nil 10 | } 11 | guard let asset = (playerItem.asset as? AVURLAsset) else { 12 | return nil 13 | } 14 | 15 | var metadata: MediaMetadata? 16 | var playerData: [String: String]? 17 | if #available(iOS 12.2, *) { 18 | let extras = MetadataUtils.getNamespacedMetadata(playerItem.externalMetadata, namespace: .BccmExtras) 19 | playerData = MetadataUtils.getNamespacedMetadata(playerItem.externalMetadata, namespace: .BccmPlayer) 20 | let artworkUri: String? = playerData?[PlayerMetadataConstants.ArtworkUri] 21 | metadata = MediaMetadata.make( 22 | withArtworkUri: artworkUri, 23 | title: playerItem.externalMetadata.first(where: { $0.identifier == AVMetadataIdentifier.commonIdentifierTitle })?.stringValue, 24 | artist: playerItem.externalMetadata.first(where: { $0.identifier == AVMetadataIdentifier.commonIdentifierArtist })?.stringValue, 25 | durationMs: !playerItem.duration.seconds.isFinite ? nil : NSNumber(floatLiteral: playerItem.duration.seconds * 1000), 26 | extras: extras 27 | ) 28 | } 29 | let mimeType: String? = playerData?[PlayerMetadataConstants.MimeType] 30 | 31 | var isLive = CMTIME_IS_INDEFINITE(playerItem.duration) 32 | if let isLiveMeta = playerData?[PlayerMetadataConstants.IsLive] { 33 | isLive = isLiveMeta == "true" 34 | } 35 | 36 | var isOffline: Bool? = playerData?[PlayerMetadataConstants.IsOffline] == "true" 37 | 38 | let id = playerData?[PlayerMetadataConstants.Id] ?? UUID().uuidString 39 | 40 | let mediaItem = MediaItem.make( 41 | withId: id, 42 | url: asset.url.absoluteString, 43 | mimeType: mimeType, 44 | metadata: metadata, 45 | isLive: isLive as NSNumber, 46 | isOffline: isOffline as NSNumber?, 47 | playbackStartPositionMs: nil, 48 | lastKnownAudioLanguage: playerItem.getSelectedAudioLanguage(), 49 | lastKnownSubtitleLanguage: playerItem.getSelectedSubtitleLanguage() 50 | ) 51 | return mediaItem 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ios/Classes/Utils/PeakBitrateController.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | import Foundation 3 | 4 | public class PeakBitrateController { 5 | private var requestedPeakBitrate: Double = 0 6 | private var audioOnlyMode: Bool = false 7 | var playerCurrentItemObserver: NSKeyValueObservation? 8 | 9 | let player: AVPlayer 10 | init(player: AVPlayer) { 11 | self.player = player 12 | playerCurrentItemObserver = player.observe(\.currentItem, options: [.new, .old], changeHandler: { _, change in 13 | if change.newValue != nil { 14 | self.updateInternalValue() 15 | } 16 | }) 17 | } 18 | 19 | func value() -> Double { 20 | return requestedPeakBitrate 21 | } 22 | 23 | func setAudioOnlyMode(_ v: Bool) { 24 | audioOnlyMode = v 25 | updateInternalValue() 26 | } 27 | 28 | func setPeakBitrate(_ v: Double) { 29 | requestedPeakBitrate = v 30 | updateInternalValue() 31 | } 32 | 33 | private func updateInternalValue() { 34 | if audioOnlyMode { 35 | debugPrint("bccm: Setting preferredPeakBitRate to 1 (Audio only)") 36 | player.currentItem?.preferredPeakBitRate = 1 37 | } else { 38 | debugPrint("bccm: Setting preferredPeakBitRate to \(requestedPeakBitrate).") 39 | player.currentItem?.preferredPeakBitRate = requestedPeakBitrate 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ios/Classes/Utils/PigeonExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PigeonExtensions.swift 3 | // bccm_player 4 | // 5 | // Created by Andreas Gangsø on 19/06/2023. 6 | // 7 | 8 | import AVKit 9 | import Foundation 10 | 11 | extension TrackType { 12 | func asAVMediaCharacteristic() -> AVMediaCharacteristic? { 13 | if self == .audio { 14 | return .audible 15 | } else if self == .text { 16 | return .legible 17 | } else if self == .video { 18 | return .visual 19 | } else { 20 | return nil 21 | } 22 | } 23 | } 24 | 25 | extension MediaMetadata { 26 | /// Because swift crashes when reading a NSDictionary with null values 27 | func safeExtras() -> [String: Any]? { 28 | return value(forKey: "extras") as? [String: Any] 29 | } 30 | } 31 | 32 | extension [String?: String?] { 33 | func removeNil() -> [String: String] { 34 | return reduce(into: [:]) { result, x in 35 | if x.key != nil, x.value != nil { 36 | result[x.key!] = x.value! 37 | } 38 | } 39 | } 40 | } 41 | 42 | extension FlutterError: Error {} 43 | 44 | /** 45 | Helper function for catching Swift errors and returing the tuple format, Flutter pigeon requires. 46 | 47 | For all other errors, it will generate a default FlutterError, based on the description of the error. 48 | */ 49 | public func returnFlutterResult(_ exec: () async throws -> T?) async -> (T?, FlutterError?) { 50 | do { 51 | return try await (exec(), nil) 52 | } catch let error as FlutterError { 53 | return (nil, error) 54 | } catch { 55 | return (nil, FlutterError(code: "unknown", message: error.localizedDescription, details: nil)) 56 | } 57 | } 58 | 59 | public func returnFlutterResult(_ exec: () async throws -> ()) async -> FlutterError? { 60 | do { 61 | try await exec() 62 | } catch let error as FlutterError { 63 | return error 64 | } catch { 65 | return FlutterError(code: "unknown", message: error.localizedDescription, details: nil) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | public func returnFlutterResult(_ exec: () async throws -> InputValue) async -> (InputValue.Value?, FlutterError?) 72 | where InputValue: FlutterValueConvertible 73 | { 74 | await returnFlutterResult { try await exec().flutterValue } 75 | } 76 | 77 | public protocol FlutterValueConvertible { 78 | associatedtype Value 79 | 80 | var flutterValue: Value { get } 81 | } 82 | 83 | extension Double: FlutterValueConvertible { 84 | public typealias Value = NSNumber 85 | 86 | public var flutterValue: NSNumber { 87 | NSNumber(value: self) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ios/Classes/Utils/PlayerMetadataConstants.swift: -------------------------------------------------------------------------------- 1 | 2 | enum MetadataNamespace: String { 3 | // Using namespaces because if we used e.g. json it would be painful to deserialize if we add any large values in the future 4 | case BccmPlayer = "media.bcc.player" 5 | case BccmExtras = "media.bcc.extras" 6 | } 7 | 8 | enum PlayerMetadataConstants { 9 | // TODO: refactor to use a PlayerData class instead of serializing into string dicts [String: String] 10 | static let MimeType = "mime_type" 11 | static let IsLive = "is_live" 12 | static let IsOffline = "is_offline" 13 | static let ArtworkUri = "artwork_uri" 14 | static let Id = "id" 15 | } 16 | -------------------------------------------------------------------------------- /ios/Classes/Utils/SimpleGCKRequestDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CastRequestDelegate.swift 3 | // bccm_player 4 | // 5 | // Created by Andreas Gangsø on 19/06/2023. 6 | // 7 | 8 | import Foundation 9 | import GoogleCast 10 | 11 | class SimpleGCKRequestDelegate: NSObject, GCKRequestDelegate { 12 | let didComplete: (() -> Void)? 13 | let didFailWithError: ((GCKError) -> Void)? 14 | let didAbort: ((GCKRequestAbortReason) -> Void)? 15 | 16 | init(didComplete: (() -> Void)?, didFailWithError: ((GCKError) -> Void)?, didAbort: ((GCKRequestAbortReason) -> Void)?) { 17 | self.didComplete = didComplete 18 | self.didFailWithError = didFailWithError 19 | self.didAbort = didAbort 20 | } 21 | 22 | func requestDidComplete(_ request: GCKRequest) { 23 | didComplete?() 24 | } 25 | 26 | func request(_ request: GCKRequest, didFailWithError error: GCKError) { 27 | didFailWithError?(error) 28 | } 29 | 30 | func request(_ request: GCKRequest, didAbortWith abortReason: GCKRequestAbortReason) { 31 | didAbort?(abortReason) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ios/Classes/Views/CastButtonView.swift: -------------------------------------------------------------------------------- 1 | 2 | import AVKit 3 | import Flutter 4 | import Foundation 5 | import GoogleCast 6 | import UIKit 7 | 8 | class CastButtonFactory: NSObject, FlutterPlatformViewFactory { 9 | private var messenger: FlutterBinaryMessenger 10 | private var playbackApi: PlaybackApiImpl 11 | 12 | init(messenger: FlutterBinaryMessenger, playbackApi: PlaybackApiImpl) { 13 | self.messenger = messenger 14 | self.playbackApi = playbackApi 15 | super.init() 16 | } 17 | 18 | public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { 19 | return FlutterStandardMessageCodec.sharedInstance() 20 | } 21 | 22 | func create( 23 | withFrame frame: CGRect, 24 | viewIdentifier viewId: Int64, 25 | arguments args: Any? 26 | ) -> FlutterPlatformView { 27 | var color: UIColor? 28 | if let argsDict = args as? [String: Any] { 29 | if let colorInt = argsDict["color"] as? Int { 30 | color = uiColorFromHex(hexValue: colorInt) 31 | } 32 | } 33 | return CastButton( 34 | frame: frame, 35 | viewId: viewId, 36 | color: color 37 | ) 38 | } 39 | } 40 | 41 | class CastButton: NSObject, FlutterPlatformView { 42 | private var _buttonView: GCKUICastButton 43 | private var _viewId: Int64 44 | 45 | init(frame: CGRect, viewId: Int64, color: UIColor?) { 46 | _buttonView = GCKUICastButton(frame: frame) 47 | if color != nil { 48 | _buttonView.tintColor = color 49 | } else { 50 | _buttonView.tintColor = UIColor(red: 110/255, green: 176/255, blue: 230/255, alpha: 1) 51 | } 52 | _viewId = viewId 53 | super.init() 54 | } 55 | 56 | func view() -> UIView { 57 | return _buttonView 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ios/Classes/Views/CastPlayerView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import GoogleCast 3 | 4 | class CastPlayerViewFactory: NSObject, FlutterPlatformViewFactory { 5 | private var messenger: FlutterBinaryMessenger 6 | private var playbackApi: PlaybackApiImpl 7 | 8 | init(messenger: FlutterBinaryMessenger, playbackApi: PlaybackApiImpl) { 9 | self.messenger = messenger 10 | self.playbackApi = playbackApi 11 | super.init() 12 | } 13 | 14 | public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { 15 | return FlutterStandardMessageCodec.sharedInstance() 16 | } 17 | 18 | func create( 19 | withFrame frame: CGRect, 20 | viewIdentifier viewId: Int64, 21 | arguments args: Any? 22 | ) -> FlutterPlatformView { 23 | let argDictionary = args as! [String: Any]? 24 | let playerController = playbackApi.getPlayer(CastPlayerController.DEFAULT_ID) 25 | guard let pc = playerController as? CastPlayerController else { 26 | fatalError("Playercontroller is of unknown type.") 27 | } 28 | return CastPlayerView(frame: frame, playerController: pc) 29 | } 30 | } 31 | 32 | class CastPlayerView: NSObject, FlutterPlatformView { 33 | private var _view: UIView = .init() 34 | private var _playerController: CastPlayerController 35 | 36 | init( 37 | frame: CGRect, 38 | playerController: CastPlayerController 39 | ) { 40 | _view.frame = frame 41 | _playerController = playerController 42 | super.init() 43 | 44 | // iOS views can be created here 45 | 46 | createNativeView(frame: frame, view: _view) 47 | } 48 | 49 | func view() -> UIView { 50 | return _view 51 | } 52 | 53 | deinit { 54 | // _playerController?.player?.pause() 55 | } 56 | 57 | func createNativeView(frame: CGRect, view _view: UIView) { 58 | // GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls() 59 | 60 | // _view.addSubview(castPlayerViewController.view) 61 | // _view.addSubview(nativeLabel) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ios/Classes/bccm_player.h: -------------------------------------------------------------------------------- 1 | 2 | #import "PlaybackPlatformApi.h" 3 | -------------------------------------------------------------------------------- /ios/bccm_player.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint bccm_player.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'bccm_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.platform = :ios, '13.0' 19 | 20 | # Flutter.framework does not contain a i386 slice. 21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 22 | s.swift_version = '5.0' 23 | s.static_framework = true 24 | s.dependency 'YouboraLib', '6.6.22' 25 | s.dependency 'YouboraAVPlayerAdapter', '6.6.8' 26 | 27 | # Protobuf is for cast 28 | s.dependency 'Protobuf', '~> 3.13' 29 | s.dependency 'google-cast-sdk', '4.8.3' 30 | 31 | end 32 | 33 | -------------------------------------------------------------------------------- /lib/bccm_player.dart: -------------------------------------------------------------------------------- 1 | export 'src/state/inherited_player_view_controller.dart'; 2 | 3 | export 'src/pigeon/playback_platform_pigeon.g.dart' 4 | show 5 | MediaItem, 6 | MediaMetadata, 7 | AppConfig, 8 | NpawConfig, 9 | CastConnectionState, 10 | PlaybackState, 11 | PlaybackEndedEvent, 12 | MediaItemTransitionEvent, 13 | PlaybackStateChangedEvent, 14 | MediaInfo, 15 | Track, 16 | VideoSize, 17 | TrackType, 18 | PlayerTracksSnapshot, 19 | PositionDiscontinuityEvent, 20 | RepeatMode, 21 | PlayerError, 22 | BufferMode, 23 | PictureInPictureModeChangedEvent; 24 | export 'src/state/player_state_notifier.dart'; 25 | export 'src/state/plugin_state_notifier.dart'; 26 | export 'src/state/player_controller.dart'; 27 | export 'src/native/chromecast_events.dart'; 28 | export 'src/playback_platform_interface.dart'; 29 | export 'src/widgets/video/player_view.dart'; 30 | export 'src/widgets/video/video_platform_view.dart'; 31 | export 'src/state/player_view_controller.dart'; 32 | export 'src/widgets/cast/cast_button.dart'; 33 | export 'src/widgets/mini_player/mini_player.dart'; 34 | export 'src/widgets/controls/play_next_button.dart'; 35 | export 'src/widgets/utils/bccm_player_state_builder.dart'; 36 | export 'src/widgets/controls/tv/tv_controls.dart'; 37 | export 'src/model/player_view_config.dart'; 38 | export 'src/theme/controls_theme_data.dart'; 39 | export 'src/theme/bccm_player_theme.dart'; 40 | export 'src/theme/player_theme.dart'; 41 | export 'src/theme/mini_player_theme_data.dart'; 42 | export 'src/pigeon/pigeon_extensions.dart'; 43 | export 'src/utils/time.dart' show calcTimeLeftMs; 44 | export 'src/utils/use_wakelock_while_palying.dart'; 45 | 46 | export 'src/pigeon/downloader_pigeon.g.dart' 47 | show DownloadConfig, Download, DownloadChangedEvent, DownloadRemovedEvent, DownloadFailedEvent, DownloadStatus; 48 | export 'src/downloader_platform_interface.dart'; 49 | export 'src/state/texture.dart'; 50 | -------------------------------------------------------------------------------- /lib/controls.dart: -------------------------------------------------------------------------------- 1 | export 'src/widgets/controls/controls_wrapper.dart'; 2 | export 'src/widgets/controls/control_fade_out.dart'; 3 | export 'src/widgets/controls/default_controls.dart'; 4 | export 'src/widgets/controls/default/settings.dart'; 5 | export 'src/widgets/controls/default/settings_option_list.dart'; 6 | export 'src/widgets/controls/default/time_skip_button.dart'; 7 | export 'src/widgets/controls/default/play_pause_button.dart'; 8 | export 'src/utils/timeline.dart'; 9 | export 'src/widgets/cast/cast_player.dart'; 10 | export 'src/widgets/controls/smooth_video_progress.dart'; 11 | -------------------------------------------------------------------------------- /lib/plugins/bcc_media.dart: -------------------------------------------------------------------------------- 1 | export '../src/plugins/bcc_media/bccm_playback_listener.dart'; 2 | -------------------------------------------------------------------------------- /lib/plugins/riverpod.dart: -------------------------------------------------------------------------------- 1 | export '../src/plugins/riverpod/providers.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/native/chromecast_events.dart: -------------------------------------------------------------------------------- 1 | import '../pigeon/chromecast_pigeon.g.dart'; 2 | 3 | extension ChromecastStreamExtensions on Stream { 4 | Stream on() { 5 | if (T == dynamic) { 6 | return this as Stream; 7 | } else { 8 | return where((event) => event is T).cast(); 9 | } 10 | } 11 | } 12 | 13 | abstract class ChromecastEvent {} 14 | 15 | class SessionEnded extends ChromecastEvent {} 16 | 17 | class SessionEnding extends ChromecastEvent {} 18 | 19 | class SessionResumeFailed extends ChromecastEvent {} 20 | 21 | class SessionResumed extends ChromecastEvent {} 22 | 23 | class SessionResuming extends ChromecastEvent {} 24 | 25 | class SessionStartFailed extends ChromecastEvent {} 26 | 27 | class SessionStarted extends ChromecastEvent {} 28 | 29 | class SessionStarting extends ChromecastEvent {} 30 | 31 | class SessionSuspended extends ChromecastEvent {} 32 | 33 | class CastSessionAvailable extends ChromecastEvent {} 34 | 35 | class CastSessionUnavailable extends ChromecastEvent { 36 | int? playbackPositionMs; 37 | CastSessionUnavailable({this.playbackPositionMs}); 38 | 39 | factory CastSessionUnavailable.from(CastSessionUnavailableEvent event) { 40 | return CastSessionUnavailable(playbackPositionMs: event.playbackPositionMs); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/native/chromecast_pigeon_listener.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../pigeon/chromecast_pigeon.g.dart'; 4 | import 'chromecast_events.dart'; 5 | 6 | class ChromecastPigeonListener implements ChromecastPigeon { 7 | ChromecastPigeonListener(); 8 | StreamController streamController = StreamController.broadcast(); 9 | Stream get stream => streamController.stream; 10 | 11 | @override 12 | void onSessionEnded() { 13 | streamController.add(SessionEnded()); 14 | } 15 | 16 | @override 17 | void onSessionEnding() { 18 | streamController.add(SessionEnding()); 19 | } 20 | 21 | @override 22 | void onSessionResumeFailed() { 23 | streamController.add(SessionResumeFailed()); 24 | } 25 | 26 | @override 27 | void onSessionResumed() { 28 | streamController.add(SessionResumed()); 29 | } 30 | 31 | @override 32 | void onSessionResuming() { 33 | streamController.add(SessionResuming()); 34 | } 35 | 36 | @override 37 | void onSessionStartFailed() { 38 | streamController.add(SessionStartFailed()); 39 | } 40 | 41 | @override 42 | void onSessionStarted() { 43 | streamController.add(SessionStarted()); 44 | } 45 | 46 | @override 47 | void onSessionStarting() { 48 | streamController.add(SessionStarting()); 49 | } 50 | 51 | @override 52 | void onSessionSuspended() { 53 | streamController.add(SessionSuspended()); 54 | } 55 | 56 | @override 57 | void onCastSessionAvailable() { 58 | streamController.add(CastSessionAvailable()); 59 | } 60 | 61 | @override 62 | void onCastSessionUnavailable(CastSessionUnavailableEvent event) { 63 | streamController.add(CastSessionUnavailable.from(event)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/native/root_pigeon_playback_listener.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../pigeon/playback_platform_pigeon.g.dart'; 4 | 5 | /// The primary listener that just forwards the events to the other listeners 6 | class RootPigeonPlaybackListener implements PlaybackListenerPigeon { 7 | RootPigeonPlaybackListener(); 8 | final List _listeners = []; 9 | final StreamController _streamController = StreamController.broadcast(); 10 | 11 | Stream get stream => _streamController.stream; 12 | 13 | void addListener(listener) { 14 | _listeners.add(listener); 15 | } 16 | 17 | void removeListener(listener) { 18 | _listeners.remove(listener); 19 | } 20 | 21 | // PlaybackListenerPigeon implementation 22 | 23 | @override 24 | void onPlaybackStateChanged(event) { 25 | _streamController.add(event); 26 | for (var listener in _listeners) { 27 | listener.onPlaybackStateChanged(event); 28 | } 29 | } 30 | 31 | @override 32 | void onPlaybackEnded(event) { 33 | _streamController.add(event); 34 | for (var listener in _listeners) { 35 | listener.onPlaybackEnded(event); 36 | } 37 | } 38 | 39 | @override 40 | void onMediaItemTransition(event) { 41 | _streamController.add(event); 42 | for (var listener in _listeners) { 43 | listener.onMediaItemTransition(event); 44 | } 45 | } 46 | 47 | @override 48 | void onPictureInPictureModeChanged(event) { 49 | _streamController.add(event); 50 | for (var listener in _listeners) { 51 | listener.onPictureInPictureModeChanged(event); 52 | } 53 | } 54 | 55 | @override 56 | void onPositionDiscontinuity(event) { 57 | _streamController.add(event); 58 | for (var listener in _listeners) { 59 | listener.onPositionDiscontinuity(event); 60 | } 61 | } 62 | 63 | @override 64 | void onPlayerStateUpdate(event) { 65 | _streamController.add(event); 66 | for (var listener in _listeners) { 67 | listener.onPlayerStateUpdate(event); 68 | } 69 | } 70 | 71 | @override 72 | void onPrimaryPlayerChanged(playerId) { 73 | for (var listener in _listeners) { 74 | listener.onPrimaryPlayerChanged(playerId); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/native/root_queue_manager_pigeon.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | import 'package:bccm_player/src/pigeon/playback_platform_pigeon.g.dart'; 3 | import 'package:bccm_player/src/queue/queue_controller.dart'; 4 | 5 | class RootQueueManagerPigeon implements QueueManagerPigeon { 6 | QueueManager? _getQueueManager(String playerId) { 7 | return BccmPlayerInterface.instance.stateNotifier.getPlayerNotifier(playerId)?.queueManager; 8 | } 9 | 10 | @override 11 | Future skipToNext(String playerId) async { 12 | return _getQueueManager(playerId)?.skipToNext(); 13 | } 14 | 15 | @override 16 | Future skipToPrevious(String playerId) async { 17 | return _getQueueManager(playerId)?.skipToPrevious(); 18 | } 19 | 20 | @override 21 | Future handlePlaybackEnded(String playerId, MediaItem? current) async { 22 | return _getQueueManager(playerId)?.handlePlaybackEnded(current); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/pigeon/pigeon_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/src/pigeon/playback_platform_pigeon.g.dart'; 2 | 3 | extension TrackX on Track { 4 | String get labelWithFallback { 5 | if (height != null) { 6 | var conditionalFrameRate = ''; 7 | if (frameRate != null && frameRate != 30) { 8 | conditionalFrameRate = ' (${frameRate!.toInt().toString()}fps)'; 9 | } 10 | 11 | return "${height}p$conditionalFrameRate"; 12 | } 13 | return (label ?? language ?? id); // + (downloaded == true ? " (downloaded)" : "") 14 | } 15 | } 16 | 17 | extension TrackListX on List { 18 | Iterable get safe => whereType(); 19 | } 20 | 21 | extension VideoSizeX on VideoSize { 22 | double get aspectRatio => width / height; 23 | } 24 | 25 | const autoTrackId = "auto"; 26 | -------------------------------------------------------------------------------- /lib/src/plugins/riverpod/providers.dart: -------------------------------------------------------------------------------- 1 | export './providers/player_provider.dart'; 2 | export './providers/plugin_state_provider.dart'; 3 | export './providers/chromecast_provider.dart'; 4 | export './providers/player_event_stream_provider.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/plugins/riverpod/providers/chromecast_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:riverpod/riverpod.dart'; 3 | import '../../../playback_platform_interface.dart'; 4 | import '../../../native/chromecast_events.dart'; 5 | 6 | final chromecastEventStreamProvider = Provider>((ref) { 7 | return BccmPlayerInterface.instance.chromecastEventStream; 8 | }); 9 | -------------------------------------------------------------------------------- /lib/src/plugins/riverpod/providers/player_event_stream_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod/riverpod.dart'; 2 | 3 | import '../../../../bccm_player.dart'; 4 | 5 | final playerEventRawStreamProvider = Provider>((ref) { 6 | return BccmPlayerInterface.instance.playerEventStream; 7 | }); 8 | 9 | final playerEventStreamProvider = Provider.family, String>((ref, playerId) { 10 | return BccmPlayerInterface.instance.playerEventStream.where((event) { 11 | try { 12 | return event.playerId == playerId; 13 | } catch (e) { 14 | return false; // hacky try-catch because pigeon doesn't support inheritance: https://github.com/flutter/flutter/issues/117819 15 | } 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /lib/src/plugins/riverpod/providers/player_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod/riverpod.dart'; 2 | import '../../../../bccm_player.dart'; 3 | import 'plugin_state_provider.dart'; 4 | 5 | final playerProviderFor = StateNotifierProvider.family((ref, playerId) { 6 | return ref.watch( 7 | pluginStateProvider.select((value) => value.players[playerId] ?? PlayerStateNotifier(keepAlive: false)), 8 | ); 9 | }); 10 | 11 | final primaryPlayerProvider = StateNotifierProvider((ref) { 12 | final playerNotifier = ref.watch( 13 | pluginStateProvider.select((value) => value.players[value.primaryPlayerId]), 14 | ); 15 | if (playerNotifier == null) return PlayerStateNotifier(keepAlive: false); 16 | return playerNotifier; 17 | }); 18 | -------------------------------------------------------------------------------- /lib/src/plugins/riverpod/providers/plugin_state_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod/riverpod.dart'; 2 | import '../../../../bccm_player.dart'; 3 | 4 | final pluginStateProvider = StateNotifierProvider((ref) { 5 | return BccmPlayerInterface.instance.stateNotifier; 6 | }); 7 | -------------------------------------------------------------------------------- /lib/src/queue/queue_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | abstract class QueueManager { 6 | void dispose(); 7 | Future skipToNext(); 8 | Future skipToPrevious(); 9 | Future handlePlaybackEnded(MediaItem? current); 10 | Future setShuffleEnabled(bool enabled); 11 | Future setNextUp(List mediaItems); 12 | Future addQueueItem(MediaItem mediaItem); 13 | Future removeQueueItem(String id); 14 | Future moveQueueItem(int fromIndex, int toIndex); 15 | Future clearQueue(); 16 | 17 | @internal 18 | void setPlayer(PlayerStateNotifier playerStateNotifier) {} 19 | 20 | ValueNotifier get shuffleEnabled; 21 | ValueNotifier> get history; 22 | ValueNotifier> get queue; 23 | ValueNotifier> get nextUp; 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/state/inherited_player_view_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../bccm_player.dart'; 4 | 5 | class InheritedBccmPlayerViewController extends InheritedNotifier { 6 | final BccmPlayerViewController controller; 7 | 8 | const InheritedBccmPlayerViewController({ 9 | super.key, 10 | required this.controller, 11 | required super.child, 12 | }) : super(notifier: controller); 13 | 14 | static BccmPlayerViewController of(BuildContext context) { 15 | return context.dependOnInheritedWidgetOfExactType()!.controller; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/state/plugin_state_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'player_state_notifier.dart'; 4 | import 'package:freezed_annotation/freezed_annotation.dart'; 5 | import 'package:state_notifier/state_notifier.dart'; 6 | 7 | part 'plugin_state_notifier.freezed.dart'; 8 | 9 | class PlayerPluginStateNotifier extends StateNotifier { 10 | final bool keepAlive; 11 | PlayerPluginStateNotifier({required this.keepAlive}) 12 | : super( 13 | const PlayerPluginState( 14 | primaryPlayerId: null, 15 | players: {}, 16 | ), 17 | ); 18 | 19 | @override 20 | // ignore: must_call_super 21 | void dispose({bool? force}) { 22 | // prevents riverpods StateNotifierProvider from disposing it 23 | if (!keepAlive || force == true) { 24 | super.dispose(); 25 | } 26 | } 27 | 28 | void setPrimaryPlayer(String? playerId) { 29 | if (playerId != null) { 30 | getOrAddPlayerNotifier(playerId); 31 | } 32 | state = state.copyWith(primaryPlayerId: playerId); 33 | } 34 | 35 | String? getPrimaryPlayerId() { 36 | return state.primaryPlayerId; 37 | } 38 | 39 | PlayerStateNotifier? getPlayerNotifier(String playerId) { 40 | return state.players[playerId]; 41 | } 42 | 43 | PlayerStateNotifier getOrAddPlayerNotifier(String playerId) { 44 | final existing = state.players[playerId]; 45 | if (existing?.mounted == true) return existing!; 46 | return _createPlayerNotifier(playerId); 47 | } 48 | 49 | void _removePlayer(String playerId) { 50 | debugPrint('removing playerId: $playerId'); 51 | final player = state.players[playerId]; 52 | if (player != null) { 53 | state = state.copyWith(players: {...state.players}..remove(playerId)); 54 | player.dispose(force: true); 55 | } 56 | } 57 | 58 | PlayerStateNotifier _createPlayerNotifier(String playerId) { 59 | final notifier = PlayerStateNotifier( 60 | keepAlive: true, 61 | onDispose: () => _removePlayer(playerId), 62 | player: PlayerState(playerId: playerId, isInitialized: true), 63 | ); 64 | state = state.copyWith( 65 | players: {...state.players, playerId: notifier}, 66 | ); 67 | return notifier; 68 | } 69 | } 70 | 71 | @freezed 72 | abstract class PlayerPluginState with _$PlayerPluginState { 73 | const factory PlayerPluginState({ 74 | required String? primaryPlayerId, 75 | required Map players, 76 | }) = _PlayerPluginState; 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/state/state_playback_listener.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/src/pigeon/playback_platform_pigeon.g.dart'; 2 | import 'package:bccm_player/src/state/plugin_state_notifier.dart'; 3 | import '../utils/extensions.dart'; 4 | 5 | class StatePlaybackListener implements PlaybackListenerPigeon { 6 | StatePlaybackListener(this.pluginStateNotifier); 7 | 8 | PlayerPluginStateNotifier pluginStateNotifier; 9 | 10 | @override 11 | void onPlaybackStateChanged(event) { 12 | pluginStateNotifier.getOrAddPlayerNotifier(event.playerId) 13 | ..setIsBuffering(event.isBuffering) 14 | ..setPlaybackState(event.playbackState) 15 | ..resyncPlaybackPositionTimer(); 16 | } 17 | 18 | @override 19 | void onPlaybackEnded(event) {} 20 | 21 | @override 22 | void onMediaItemTransition(event) { 23 | pluginStateNotifier.getOrAddPlayerNotifier(event.playerId).setMediaItem(event.mediaItem); 24 | } 25 | 26 | @override 27 | void onPictureInPictureModeChanged(event) { 28 | pluginStateNotifier.getOrAddPlayerNotifier(event.playerId).setIsInPipMode(event.isInPipMode); 29 | } 30 | 31 | @override 32 | void onPositionDiscontinuity(event) { 33 | final positionMs = event.playbackPositionMs?.finiteOrNull()?.round(); 34 | pluginStateNotifier.getOrAddPlayerNotifier(event.playerId) 35 | ..setPlaybackPosition(positionMs) 36 | ..resyncPlaybackPositionTimer(); 37 | } 38 | 39 | @override 40 | void onPlayerStateUpdate(event) { 41 | pluginStateNotifier.getOrAddPlayerNotifier(event.playerId).setStateFromSnapshot(event.snapshot); 42 | } 43 | 44 | @override 45 | void onPrimaryPlayerChanged(event) { 46 | pluginStateNotifier.setPrimaryPlayer(event.playerId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/state/texture.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | 3 | class BccmTexture { 4 | final int textureId; 5 | 6 | BccmTexture._internal(this.textureId); 7 | 8 | static Future create() async { 9 | final textureId = await BccmPlayerInterface.instance.createVideoTexture(); 10 | return BccmTexture._internal(textureId); 11 | } 12 | 13 | Future dispose() async { 14 | await BccmPlayerInterface.instance.disposeVideoTexture(textureId); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/theme/bccm_player_theme.dart: -------------------------------------------------------------------------------- 1 | export 'player_theme.dart'; 2 | export 'mini_player_theme_data.dart'; 3 | export 'controls_theme_data.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/theme/mini_player_theme_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BccmMiniPlayerThemeData { 4 | final Color? iconColor; 5 | final Color? backgroundColor; 6 | final Color? thumbnailBorderColor; 7 | final Color? topBorderColor; 8 | final Color? progressColor; 9 | final TextStyle? titleStyle; 10 | final TextStyle? secondaryTitleStyle; 11 | 12 | BccmMiniPlayerThemeData({ 13 | this.iconColor, 14 | this.backgroundColor, 15 | this.thumbnailBorderColor, 16 | this.topBorderColor, 17 | this.progressColor, 18 | this.titleStyle, 19 | this.secondaryTitleStyle, 20 | }); 21 | 22 | factory BccmMiniPlayerThemeData.defaultTheme(BuildContext context) { 23 | final theme = Theme.of(context); 24 | return BccmMiniPlayerThemeData( 25 | iconColor: theme.colorScheme.onSurface, 26 | backgroundColor: theme.colorScheme.surface, 27 | thumbnailBorderColor: Colors.white.withOpacity(0.01), 28 | topBorderColor: theme.colorScheme.onSurface.withOpacity(0.1), 29 | progressColor: theme.colorScheme.onSurface, 30 | titleStyle: theme.textTheme.labelMedium!.copyWith(color: theme.colorScheme.onSurface), 31 | secondaryTitleStyle: theme.textTheme.labelSmall?.copyWith(color: theme.colorScheme.primary), 32 | ); 33 | } 34 | 35 | BccmMiniPlayerThemeData fillWithDefaults(BccmMiniPlayerThemeData defaults) { 36 | return BccmMiniPlayerThemeData( 37 | iconColor: iconColor ?? defaults.iconColor, 38 | backgroundColor: backgroundColor ?? defaults.backgroundColor, 39 | thumbnailBorderColor: thumbnailBorderColor ?? defaults.thumbnailBorderColor, 40 | topBorderColor: topBorderColor ?? defaults.topBorderColor, 41 | progressColor: progressColor ?? defaults.progressColor, 42 | titleStyle: titleStyle ?? defaults.titleStyle, 43 | secondaryTitleStyle: secondaryTitleStyle ?? defaults.secondaryTitleStyle, 44 | ); 45 | } 46 | 47 | BccmMiniPlayerThemeData copyWith({ 48 | Color? iconColor, 49 | Color? backgroundColor, 50 | Color? thumbnailBorderColor, 51 | Color? topBorderColor, 52 | Color? progressColor, 53 | TextStyle? titleStyle, 54 | TextStyle? secondaryTitleStyle, 55 | }) { 56 | return BccmMiniPlayerThemeData( 57 | iconColor: iconColor ?? this.iconColor, 58 | backgroundColor: backgroundColor ?? this.backgroundColor, 59 | thumbnailBorderColor: thumbnailBorderColor ?? this.thumbnailBorderColor, 60 | topBorderColor: topBorderColor ?? this.topBorderColor, 61 | progressColor: progressColor ?? this.progressColor, 62 | titleStyle: titleStyle ?? this.titleStyle, 63 | secondaryTitleStyle: secondaryTitleStyle ?? this.secondaryTitleStyle, 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/theme/player_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/src/utils/extensions.dart'; 2 | import 'package:bccm_player/src/theme/controls_theme_data.dart'; 3 | import 'package:bccm_player/src/theme/mini_player_theme_data.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class BccmPlayerTheme extends InheritedWidget { 7 | BccmPlayerTheme({ 8 | super.key, 9 | required this.playerTheme, 10 | Widget Function(BuildContext)? builder, 11 | Widget? child, 12 | }) : assert(child != null || builder != null, "Either child or builder must be set."), 13 | assert(child == null || builder == null, "You cant set both child and builder at the same time."), 14 | super(child: Builder(builder: builder ?? (context) => child!)); 15 | 16 | final BccmPlayerThemeData playerTheme; 17 | 18 | @override 19 | bool updateShouldNotify(BccmPlayerTheme oldWidget) => oldWidget.playerTheme != playerTheme; 20 | static BccmPlayerThemeData? read(BuildContext context) => 21 | context.getElementForInheritedWidgetOfExactType()?.widget.asOrNull()?.playerTheme; 22 | static BccmPlayerThemeData? maybeOf(BuildContext context) => context.dependOnInheritedWidgetOfExactType()?.playerTheme; 23 | static BccmPlayerThemeData rawOf(BuildContext context) => context.dependOnInheritedWidgetOfExactType()!.playerTheme; 24 | 25 | static BccmPlayerThemeData safeOf(BuildContext context) { 26 | final theme = context.dependOnInheritedWidgetOfExactType()?.playerTheme; 27 | final defaults = BccmPlayerThemeData.defaultTheme(context); 28 | return theme?.fillWithDefaults(defaults) ?? defaults; 29 | } 30 | } 31 | 32 | class BccmPlayerThemeData { 33 | BccmPlayerThemeData({this.miniPlayer, this.controls}); 34 | 35 | final BccmMiniPlayerThemeData? miniPlayer; 36 | final BccmControlsThemeData? controls; 37 | 38 | factory BccmPlayerThemeData.defaultTheme(BuildContext context) { 39 | return BccmPlayerThemeData( 40 | miniPlayer: BccmMiniPlayerThemeData.defaultTheme(context), 41 | controls: BccmControlsThemeData.defaultTheme(context), 42 | ); 43 | } 44 | 45 | BccmPlayerThemeData fillWithDefaults(BccmPlayerThemeData defaults) { 46 | return BccmPlayerThemeData( 47 | miniPlayer: miniPlayer?.fillWithDefaults(defaults.miniPlayer!) ?? defaults.miniPlayer, 48 | controls: controls?.fillWithDefaults(defaults.controls!) ?? defaults.controls, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/utils/debouncer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'dart:async'; 3 | 4 | /// If two calls are made within [milliseconds], the first one will be cancelled. 5 | /// If [debounceInitial] is true (default is false), the first call will also have a delay 6 | class Debouncer { 7 | final int milliseconds; 8 | Timer? _timer; 9 | VoidCallback? _currentAction; 10 | bool debounceInitial; 11 | 12 | Debouncer({ 13 | required this.milliseconds, 14 | this.debounceInitial = true, 15 | }); 16 | 17 | run(VoidCallback action) { 18 | // If first call 19 | if (debounceInitial == false && _timer?.isActive != true) { 20 | action(); 21 | _currentAction = () {}; 22 | } else { 23 | _currentAction = action; 24 | } 25 | _timer?.cancel(); 26 | _timer = Timer(Duration(milliseconds: milliseconds), _currentAction!); 27 | } 28 | 29 | forceEarly() { 30 | _timer?.cancel(); 31 | _currentAction?.call(); 32 | } 33 | } 34 | 35 | /// A class which upon calling run() replaces the current pending action with a new one, 36 | /// and executes the pending action when the current future is done. 37 | /// It differes from a debouncer in that it doesnt use any timers. 38 | class OneAsyncAtATime { 39 | Completer? _currentCompleter; 40 | Future Function()? _nextAction; 41 | 42 | OneAsyncAtATime(); 43 | 44 | Future runWhenCurrentIsDone(Future Function() action) async { 45 | _nextAction = action; 46 | if (_currentCompleter == null) { 47 | await _goNext(); 48 | } 49 | } 50 | 51 | Future _goNext() async { 52 | if (_nextAction == null) return; 53 | _currentCompleter = Completer(); 54 | try { 55 | final action = _nextAction; 56 | _nextAction = null; 57 | await action!(); 58 | _currentCompleter?.complete(); 59 | } catch (e) { 60 | _currentCompleter?.completeError(e); 61 | } 62 | _currentCompleter = null; 63 | _goNext(); 64 | } 65 | 66 | void reset() { 67 | _nextAction = null; 68 | _currentCompleter?.completeError('disposed'); 69 | _currentCompleter = null; 70 | } 71 | 72 | bool get hasPending => _nextAction != null; 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/utils/extensions.dart: -------------------------------------------------------------------------------- 1 | extension AsExtension on Object? { 2 | X as() => this as X; 3 | X? asOrNull() { 4 | var self = this; 5 | return self is X ? self : null; 6 | } 7 | } 8 | 9 | extension FiniteOrNull on double { 10 | double? finiteOrNull() { 11 | return isFinite ? this : null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/utils/num.dart: -------------------------------------------------------------------------------- 1 | double safeDouble(double input) => input.isNaN || !input.isFinite ? 0 : input.toDouble(); 2 | int safeInt(int input) => input.isNaN || !input.isFinite ? 0 : input; 3 | -------------------------------------------------------------------------------- /lib/src/utils/svg_icons.dart: -------------------------------------------------------------------------------- 1 | class SvgIcons { 2 | static const close = ''' 3 | 4 | 5 | '''; 6 | 7 | static const play = ''' 8 | 9 | 10 | '''; 11 | 12 | static const pause = ''' 13 | 14 | 15 | 16 | '''; 17 | 18 | static const castButton = ''' 19 | 20 | 21 | ic_cast_black_24dp 22 | Created with Sketch. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | '''; 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/utils/time.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | /// Convert milliseconds to 'hh:mm:ss' 4 | String getFormattedDuration(num durationMs) { 5 | final duration = Duration(milliseconds: durationMs.toInt()); 6 | return [ 7 | if (duration.inHours != 0) duration.inHours.toString().padLeft(2, '0'), 8 | (duration.inMinutes % 60).toString().padLeft(2, '0'), 9 | (duration.inSeconds % 60).toString().padLeft(2, '0') 10 | ].join(':'); 11 | } 12 | 13 | double calcTimeLeftMs({required num? duration, required num? currentMs}) { 14 | currentMs ??= 0; 15 | duration ??= 0; 16 | if (!duration.isFinite) duration = 0; 17 | if (!currentMs.isFinite) currentMs = 0; 18 | 19 | return max(0, (duration - currentMs).toDouble()); 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/utils/transparent_image.dart: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | Copyright (c) 2018 Brian Egan 4 | https://github.com/brianegan/transparent_image 5 | */ 6 | 7 | library transparent_image; 8 | 9 | import 'dart:typed_data'; 10 | 11 | final Uint8List kTransparentImage = Uint8List.fromList([ 12 | 0x89, 13 | 0x50, 14 | 0x4E, 15 | 0x47, 16 | 0x0D, 17 | 0x0A, 18 | 0x1A, 19 | 0x0A, 20 | 0x00, 21 | 0x00, 22 | 0x00, 23 | 0x0D, 24 | 0x49, 25 | 0x48, 26 | 0x44, 27 | 0x52, 28 | 0x00, 29 | 0x00, 30 | 0x00, 31 | 0x01, 32 | 0x00, 33 | 0x00, 34 | 0x00, 35 | 0x01, 36 | 0x08, 37 | 0x06, 38 | 0x00, 39 | 0x00, 40 | 0x00, 41 | 0x1F, 42 | 0x15, 43 | 0xC4, 44 | 0x89, 45 | 0x00, 46 | 0x00, 47 | 0x00, 48 | 0x0A, 49 | 0x49, 50 | 0x44, 51 | 0x41, 52 | 0x54, 53 | 0x78, 54 | 0x9C, 55 | 0x63, 56 | 0x00, 57 | 0x01, 58 | 0x00, 59 | 0x00, 60 | 0x05, 61 | 0x00, 62 | 0x01, 63 | 0x0D, 64 | 0x0A, 65 | 0x2D, 66 | 0xB4, 67 | 0x00, 68 | 0x00, 69 | 0x00, 70 | 0x00, 71 | 0x49, 72 | 0x45, 73 | 0x4E, 74 | 0x44, 75 | 0xAE, 76 | 0x42, 77 | 0x60, 78 | 0x82, 79 | ]); 80 | -------------------------------------------------------------------------------- /lib/src/utils/use_wakelock_while_palying.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:wakelock_plus/wakelock_plus.dart'; 4 | 5 | /// Enables wakelock while playing and while this widget is mounted. 6 | void useWakelockWhilePlaying(BccmPlayerController player) { 7 | useEffect(() { 8 | void listener() { 9 | if (player.value.playbackState != PlaybackState.paused) { 10 | WakelockPlus.enable(); 11 | } else { 12 | WakelockPlus.disable(); 13 | } 14 | } 15 | 16 | player.addListener(listener); 17 | return () { 18 | player.removeListener(listener); 19 | WakelockPlus.disable(); 20 | }; 21 | }, [player]); 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/web/js/bccm_video_player.dart: -------------------------------------------------------------------------------- 1 | @JS('window.bccmVideoPlayer') 2 | library bccm_video_player; 3 | 4 | import 'package:js/js.dart'; 5 | 6 | @JS() 7 | external void createPlayer(String? elementId, Options options); 8 | 9 | @JS() 10 | @anonymous 11 | class Options { 12 | external factory Options({ 13 | SrcOptions src, 14 | LanguagePreferenceDefaults languagePreferenceDefaults, 15 | VideoJsOptions? videojs, 16 | NpawOptions? npaw, 17 | }); 18 | } 19 | 20 | @JS() 21 | @anonymous 22 | class SrcOptions { 23 | external factory SrcOptions({String type, String src}); 24 | } 25 | 26 | @JS() 27 | @anonymous 28 | class LanguagePreferenceDefaults { 29 | external factory LanguagePreferenceDefaults({String? audio, String? subtitles}); 30 | } 31 | 32 | @JS() 33 | @anonymous 34 | class VideoJsOptions { 35 | external factory VideoJsOptions({bool? autoplay, bool? fluid}); 36 | } 37 | 38 | @JS() 39 | @anonymous 40 | class NpawOptions { 41 | external factory NpawOptions({ 42 | bool? enabled, 43 | String? accountCode, 44 | NpawTrackingOptions? tracking, 45 | }); 46 | } 47 | 48 | @JS() 49 | @anonymous 50 | class NpawTrackingOptions { 51 | external factory NpawTrackingOptions({ 52 | bool? isLive, 53 | String? userId, 54 | String? sessionId, 55 | String? ageGroup, 56 | NpawMetadataOptions? metadata, 57 | }); 58 | } 59 | 60 | @JS() 61 | @anonymous 62 | class NpawMetadataOptions { 63 | external factory NpawMetadataOptions({ 64 | String? contentId, 65 | String? title, 66 | String? episodeTitle, 67 | String? seasonTitle, 68 | String? seasonId, 69 | String? showTitle, 70 | String? showId, 71 | Map? overrides, 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/widgets/cast/cast_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/gestures.dart'; 3 | import 'package:flutter/rendering.dart'; 4 | import 'package:universal_io/io.dart'; 5 | 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/services.dart'; 8 | 9 | class CastButton extends StatelessWidget { 10 | const CastButton({super.key, this.color}); 11 | 12 | final methodChannel = const MethodChannel('bccm_player/cast_button'); 13 | final Color? color; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final creationParams = { 18 | if (color != null) 'color': color!.value, 19 | }; 20 | if (Platform.isAndroid) { 21 | return SizedBox( 22 | width: 24, 23 | child: _Android(creationParams: creationParams), 24 | ); 25 | } else if (Platform.isIOS && const String.fromEnvironment('IS_MAESTRO_TEST', defaultValue: 'false') != 'true') { 26 | return SizedBox( 27 | width: 24, 28 | child: UiKitView( 29 | viewType: 'bccm_player/cast_button', 30 | creationParams: creationParams, 31 | creationParamsCodec: const StandardMessageCodec(), 32 | ), 33 | ); 34 | } 35 | return Container(); 36 | } 37 | } 38 | 39 | class _Android extends StatelessWidget { 40 | const _Android({ 41 | required this.creationParams, 42 | }); 43 | 44 | final Map creationParams; 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return PlatformViewLink( 49 | viewType: 'bccm_player/cast_button', 50 | surfaceFactory: (context, controller) { 51 | return AndroidViewSurface( 52 | controller: controller as AndroidViewController, 53 | hitTestBehavior: PlatformViewHitTestBehavior.opaque, 54 | gestureRecognizers: { 55 | Factory(() => TapGestureRecognizer()), 56 | Factory(() => HorizontalDragGestureRecognizer()), 57 | }, 58 | ); 59 | }, 60 | onCreatePlatformView: (params) { 61 | return PlatformViewsService.initAndroidView( 62 | id: params.id, 63 | viewType: 'bccm_player/cast_button', 64 | layoutDirection: TextDirection.ltr, 65 | creationParams: creationParams, 66 | creationParamsCodec: const StandardMessageCodec(), 67 | onFocus: () { 68 | params.onFocusChanged(true); 69 | }, 70 | ) 71 | ..addOnPlatformViewCreatedListener((val) { 72 | params.onPlatformViewCreated(val); 73 | }) 74 | ..create(); 75 | }, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/widgets/cast/cast_player.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | import 'package:bccm_player/src/utils/svg_icons.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_svg/svg.dart'; 5 | 6 | class DefaultCastPlayer extends StatelessWidget { 7 | const DefaultCastPlayer({ 8 | super.key, 9 | required this.aspectRatio, 10 | this.castButton, 11 | }); 12 | 13 | final double aspectRatio; 14 | final Widget? castButton; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return GestureDetector( 19 | behavior: HitTestBehavior.opaque, 20 | onTap: () { 21 | BccmPlayerInterface.instance.openExpandedCastController(); 22 | }, 23 | child: ClipRect( 24 | child: AspectRatio( 25 | aspectRatio: aspectRatio, 26 | child: Container( 27 | decoration: BoxDecoration(color: BccmPlayerTheme.safeOf(context).controls?.settingsListBackgroundColor), 28 | child: Center( 29 | child: castButton ?? 30 | SvgPicture.string( 31 | SvgIcons.castButton, 32 | height: 100, 33 | colorFilter: ColorFilter.mode(BccmPlayerTheme.safeOf(context).controls?.primaryColor ?? Colors.white, BlendMode.srcIn), 34 | ), 35 | ), 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/widgets/controls/control_fade_out.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'controls_wrapper.dart'; 4 | 5 | class ControlFadeOut extends StatelessWidget { 6 | const ControlFadeOut({super.key, required this.child, this.blockBackgroundClicks}); 7 | 8 | final Widget child; 9 | final bool? blockBackgroundClicks; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final animation = ControlsState.of(context).visibilityAnimation; 14 | 15 | return AnimatedBuilder( 16 | animation: animation, 17 | child: child, 18 | builder: (context, child) => IgnorePointer( 19 | ignoring: animation.value < 0.1, 20 | child: Opacity( 21 | opacity: animation.value, 22 | child: _maybeBlock(child!), 23 | ), 24 | ), 25 | ); 26 | } 27 | 28 | Widget _maybeBlock(Widget child) { 29 | if (blockBackgroundClicks == true) { 30 | return Stack( 31 | children: [ 32 | Positioned.fill( 33 | child: GestureDetector( 34 | behavior: HitTestBehavior.opaque, 35 | onTap: () {}, 36 | ), 37 | ), 38 | child, 39 | ], 40 | ); 41 | } 42 | return child; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/widgets/controls/default/fullscreen_button.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: invalid_use_of_protected_member 2 | 3 | import 'package:bccm_player/bccm_player.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class FullscreenButton extends StatelessWidget { 7 | const FullscreenButton({ 8 | super.key, 9 | required this.viewController, 10 | this.padding, 11 | }); 12 | 13 | final BccmPlayerViewController viewController; 14 | final EdgeInsets? padding; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final controlsTheme = BccmPlayerTheme.safeOf(context).controls!; 19 | void onTap() { 20 | if (!viewController.isFullscreen) { 21 | viewController.enterFullscreen(); 22 | } else { 23 | viewController.exitFullscreen(); 24 | } 25 | } 26 | 27 | return GestureDetector( 28 | behavior: HitTestBehavior.opaque, 29 | onTap: onTap, 30 | child: Container( 31 | height: double.infinity, 32 | padding: padding, 33 | child: FocusableActionDetector( 34 | actions: { 35 | ActivateIntent: CallbackAction( 36 | onInvoke: (Intent intent) => onTap(), 37 | ), 38 | }, 39 | child: Icon( 40 | viewController.isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen, 41 | color: controlsTheme.iconColor, 42 | ), 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/widgets/controls/default/play_pause_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | import 'package:bccm_player/src/utils/svg_icons.dart'; 3 | import 'package:bccm_player/src/widgets/mini_player/loading_indicator.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_hooks/flutter_hooks.dart'; 6 | import 'package:flutter_svg/svg.dart'; 7 | 8 | class PlayPauseButton extends HookWidget { 9 | const PlayPauseButton({super.key, required this.player, this.onPressed, this.iconSize = 68}); 10 | 11 | final BccmPlayerController player; 12 | final void Function(bool newState)? onPressed; 13 | final double? iconSize; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final controlsTheme = BccmPlayerTheme.safeOf(context).controls!; 18 | final state = useListenableSelector(player, () => player.value.playbackState); 19 | if (state != PlaybackState.playing) { 20 | return IconButton( 21 | autofocus: true, 22 | constraints: BoxConstraints.tightFor(width: iconSize, height: iconSize), 23 | icon: Padding( 24 | padding: const EdgeInsets.only(left: 4), 25 | child: SvgPicture.string( 26 | SvgIcons.play, 27 | width: double.infinity, 28 | height: double.infinity, 29 | colorFilter: ColorFilter.mode(controlsTheme.iconColor ?? Colors.white, BlendMode.srcIn), 30 | ), 31 | ), 32 | color: controlsTheme.iconColor, 33 | onPressed: () { 34 | player.play(); 35 | onPressed?.call(true); 36 | }, 37 | ); 38 | } else { 39 | return IconButton( 40 | constraints: BoxConstraints.tightFor(width: iconSize, height: iconSize), 41 | icon: player.value.isBuffering == true 42 | ? LoadingIndicator( 43 | width: 42, 44 | height: 42, 45 | color: controlsTheme.iconColor, 46 | ) 47 | : Padding( 48 | padding: const EdgeInsets.all(2), 49 | child: SvgPicture.string( 50 | SvgIcons.pause, 51 | width: double.infinity, 52 | height: double.infinity, 53 | ), 54 | ), 55 | iconSize: 42, 56 | color: controlsTheme.iconColor, 57 | onPressed: () { 58 | player.pause(); 59 | onPressed?.call(false); 60 | }, 61 | ); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/widgets/controls/default/settings_option_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../theme/player_theme.dart'; 4 | 5 | class SettingsOption { 6 | final T value; 7 | final String label; 8 | final bool isSelected; 9 | 10 | const SettingsOption({ 11 | required this.value, 12 | required this.label, 13 | this.isSelected = false, 14 | }); 15 | } 16 | 17 | Future?> showModalOptionList({required BuildContext context, required List> options}) async { 18 | return await showModalBottomSheet>( 19 | context: context, 20 | isDismissible: true, 21 | builder: (context) => SettingsOptionList( 22 | onSelect: (option) { 23 | // select this track 24 | Navigator.pop(context, option); 25 | }, 26 | options: options, 27 | ), 28 | ); 29 | } 30 | 31 | class SettingsOptionList extends StatelessWidget { 32 | const SettingsOptionList({ 33 | super.key, 34 | required this.options, 35 | required this.onSelect, 36 | }); 37 | 38 | final List> options; 39 | 40 | final void Function(SettingsOption value) onSelect; 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | final controlsTheme = BccmPlayerTheme.safeOf(context).controls; 45 | return Material( 46 | color: controlsTheme?.settingsListBackgroundColor, 47 | child: ListView( 48 | shrinkWrap: true, 49 | cacheExtent: 1000, 50 | children: [ 51 | for (final option in options) 52 | ListTile( 53 | dense: true, 54 | onTap: () { 55 | onSelect(option); 56 | }, 57 | autofocus: option.isSelected, 58 | title: Text(option.label, style: controlsTheme?.settingsListTextStyle), 59 | trailing: option.isSelected ? const Icon(Icons.check) : null, 60 | ), 61 | ], 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/widgets/controls/default/time_skip_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/src/theme/bccm_player_theme.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class TimeSkipButton extends StatelessWidget { 5 | const TimeSkipButton({ 6 | super.key, 7 | required this.forwardRewindDurationSec, 8 | required this.icon, 9 | required this.onPressed, 10 | this.iconSize = 52, 11 | }); 12 | 13 | final int forwardRewindDurationSec; 14 | final Widget icon; 15 | final VoidCallback onPressed; 16 | final double iconSize; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | final controlsTheme = BccmPlayerTheme.safeOf(context).controls; 21 | return Stack( 22 | alignment: Alignment.center, 23 | children: [ 24 | Padding( 25 | padding: const EdgeInsets.only(top: 7), 26 | child: Text( 27 | "$forwardRewindDurationSec", 28 | textAlign: TextAlign.center, 29 | style: TextStyle( 30 | fontSize: 12, 31 | color: controlsTheme?.iconColor, 32 | ), 33 | ), 34 | ), 35 | IconButton( 36 | icon: icon, 37 | padding: const EdgeInsets.all(0), 38 | iconSize: iconSize, 39 | color: controlsTheme?.iconColor, 40 | onPressed: onPressed, 41 | ), 42 | ], 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/widgets/mini_player/loading_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingIndicator extends StatelessWidget { 4 | const LoadingIndicator({ 5 | super.key, 6 | this.width, 7 | this.height, 8 | this.color, 9 | }); 10 | 11 | final double? width; 12 | final double? height; 13 | final Color? color; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return SizedBox(width: width ?? 32, height: height ?? 32, child: CircularProgressIndicator(strokeWidth: 2, color: color)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/widgets/utils/bccm_player_plugin_state_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BccmPlayerPluginStateBuilder extends StatelessWidget { 4 | const BccmPlayerPluginStateBuilder({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return const Placeholder(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/widgets/video/native_player_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class NativeBccmPlayerView extends StatelessWidget implements BccmPlayerView { 5 | const NativeBccmPlayerView( 6 | this.playerController, { 7 | super.key, 8 | }); 9 | 10 | final BccmPlayerController playerController; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return VideoPlatformView( 15 | playerController: playerController, 16 | showControls: true, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Setup 2 | # pip install mkdocs 3 | # pip install mkdocs-exclude 4 | # pip install pymdown-extensions 5 | # 6 | # Usage 7 | # Serve: mkdocs serve 8 | # Deploy: mkdocs gh-deploy 9 | 10 | site_name: BccmPlayer docs 11 | docs_dir: doc/ 12 | theme: 13 | name: readthedocs 14 | plugins: 15 | - exclude: 16 | glob: 17 | - api/* 18 | markdown_extensions: 19 | - pymdownx.highlight: 20 | anchor_linenums: true 21 | - pymdownx.superfences 22 | -------------------------------------------------------------------------------- /pigeons/README.md: -------------------------------------------------------------------------------- 1 | # Pigeons 2 | 3 | Pigeon is used to generate type-safe code for communicating between flutter and iOS/Android. 4 | Pigeon doesn't use build_runner, so the commands below need to be re-run whenever you change the dart pigeon files. 5 | 6 | ```sh 7 | 8 | # When you change playback_platform_pigeon.dart, run: 9 | dart run pigeon --input pigeons/playback_platform_pigeon.dart 10 | 11 | # When you change chromecast_pigeon.dart, run: 12 | dart run pigeon --input pigeons/chromecast_pigeon.dart 13 | 14 | ``` 15 | -------------------------------------------------------------------------------- /pigeons/chromecast_pigeon.dart: -------------------------------------------------------------------------------- 1 | import 'package:pigeon/pigeon.dart'; 2 | 3 | // IMPORTANT INFORMATION 4 | // This is a template pigeon file, 5 | // After doing edits to this file you have to run pigeon to generate playback_platform_pigeon.g.dart: 6 | // 7 | // ```sh 8 | // dart run pigeon --input pigeons/chromecast_pigeon.dart 9 | // ``` 10 | // 11 | // See the "Contributing" docs for bccm_player for more info. 12 | 13 | @ConfigurePigeon(PigeonOptions( 14 | dartOut: 'lib/src/pigeon/chromecast_pigeon.g.dart', 15 | dartOptions: DartOptions(), 16 | javaOut: 'android/src/main/java/media/bcc/bccm_player/pigeon/ChromecastControllerPigeon.java', 17 | javaOptions: JavaOptions(package: 'media.bcc.bccm_player.pigeon'), 18 | objcHeaderOut: 'ios/Classes/Pigeon/ChromecastPigeon.h', 19 | objcSourceOut: 'ios/Classes/Pigeon/ChromecastPigeon.m', 20 | objcOptions: ObjcOptions(), 21 | )) 22 | 23 | /// An API called by the native side to notify about chromecast changes 24 | @FlutterApi() 25 | abstract class ChromecastPigeon { 26 | @ObjCSelector("onSessionEnded") 27 | void onSessionEnded(); 28 | 29 | @ObjCSelector("onSessionEnding") 30 | void onSessionEnding(); 31 | 32 | @ObjCSelector("onSessionResumeFailed") 33 | void onSessionResumeFailed(); 34 | 35 | @ObjCSelector("onSessionResumed") 36 | void onSessionResumed(); 37 | 38 | @ObjCSelector("onSessionResuming") 39 | void onSessionResuming(); 40 | 41 | @ObjCSelector("onSessionStartFailed") 42 | void onSessionStartFailed(); 43 | 44 | @ObjCSelector("onSessionStarted") 45 | void onSessionStarted(); 46 | 47 | @ObjCSelector("onSessionStarting") 48 | void onSessionStarting(); 49 | 50 | @ObjCSelector("onSessionSuspended") 51 | void onSessionSuspended(); 52 | 53 | @ObjCSelector("onCastSessionAvailable") 54 | void onCastSessionAvailable(); 55 | 56 | @ObjCSelector("onCastSessionUnavailable:") 57 | void onCastSessionUnavailable(CastSessionUnavailableEvent event); 58 | } 59 | 60 | class CastSessionUnavailableEvent { 61 | int? playbackPositionMs; 62 | } 63 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bccm_player 2 | description: ExoPlayer/AVPlayer via platform views, with cast, PiP, background audio, audio selection, etc. 3 | version: 1.1.3 4 | documentation: https://bcc-code.github.io/bccm-player/ 5 | repository: https://github.com/bcc-code/bccm-player 6 | 7 | environment: 8 | sdk: ">=3.0.0 <4.0.0" 9 | flutter: ">=2.5.0" 10 | 11 | dependencies: 12 | collection: ^1.17.1 13 | flutter: 14 | sdk: flutter 15 | flutter_hooks: ^0.20.5 16 | flutter_plugin_android_lifecycle: ^2.0.9 17 | flutter_state_notifier: ^1.0.0 18 | flutter_svg: ^2.0.2 19 | flutter_web_plugins: 20 | sdk: flutter 21 | freezed: ^2.3.2 22 | freezed_annotation: ^2.2.0 23 | js: ^0.7.1 24 | meta: ^1.9.1 25 | plugin_platform_interface: ^2.0.2 26 | riverpod: ^2.2.0 27 | state_notifier: ^1.0.0 28 | universal_io: ^2.0.4 29 | uuid: ^4.5.0 30 | wakelock_plus: ^1.1.1 31 | 32 | dev_dependencies: 33 | flutter_test: 34 | sdk: flutter 35 | flutter_lints: ^4.0.0 36 | pigeon: ^22.3.0 37 | build_runner: ^2.3.3 38 | mockito: ^5.4.2 39 | 40 | # The following section is specific to Flutter packages. 41 | flutter: 42 | # This section identifies this Flutter project as a plugin project. 43 | # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) 44 | # which should be registered in the plugin registry. This is required for 45 | # using method channels. 46 | # The Android 'package' specifies package in which the registered class is. 47 | # This is required for using method channels on Android. 48 | # The 'ffiPlugin' specifies that native code should be built and bundled. 49 | # This is required for using `dart:ffi`. 50 | # All these are used by the tooling to maintain consistency when 51 | # adding or updating assets for this project. 52 | plugin: 53 | platforms: 54 | android: 55 | package: media.bcc.bccm_player 56 | pluginClass: BccmPlayerPlugin 57 | ios: 58 | pluginClass: BccmPlayerPlugin 59 | -------------------------------------------------------------------------------- /test/controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | 5 | import 'utils/mocks.mocks.dart'; 6 | 7 | void main() { 8 | late MockBccmPlayerInterface mockPlayerInterface; 9 | 10 | setUp(() { 11 | mockPlayerInterface = MockBccmPlayerInterface(); 12 | BccmPlayerInterface.instance = mockPlayerInterface; 13 | }); 14 | 15 | test('intialize', () async { 16 | // Arrange 17 | const fakePlayerId = '12345678-1234-1234-1234-123456789012'; 18 | const fakeUrl = 'url.mp4'; 19 | 20 | final stateNotifier = MockPlayerPluginStateNotifier(); 21 | final playerStateNotifier = PlayerStateNotifier(keepAlive: false, player: const PlayerState(playerId: fakePlayerId)); 22 | 23 | when(mockPlayerInterface.stateNotifier).thenAnswer((_) => stateNotifier); 24 | when(stateNotifier.getOrAddPlayerNotifier(any)).thenReturn(playerStateNotifier); 25 | when(mockPlayerInterface.newPlayer()).thenAnswer((_) async => fakePlayerId); 26 | 27 | // Act 28 | final BccmPlayerController controller = BccmPlayerController.networkUrl(Uri.parse(fakeUrl)); 29 | await controller.initialize(); 30 | 31 | // Assert 32 | verify(mockPlayerInterface.newPlayer()).called(1); 33 | final replaceCurrentMediaItemCall = verify(mockPlayerInterface.replaceCurrentMediaItem(fakePlayerId, captureAny)); 34 | expect((replaceCurrentMediaItemCall.captured[0] as MediaItem).url, fakeUrl); 35 | 36 | playerStateNotifier.dispose(); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /test/utils/mocks.dart: -------------------------------------------------------------------------------- 1 | import 'package:bccm_player/bccm_player.dart'; 2 | import 'package:mockito/annotations.dart'; 3 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 4 | 5 | @GenerateNiceMocks([ 6 | // ignore: deprecated_member_use 7 | MockSpec(mixingIn: [MockPlatformInterfaceMixin]), 8 | MockSpec(), 9 | MockSpec(), 10 | ]) 11 | export 'mocks.mocks.dart'; 12 | --------------------------------------------------------------------------------