├── .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 |
--------------------------------------------------------------------------------
/android/src/main/res/menu/cast_expanded_controller.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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