diff --git a/packages/video_player/specs/future/IOS_HLS_CACHE_PLAN.md b/packages/video_player/specs/future/IOS_HLS_CACHE_PLAN.md new file mode 100644 index 000000000000..11677775111f --- /dev/null +++ b/packages/video_player/specs/future/IOS_HLS_CACHE_PLAN.md @@ -0,0 +1,383 @@ +# Phase 2: HLS Segment Caching — Updated Plan +## Inspired by Media3 SimpleCache Architecture + +--- + +## What Changed From Previous Plan + +The original iOS plan used a **sophisticated proxy** as the primary unit of work. +This update flips that: the **cache storage layer is the primary unit**, and the proxy +becomes thin HTTP plumbing on top of it. + +This mirrors exactly how Media3 works on Android: +- Android: `CacheDataSource` (smart) → `SimpleCache` (storage) — no proxy needed +- iOS: `NWListener proxy` (thin) → `HLSSimpleCache` (smart, Media3-inspired) — same storage model + +--- + +## Android — No Changes + +Android's plan is unchanged. Media3's `SimpleCache` + `CacheDataSource.Factory` +is already the right implementation. It requires ~15 lines of code in +`HttpVideoAsset.java`. See original plan. + +**Key reminder:** Use `NoOpCacheEvictor` for downloaded content (never +auto-evict) and `LeastRecentlyUsedCacheEvictor` for streaming cache only. + +--- + +## iOS — Revised Architecture + +### The Core Insight From Media3 + +Media3 never uses a proxy. Instead it inserts a `CacheDataSource` into +ExoPlayer's data pipeline — a layer that intercepts byte requests, checks cache, +and falls through to network on miss. iOS cannot do this because AVPlayer is a +black box with no data source interface. + +But the **storage model** is fully portable. The proxy on iOS becomes thin +and dumb. All the intelligence lives in the cache layer. + +``` +ANDROID (no proxy needed): +ExoPlayer → CacheDataSource → [cache hit] FileDataSource → disk + → [cache miss] TeeDataSource → HttpDataSource + SimpleCache + +iOS (proxy required, but thin): +AVPlayer → NWListener proxy (50 lines) → HLSSimpleCache (200 lines, Media3-inspired) + → [cache hit] read file from disk + → [cache miss] fetch R2 + write to disk simultaneously +``` + +--- + +## Media3 Concepts to Port to Swift + +### 1. CacheSpan → `HLSCacheSpan` + +In Media3, a CacheSpan is a byte range within a resource that may or may not be +cached. For HLS, this maps perfectly to segments — each `.m4s` is a natural +cache span. One file = one segment = one span. No byte-range arithmetic. + +```swift +struct HLSCacheSpan { + let cacheKey: String // stable key (URL without query params) + let fileName: String // actual file on disk + let length: Int // bytes + let lastAccessTime: Date // for LRU eviction + let isFullyCached: Bool // always true for completed segments +} +``` + +### 2. CacheKeyFactory → `HLSCacheKeyFactory` + +Media3 separates cache key from URL. Critical for R2 signed URLs where the same +segment gets a different `?X-Amz-Signature=...` on every request. + +```swift +struct HLSCacheKeyFactory { + /// Strips query params to produce a stable cache key. + /// "https://r2.example.com/videos/abc/480p/seg001.m4s?X-Amz-Signature=xyz" + /// → "https://r2.example.com/videos/abc/480p/seg001.m4s" + static func cacheKey(for url: URL) -> String { + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + components?.queryItems = nil + return components?.url?.absoluteString ?? url.absoluteString + } +} +``` + +### 3. FLAG_BLOCK_ON_CACHE → Per-key mutex (`HLSCacheLock`) + +Media3's `FLAG_BLOCK_ON_CACHE` prevents two simultaneous reads of the same +segment from both triggering network requests. One waits; the other serves from +the cache written by the first. + +```swift +actor HLSCacheLock { + private var lockedKeys: Set = [] + private var waiters: [String: [CheckedContinuation]] = [:] + + func lock(_ key: String) async { + if lockedKeys.contains(key) { + await withCheckedContinuation { continuation in + waiters[key, default: []].append(continuation) + } + } else { + lockedKeys.insert(key) + } + } + + func unlock(_ key: String) { + lockedKeys.remove(key) + waiters.removeValue(forKey: key)?.forEach { $0.resume() } + } +} +``` + +### 4. TeeDataSource → Simultaneous write-while-serve + +Media3's `TeeDataSource` writes bytes to cache while simultaneously serving them +to the player. On iOS the proxy does this: it fetches from R2, writes each chunk +to disk, and forwards to AVPlayer in the same stream. No "download then serve" +delay. + +```swift +// In HLSSimpleCache.fetchAndCache(): +func fetchAndCache(url: URL, respondTo connection: NWConnection) async { + let key = HLSCacheKeyFactory.cacheKey(for: url) + await cacheLock.lock(key) + defer { Task { await cacheLock.unlock(key) } } + + var accumulated = Data() + // Stream from R2, accumulate bytes, forward to AVPlayer simultaneously + for try await chunk in URLSession.shared.bytes(from: url).0 { + accumulated.append(chunk) + // Forward chunk to AVPlayer connection as it arrives (streaming response) + } + // Write complete segment to disk once fully received + diskCache.write(accumulated, forKey: key) +} +``` + +### 5. LeastRecentlyUsedCacheEvictor → `HLSCacheEvictor` + +Identical logic to Media3 — evict least recently accessed spans when over +size limit. Runs after every write. + +```swift +struct HLSCacheEvictor { + let maxBytes: Int // default 500MB for streaming cache + + func evictIfNeeded(from index: inout [String: HLSCacheSpan], cacheDir: URL) { + let total = index.values.reduce(0) { $0 + $1.length } + guard total > maxBytes else { return } + + let sorted = index.values.sorted { $0.lastAccessTime < $1.lastAccessTime } + var freed = 0 + let target = total - maxBytes + + for span in sorted { + try? FileManager.default.removeItem( + at: cacheDir.appendingPathComponent(span.fileName)) + index.removeValue(forKey: span.cacheKey) + freed += span.length + if freed >= target { break } + } + } +} +``` + +### 6. StandaloneDatabaseProvider → `HLSCacheIndex` + +Media3 persists the cache index to SQLite so it survives process death and app +restarts. On iOS, persist to a JSON file in the same cache directory. +Rebuilt from disk on launch if the index file is missing (same fallback as Media3). + +```swift +actor HLSCacheIndex { + private var spans: [String: HLSCacheSpan] = [:] // cacheKey → span + private let indexURL: URL + + // Persist index to disk (called after every write/eviction) + func save() throws { ... } + + // Load index from disk on launch + func load() throws { ... } + + // Fallback: rebuild from disk if index file missing/corrupt + func rebuildFromDisk(cacheDir: URL) { ... } +} +``` + +--- + +## Revised iOS File Structure + +### New files (all Swift, zero external dependencies) + +``` +Sources/video_player_avfoundation/cache/ +├── HLSCacheKeyFactory.swift ~30 lines URL → stable cache key +├── HLSCacheSpan.swift ~20 lines Cache entry model +├── HLSCacheLock.swift ~40 lines Per-key mutex (FLAG_BLOCK_ON_CACHE) +├── HLSCacheIndex.swift ~80 lines Persistent index (StandaloneDatabaseProvider equivalent) +├── HLSCacheEvictor.swift ~50 lines LRU eviction +├── HLSSimpleCache.swift ~120 lines Facade (SimpleCache equivalent) — read/write/evict +├── HLSProxyServer.swift ~80 lines NWListener HTTP server (thin — just plumbing) +└── HLSProxyServerBridge.swift ~15 lines @objc bridge for FVPVideoPlayerPlugin.m +``` + +**~435 lines total across 8 files.** + +Compare to original plan: ~315 lines across 4 files but with a monolithic proxy +doing too much. This separation mirrors Media3's own package structure and makes +each component independently testable. + +### Modified files + +``` +FVPVideoPlayerPlugin.m +5 lines Start proxy, swap URL for HLS +``` + +--- + +## Detailed Component Responsibilities + +### `HLSSimpleCache` (the central facade) + +Direct equivalent of Media3's `SimpleCache`. All other components are injected. + +```swift +final class HLSSimpleCache { + static let shared = HLSSimpleCache() + + private let cacheDir: URL + private let index: HLSCacheIndex + private let evictor: HLSCacheEvictor + private let lock: HLSCacheLock + private let ioQueue = DispatchQueue(label: "hls.cache.io", + attributes: .concurrent) + + // Read — equivalent to CacheDataSource cache hit path + func data(for url: URL) async -> Data? { + let key = HLSCacheKeyFactory.cacheKey(for: url) + guard let span = await index.span(for: key) else { return nil } + let fileURL = cacheDir.appendingPathComponent(span.fileName) + let data = try? Data(contentsOf: fileURL) + if data != nil { await index.updateAccessTime(for: key) } + return data + } + + // Write — equivalent to TeeDataSource write path + func store(_ data: Data, for url: URL) async { + let key = HLSCacheKeyFactory.cacheKey(for: url) + let fileName = key.sha256() + ".m4s" + let fileURL = cacheDir.appendingPathComponent(fileName) + try? data.write(to: fileURL, options: .atomic) + let span = HLSCacheSpan(cacheKey: key, fileName: fileName, + length: data.count, lastAccessTime: Date(), + isFullyCached: true) + await index.set(span, for: key) + await index.save() + evictor.evictIfNeeded(...) + } + + // Cache management API (exposed via Pigeon) + func clearAll() async { ... } + func currentSizeBytes() async -> Int { ... } + func removeContent(for url: URL) async { ... } +} +``` + +### `HLSProxyServer` (thin HTTP plumbing) + +All cache logic delegated to `HLSSimpleCache`. The proxy does only two things: +parse the request URL and write the HTTP response. + +```swift +final class HLSProxyServer { + private func handleSegmentRequest(_ url: URL, + connection: NWConnection) async { + // 1. Check cache (HLSSimpleCache handles key normalisation + lock) + if let cached = await HLSSimpleCache.shared.data(for: url) { + respond(with: cached, to: connection) + return + } + + // 2. Cache miss — acquire lock, fetch, write, serve + let key = HLSCacheKeyFactory.cacheKey(for: url) + await HLSSimpleCache.shared.lock.lock(key) + defer { Task { await HLSSimpleCache.shared.lock.unlock(key) } } + + // Re-check after acquiring lock (another request may have cached it) + if let cached = await HLSSimpleCache.shared.data(for: url) { + respond(with: cached, to: connection) + return + } + + // Fetch and simultaneously serve + write (TeeDataSource pattern) + guard let data = try? await URLSession.shared.data(from: url).0 else { + respond(statusCode: 502, to: connection); return + } + await HLSSimpleCache.shared.store(data, for: url) + respond(with: data, to: connection) + } +} +``` + +--- + +## Graceful Degradation (FLAG_IGNORE_CACHE_ON_ERROR equivalent) + +If the proxy is killed by iOS under memory pressure: +- AVPlayer's pending request to `localhost:PORT` gets a connection refused +- AVPlayer retries — this time the proxy has restarted (it restarts on every + `+registerWithRegistrar:` call) +- If it hasn't restarted yet, AVPlayer falls back to fetching from R2 directly + after a timeout + +This is equivalent to Media3's `FLAG_IGNORE_CACHE_ON_ERROR`: cache errors +fall through to upstream. AVPlayer handles this correctly for HLS — a failed +segment request triggers a retry, not a playback failure. + +The cache index on disk is never lost — `HLSCacheIndex` persists after every +write. Even if the proxy is killed mid-write, the index only includes completed +segments (`isFullyCached: true`). Partial writes are discarded on index reload, +same as Media3's `SimpleCache` behaviour. + +--- + +## Pigeon API (unchanged from original plan) + +```dart +// Cache management — exposed to Flutter layer +void setCacheMaxSize(int maxSizeBytes); +void clearCache(); +int getCacheSize(); +bool isCacheEnabled(); +void setCacheEnabled(bool enabled); +``` + +--- + +## Implementation Order + +| Step | Component | Effort | Notes | +|------|-----------|--------|-------| +| 1 | `HLSCacheKeyFactory` | 1 hour | Pure function, trivial, test first | +| 2 | `HLSCacheSpan` + `HLSCacheIndex` | 1 day | Storage model + persistence | +| 3 | `HLSCacheEvictor` | 0.5 day | LRU logic, unit-testable in isolation | +| 4 | `HLSCacheLock` | 0.5 day | Swift actor, straightforward | +| 5 | `HLSSimpleCache` | 1 day | Wires 1-4 together, integration tests | +| 6 | `HLSProxyServer` | 1 day | NWListener + delegates to HLSSimpleCache | +| 7 | `HLSProxyServerBridge` + plugin hook | 0.5 day | ObjC bridge, 1 line in plugin | + +**Total: ~5-6 days** + +Steps 1-5 can be developed and tested entirely without AVPlayer or Flutter. +`HLSSimpleCache` is a standalone Swift module — write unit tests for it before +touching any native plugin code. + +--- + +## Key Differences From Original Plan + +| Aspect | Original Plan | Updated Plan | +|--------|--------------|--------------| +| Primary complexity | Proxy (monolithic) | Cache storage layer (Media3-inspired) | +| Proxy role | Smart — cache logic inside proxy | Thin — just HTTP plumbing | +| URL normalisation | Manual SHA256 of full URL | `HLSCacheKeyFactory` (R2 signed URL safe) | +| Concurrent request handling | Basic mutex | `HLSCacheLock` actor (FLAG_BLOCK_ON_CACHE) | +| Write-while-serve | Not addressed | Explicit TeeDataSource pattern | +| Index persistence | Rebuilt from disk on launch | Persisted JSON index (StandaloneDatabaseProvider equivalent) | +| Graceful degradation on kill | Not addressed | FLAG_IGNORE_CACHE_ON_ERROR equivalent | +| Testability | Hard (proxy entangled with cache) | Each component independently testable | + +--- + +## What This Does NOT Include (Separate Phase) + +- `AVAssetDownloadURLSession` offline downloads — independent feature, separate phase +- ABR quality control API — independent feature, no dependency on cache +- Android `DownloadManager` — independent feature, shares `SimpleCache` instance \ No newline at end of file diff --git a/packages/video_player/video_player/example/android/app/build.gradle b/packages/video_player/video_player/example/android/app/build.gradle index 5a8787bcc93a..36ee081e7793 100644 --- a/packages/video_player/video_player/example/android/app/build.gradle +++ b/packages/video_player/video_player/example/android/app/build.gradle @@ -24,6 +24,7 @@ if (flutterVersionName == null) { android { namespace = "io.flutter.plugins.videoplayerexample" + ndkVersion = "28.2.13676358" compileSdk = flutter.compileSdkVersion compileOptions { diff --git a/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml b/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml index c2843a4f26fa..f61cc5d42e50 100644 --- a/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml +++ b/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml @@ -5,11 +5,12 @@ android:label="video_player_example" android:networkSecurityConfig="@xml/network_security_config"> diff --git a/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist b/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7652ba..391a902b2beb 100644 --- a/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj index 2c7c8b508260..b96320b4a073 100644 --- a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj @@ -239,23 +239,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 40E43985C26639614BC3B419 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -386,6 +369,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 847V53Q7SL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -518,6 +502,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 847V53Q7SL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -540,6 +525,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 847V53Q7SL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/packages/video_player/video_player/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/video_player/video_player/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d22f10b2ab63..8122b0a0c2f2 100644 --- a/packages/video_player/video_player/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/packages/video_player/video_player/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,116 +1,121 @@ { "images" : [ { - "size" : "20x20", - "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" }, { - "size" : "20x20", - "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" }, { - "size" : "29x29", - "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" }, { - "size" : "29x29", - "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" }, { - "size" : "29x29", - "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" }, { - "size" : "40x40", - "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" }, { - "size" : "40x40", - "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" }, { - "size" : "60x60", - "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" }, { - "size" : "60x60", - "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" }, { - "size" : "20x20", - "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" }, { - "size" : "20x20", - "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" }, { - "size" : "29x29", - "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" }, { - "size" : "29x29", - "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" }, { - "size" : "40x40", - "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" }, { - "size" : "40x40", - "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" }, { - "size" : "76x76", - "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" }, { - "size" : "76x76", - "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" }, { - "size" : "83.5x83.5", - "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } } diff --git a/packages/video_player/video_player/example/ios/Runner/Info.plist b/packages/video_player/video_player/example/ios/Runner/Info.plist index c7d0e4eb5eda..55a3d0a30791 100644 --- a/packages/video_player/video_player/example/ios/Runner/Info.plist +++ b/packages/video_player/video_player/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -29,6 +31,15 @@ NSAllowsArbitraryLoads + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + audio + fetch + processing + remote-notification + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -46,9 +57,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/packages/video_player/video_player/example/lib/decoder_demo.dart b/packages/video_player/video_player/example/lib/decoder_demo.dart new file mode 100644 index 000000000000..55689ad88faf --- /dev/null +++ b/packages/video_player/video_player/example/lib/decoder_demo.dart @@ -0,0 +1,336 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +import 'decoder_retry_example.dart'; + +class DecoderDemo extends StatefulWidget { + const DecoderDemo(this.viewType, {super.key}); + + final VideoViewType viewType; + + @override + State createState() => _DecoderDemoState(); +} + +class _DecoderDemoState extends State { + late VideoPlayerController _controller; + List _decoders = []; + String? _currentDecoder; + bool? _isCurrentHw; + String? _selectedDecoderName; // null = auto + final List _log = []; + bool _retrying = false; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.networkUrl( + Uri.parse( + 'https://test-storage.reallifeglobal.com/demo_hls_1080/master.m3u8', + ), + formatHint: VideoFormat.hls, + viewType: widget.viewType, + ); + _controller.addListener(_onControllerUpdate); + _controller.setLooping(true); + _controller.initialize().then((_) { + setState(() {}); + _refreshDecoders(); + }); + } + + @override + void dispose() { + _controller.removeListener(_onControllerUpdate); + _controller.dispose(); + super.dispose(); + } + + void _onControllerUpdate() { + final String? name = _controller.value.decoderName; + final bool? isHw = _controller.value.isDecoderHardwareAccelerated; + if (name != null && name != _currentDecoder) { + _addLog( + 'Decoder changed -> $name ' + '(${(isHw ?? false) ? 'HW' : 'SW'})', + ); + _currentDecoder = name; + _isCurrentHw = isHw; + // Refresh decoder list to update isSelected + _refreshDecoders(); + } + setState(() {}); + } + + Future _refreshDecoders() async { + final List decoders = + await _controller.getAvailableDecoders(); + final String? current = await _controller.getCurrentDecoderName(); + setState(() { + _decoders = decoders; + _currentDecoder = current; + }); + } + + Future _selectDecoder(String? decoderName) async { + _selectedDecoderName = decoderName; + final String label = decoderName ?? 'Auto'; + _addLog('Switching to decoder: $label'); + try { + await _controller.setVideoDecoder(decoderName); + _addLog('Decoder switch complete'); + await _refreshDecoders(); + } catch (e) { + _addLog('Decoder switch failed: $e'); + } + } + + Future _runRetryDemo() async { + if (_retrying) { + return; + } + setState(() { + _retrying = true; + }); + _addLog('--- Retry demo started ---'); + + final retrier = DecoderRetrier( + _controller, + settleDelay: const Duration(seconds: 1), + onAttempt: (VideoDecoderInfo decoder, int attempt) { + _addLog( + 'Attempt $attempt: trying ${decoder.name} ' + '(${decoder.isHardwareAccelerated ? 'HW' : 'SW'})', + ); + }, + onSuccess: (VideoDecoderInfo decoder) { + _addLog('Success: ${decoder.name} works!'); + }, + onExhausted: () { + _addLog('All decoders exhausted — none worked'); + }, + ); + + final String? result = await retrier.retryWithFallback(); + _addLog( + result != null + ? '--- Retry demo done: using $result ---' + : '--- Retry demo done: no working decoder ---', + ); + await _refreshDecoders(); + setState(() => _retrying = false); + } + + void _addLog(String message) { + setState(() { + final String ts = TimeOfDay.now().format(context); + _log.insert(0, '[$ts] $message'); + if (_log.length > 50) { + _log.removeLast(); + } + }); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Video player + if (_controller.value.isInitialized) + AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ) + else + const SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator()), + ), + + const SizedBox(height: 8), + + // Playback controls + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: () => _controller.value.isPlaying + ? _controller.pause() + : _controller.play(), + icon: Icon( + _controller.value.isPlaying + ? Icons.pause + : Icons.play_arrow, + ), + ), + ], + ), + + const Divider(), + + // Current decoder info + Text( + 'Current Decoder', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + if (_currentDecoder != null) + Row( + children: [ + Expanded( + child: Text( + _currentDecoder!, + style: const TextStyle(fontFamily: 'monospace'), + ), + ), + Chip( + label: Text((_isCurrentHw ?? false) ? 'HW' : 'SW'), + backgroundColor: + (_isCurrentHw ?? false) ? Colors.green[100] : Colors.orange[100], + ), + ], + ) + else + const Text('Not initialized yet'), + + const Divider(), + + // Available decoders + Text( + 'Available Decoders', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + + // Auto option + RadioListTile( + title: const Text('Auto (system default)'), + value: null, + groupValue: _selectedDecoderName, + dense: true, + onChanged: (String? value) => _selectDecoder(null), + ), + + // Decoder list + if (_decoders.isEmpty) + const Padding( + padding: EdgeInsets.all(8.0), + child: Text('No decoders available (play a video first)'), + ) + else + ..._decoders.map( + (VideoDecoderInfo d) => RadioListTile( + title: Text( + d.name, + style: const TextStyle(fontFamily: 'monospace', fontSize: 13), + ), + subtitle: Row( + children: [ + Text(d.mimeType, style: const TextStyle(fontSize: 11)), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 1, + ), + decoration: BoxDecoration( + color: d.isHardwareAccelerated + ? Colors.green[100] + : Colors.orange[100], + borderRadius: BorderRadius.circular(4), + ), + child: Text( + d.isHardwareAccelerated ? 'HW' : 'SW', + style: const TextStyle(fontSize: 11), + ), + ), + ], + ), + value: d.name, + groupValue: _selectedDecoderName, + dense: true, + onChanged: (String? value) => _selectDecoder(value), + ), + ), + + const Divider(), + + // Retry demo + Text( + 'Decoder Retry Demo', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + const Text( + 'Iterates through all available decoders (HW first, then SW) ' + 'until one works. Demonstrates best practices for handling ' + 'decoder failures on problematic devices.', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + const SizedBox(height: 8), + ElevatedButton.icon( + onPressed: _retrying ? null : _runRetryDemo, + icon: _retrying + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + label: Text(_retrying ? 'Retrying...' : 'Run Decoder Retry'), + ), + + const Divider(), + + // Event log + Text( + 'Event Log', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + Container( + height: 200, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + ), + child: _log.isEmpty + ? const Center( + child: Text( + 'Events will appear here', + style: TextStyle(color: Colors.grey), + ), + ) + : ListView.builder( + itemCount: _log.length, + padding: const EdgeInsets.all(8), + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text( + _log[index], + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/video_player/video_player/example/lib/decoder_retry_example.dart b/packages/video_player/video_player/example/lib/decoder_retry_example.dart new file mode 100644 index 000000000000..4cdcd811cc93 --- /dev/null +++ b/packages/video_player/video_player/example/lib/decoder_retry_example.dart @@ -0,0 +1,135 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +/// Best-practice example for iterating through video decoders on failure. +/// +/// This is NOT part of the plugin — it lives in the example app to show how +/// apps should handle decoder fallback on problematic devices. +/// +/// Strategy: +/// 1. Query available decoders for the current video's MIME type. +/// 2. Order: HW decoders first, then SW decoders. +/// 3. Try each decoder, actively monitoring for errors (including runtime +/// MediaCodec crashes that happen during frame decoding, not just init). +/// 4. Return the working decoder name (or null if all failed). +/// +/// The app can persist the working decoder name (e.g. SharedPreferences) +/// and pass it to `controller.setVideoDecoder(savedName)` on next launch. +library; + +import 'dart:async'; + +import 'package:video_player/video_player.dart'; + +class DecoderRetrier { + DecoderRetrier( + this.controller, { + this.onAttempt, + this.onSuccess, + this.onExhausted, + this.settleDelay = const Duration(seconds: 3), + }); + + final VideoPlayerController controller; + + /// Called before each decoder attempt. + final void Function(VideoDecoderInfo decoder, int attempt)? onAttempt; + + /// Called when a working decoder is found. + final void Function(VideoDecoderInfo decoder)? onSuccess; + + /// Called when all decoders have been exhausted without success. + final void Function()? onExhausted; + + /// How long to wait after switching to confirm no runtime decoder crash. + /// + /// Runtime MediaCodec errors (e.g. Huawei HiSilicon OMX.hisi crashes) + /// can occur several hundred milliseconds after init succeeds, so this + /// should be long enough to catch those. 3 seconds is a good default. + final Duration settleDelay; + + /// Tries each available decoder in priority order (HW first, then SW). + /// + /// Returns the name of the working decoder, or null if all failed. + Future retryWithFallback() async { + final List decoders = + await controller.getAvailableDecoders(); + if (decoders.isEmpty) { + onExhausted?.call(); + return null; + } + + // Priority: HW decoders first, then SW decoders. + final ordered = [ + ...decoders.where((VideoDecoderInfo d) => d.isHardwareAccelerated), + ...decoders.where((VideoDecoderInfo d) => d.isSoftwareOnly), + ]; + + for (var i = 0; i < ordered.length; i++) { + final VideoDecoderInfo decoder = ordered[i]; + onAttempt?.call(decoder, i + 1); + + try { + await controller.setVideoDecoder(decoder.name); + + // Actively listen for errors during the settle period. + // Runtime MediaCodec crashes (the Huawei/HiSilicon bug) happen + // after init succeeds, during actual frame decoding — so a simple + // hasError check after a delay is not enough. We need to watch + // for errors that arrive asynchronously. + final bool stable = await _waitForStability(); + if (!stable) { + continue; + } + + onSuccess?.call(decoder); + return decoder.name; + } catch (_) { + // setVideoDecoder itself threw — try next decoder. + continue; + } + } + + onExhausted?.call(); + return null; + } + + /// Waits [settleDelay] while monitoring for errors. + /// + /// Returns true if the player remained error-free, false if an error + /// was detected (including runtime MediaCodec errors). + Future _waitForStability() async { + // If already in error state (e.g. init-time failure), fail immediately. + if (controller.value.hasError) { + return false; + } + + final completer = Completer(); + + // Listen for value changes that indicate an error. + void listener() { + if (controller.value.hasError && !completer.isCompleted) { + completer.complete(false); + } + } + + controller.addListener(listener); + + // If no error arrives within settleDelay, consider it stable. + final timeout = Timer(settleDelay, () { + if (!completer.isCompleted) { + completer.complete(!controller.value.hasError); + } + }); + + try { + return await completer.future; + } finally { + controller.removeListener(listener); + timeout.cancel(); + } + } +} diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index 4aa855e715e3..88de46cb5c69 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import 'audio_tracks_demo.dart'; +import 'decoder_demo.dart'; void main() { runApp(MaterialApp(home: _App())); @@ -21,7 +22,7 @@ class _App extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultTabController( - length: 3, + length: 5, child: Scaffold( key: const ValueKey('home_page'), appBar: AppBar( @@ -52,6 +53,20 @@ class _App extends StatelessWidget { ); }, ), + IconButton( + key: const ValueKey('pip_bg_demo'), + icon: const Icon(Icons.picture_in_picture), + tooltip: 'PiP & Background Demo', + onPressed: () { + Navigator.push<_PipBackgroundDemo>( + context, + MaterialPageRoute<_PipBackgroundDemo>( + builder: (BuildContext context) => + const _PipBackgroundDemo(), + ), + ); + }, + ), ], bottom: const TabBar( isScrollable: true, @@ -59,6 +74,8 @@ class _App extends StatelessWidget { Tab(icon: Icon(Icons.cloud), text: 'Remote'), Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'), Tab(icon: Icon(Icons.list), text: 'List example'), + Tab(icon: Icon(Icons.hd), text: 'HLS / ABR'), + Tab(icon: Icon(Icons.memory), text: 'Decoders'), ], ), ), @@ -76,6 +93,14 @@ class _App extends StatelessWidget { builder: (VideoViewType viewType) => _ButterFlyAssetVideoInList(viewType), ), + _ViewTypeTabBar( + builder: (VideoViewType viewType) => + _HlsAbrDemo(viewType), + ), + _ViewTypeTabBar( + builder: (VideoViewType viewType) => + DecoderDemo(viewType), + ), ], ), ), @@ -474,6 +499,664 @@ class _ControlsOverlay extends StatelessWidget { } } +class _PipBackgroundDemo extends StatefulWidget { + const _PipBackgroundDemo(); + + @override + State<_PipBackgroundDemo> createState() => _PipBackgroundDemoState(); +} + +class _PipBackgroundDemoState extends State<_PipBackgroundDemo> { + late VideoPlayerController _controller; + bool _pipSupported = false; + bool _autoEnterPip = false; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.networkUrl( + Uri.parse( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + ), + ); + _controller.addListener(() => setState(() {})); + _controller.setLooping(true); + _controller.initialize().then((_) { + setState(() {}); + _checkPipSupport(); + }); + } + + Future _checkPipSupport() async { + final supported = await _controller.isPipSupported; + setState(() { + _pipSupported = supported; + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final bool isPipActive = _controller.value.isPipActive; + final Size? pipSize = _controller.value.pipSize; + + // TODO(you): pipSize workaround for Flutter viewport bug in PiP. + // Remove this block and uncomment the MediaQuery block below once + // https://github.com/flutter/flutter/pull/182326 lands in stable. + if (isPipActive && _controller.value.isInitialized && pipSize != null) { + return Scaffold( + backgroundColor: Colors.black, + body: SizedBox( + width: pipSize.width, + height: pipSize.height, + child: FittedBox( + child: SizedBox( + width: _controller.value.size.width, + height: _controller.value.size.height, + child: VideoPlayer(_controller), + ), + ), + ), + ); + } + // // https://github.com/flutter/flutter/pull/182326 lands in stable. + // // MediaQuery-based PiP layout — requires Flutter with #182326 fix. + // final Size windowSize = MediaQuery.sizeOf(context); + // final bool isPipLayout = isPipActive || windowSize.shortestSide < 250; + // + // if (isPipLayout && _controller.value.isInitialized) { + // return Scaffold( + // backgroundColor: Colors.black, + // body: AspectRatio( + // aspectRatio: _controller.value.aspectRatio, + // child: VideoPlayer(_controller), + // ), + // ); + // } + + return Scaffold( + appBar: AppBar(title: const Text('PiP & Background Playback')), + body: Column( + children: [ + if (_controller.value.isInitialized) + AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ) + else + const SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator()), + ), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Play/Pause + Row( + children: [ + IconButton( + icon: Icon( + _controller.value.isPlaying + ? Icons.pause + : Icons.play_arrow, + ), + onPressed: () { + _controller.value.isPlaying + ? _controller.pause() + : _controller.play(); + }, + ), + Text( + _controller.value.isPlaying ? 'Playing' : 'Paused', + ), + ], + ), + const Divider(), + + // PiP section + Text( + 'Picture-in-Picture', + style: Theme + .of(context) + .textTheme + .titleMedium, + ), + const SizedBox(height: 8), + Text('Supported: $_pipSupported'), + Text('Active: ${_controller.value.isPipActive}'), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + ElevatedButton.icon( + icon: const Icon(Icons.picture_in_picture), + label: const Text('Enter PiP'), + onPressed: _pipSupported + ? () => _controller.enterPip() + : null, + ), + ElevatedButton.icon( + icon: const Icon(Icons.fullscreen_exit), + label: const Text('Exit PiP'), + onPressed: _controller.value.isPipActive + ? () => _controller.exitPip() + : null, + ), + ElevatedButton.icon( + icon: Icon(_autoEnterPip + ? Icons.auto_awesome + : Icons.auto_awesome_outlined), + label: Text(_autoEnterPip + ? 'Disable Auto-PiP' + : 'Enable Auto-PiP'), + onPressed: _pipSupported + ? () { + final newValue = !_autoEnterPip; + _controller.setAutoEnterPip(newValue); + setState(() => _autoEnterPip = newValue); + } + : null, + ), + ], + ), + const Divider(), + + // Background playback section + Text( + 'Background Playback', + style: Theme + .of(context) + .textTheme + .titleMedium, + ), + const SizedBox(height: 8), + Text( + 'Enabled: ${_controller.value.isPlayingInBackground}', + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + ElevatedButton.icon( + icon: const Icon(Icons.volume_up), + label: const Text('Enable Background'), + onPressed: !_controller.value.isPlayingInBackground + ? () => + _controller.enableBackgroundPlayback( + mediaInfo: const MediaInfo( + title: 'Bumblebee Video', + artist: 'Flutter', + artworkUrl: + 'https://storage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg', + ), + ) + : null, + ), + ElevatedButton.icon( + icon: const Icon(Icons.volume_off), + label: const Text('Disable Background'), + onPressed: _controller.value.isPlayingInBackground + ? () => _controller.disableBackgroundPlayback() + : null, + ), + ], + ), + const SizedBox(height: 16), + const Text( + 'Tip: Enable background playback, start playing, ' + 'then press the home button. Audio should continue ' + 'playing.', + style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 12, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class _HlsAbrDemo extends StatefulWidget { + const _HlsAbrDemo(this.viewType); + + final VideoViewType viewType; + + @override + State<_HlsAbrDemo> createState() => _HlsAbrDemoState(); +} + +class _HlsAbrDemoState extends State<_HlsAbrDemo> { + late VideoPlayerController _controller; + List _qualities = []; + VideoQuality? _currentQuality; + int _cacheSizeBytes = 0; + bool _cacheEnabled = true; + String? _activeConstraint; + final List _log = []; + + // Quality buttons are built dynamically from getAvailableQualities(). + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.networkUrl( + Uri.parse( + 'https://test-storage.reallifeglobal.com/demo_hls_1080/master.m3u8', + ), + formatHint: VideoFormat.hls, + viewType: widget.viewType, + ); + _controller.addListener(_onControllerUpdate); + _controller.setLooping(true); + _controller.initialize().then((_) { + setState(() {}); + _refreshAll(); + }); + } + + @override + void dispose() { + _controller.removeListener(_onControllerUpdate); + _controller.dispose(); + super.dispose(); + } + + void _onControllerUpdate() { + final VideoQuality? q = _controller.value.currentQuality; + if (q != null && q != _currentQuality) { + _addLog( + 'Quality changed -> ${q.width}x${q.height} ' + '@ ${_formatBitrate(q.bitrate)}', + ); + _currentQuality = q; + } + setState(() {}); + } + + Future _refreshAll() async { + await Future.wait(>[ + _refreshQualities(), + _refreshCacheInfo(), + ]); + } + + Future _refreshQualities() async { + final List qualities = + await _controller.getAvailableQualities(); + // Sort by height ascending so buttons show low-to-high. + qualities.sort( + (VideoQuality a, VideoQuality b) => a.height.compareTo(b.height), + ); + setState(() { + _qualities = qualities; + }); + } + + Future _refreshCacheInfo() async { + final int size = await VideoPlayerController.getCacheSize(); + final bool enabled = await VideoPlayerController.isCacheEnabled(); + setState(() { + _cacheSizeBytes = size; + _cacheEnabled = enabled; + }); + } + + Future _forceQuality(int width, int height, String label) async { + // To force a specific quality, set BOTH max resolution AND max bitrate. + // This tells the track selector to pick the variant that fits both + // constraints. + await _controller.setMaxResolution(width, height); + _addLog('Set max resolution: ${width}x$height ($label)'); + setState(() { + _activeConstraint = label; + }); + } + + Future _removeConstraints() async { + await _controller.setMaxBitrate(999999999); + await _controller.setMaxResolution(9999, 9999); + _addLog('Removed all quality constraints (auto ABR)'); + setState(() { + _activeConstraint = 'Auto'; + }); + } + + void _addLog(String message) { + final String timestamp = DateTime.now().toIso8601String().substring(11, 19); + setState(() { + _log.insert(0, '[$timestamp] $message'); + if (_log.length > 30) { + _log.removeLast(); + } + }); + } + + String _formatBitrate(int bps) { + if (bps <= 0) { + return 'unknown'; + } + if (bps < 1000) { + return '$bps bps'; + } + if (bps < 1000000) { + return '${(bps / 1000).toStringAsFixed(0)} kbps'; + } + return '${(bps / 1000000).toStringAsFixed(1)} Mbps'; + } + + String _formatBytes(int bytes) { + if (bytes < 1024) { + return '$bytes B'; + } + if (bytes < 1024 * 1024) { + return '${(bytes / 1024).toStringAsFixed(1)} KB'; + } + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + } + + String _qualityLabel(VideoQuality q) { + return '${q.width}x${q.height} @ ${_formatBitrate(q.bitrate)}'; + } + + @override + Widget build(BuildContext context) { + final TextStyle? titleStyle = Theme.of(context).textTheme.titleMedium; + const monoStyle = TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ); + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Video player + Container( + padding: const EdgeInsets.all(12), + child: AspectRatio( + aspectRatio: _controller.value.isInitialized + ? _controller.value.aspectRatio + : 16 / 9, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + // Current quality badge overlay + if (_currentQuality != null) + Positioned( + top: 8, + right: 8, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${_currentQuality!.height}p ' + '${_formatBitrate(_currentQuality!.bitrate)}', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Force Quality section + Text('Force Quality', style: titleStyle), + const SizedBox(height: 4), + Text( + 'Active: ${_activeConstraint ?? "Auto (no constraint)"}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Wrap( + spacing: 6, + runSpacing: 6, + children: [ + for (final VideoQuality q in _qualities) + _QualityButton( + label: '${q.height}p', + detail: _formatBitrate(q.bitrate), + isActive: _activeConstraint == '${q.height}p', + onPressed: () => + _forceQuality(q.width, q.height, '${q.height}p'), + ), + _QualityButton( + label: 'Auto', + detail: 'ABR decides', + isActive: _activeConstraint == 'Auto', + onPressed: _removeConstraints, + ), + ], + ), + const SizedBox(height: 8), + Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.amber.shade50, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.amber.shade200), + ), + child: const Text( + 'Note: Already-buffered segments play at their original ' + 'quality. After changing quality, seek forward past the ' + 'buffer to see the new quality immediately, or wait for ' + 'the buffer to drain during normal playback.', + style: TextStyle(fontSize: 11), + ), + ), + + // Available qualities + if (_qualities.isNotEmpty) ...[ + const SizedBox(height: 12), + Text('Available Variants', style: titleStyle), + const SizedBox(height: 4), + for (final VideoQuality q in _qualities) + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text( + '${q.isSelected ? "-> " : " "}' + '${_qualityLabel(q)}' + '${q.codec != null ? " [${q.codec}]" : ""}', + style: monoStyle.copyWith( + fontWeight: q.isSelected + ? FontWeight.bold + : FontWeight.normal, + color: q.isSelected ? Colors.green.shade700 : null, + ), + ), + ), + ] else ...[ + const SizedBox(height: 8), + TextButton.icon( + icon: const Icon(Icons.refresh, size: 16), + label: const Text('Load available qualities'), + onPressed: _refreshQualities, + ), + ], + const Divider(), + + // Cache section + Row( + children: [ + Text('Cache', style: titleStyle), + const Spacer(), + Text( + '${_formatBytes(_cacheSizeBytes)}' + ' ${_cacheEnabled ? "(ON)" : "(OFF)"}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.refresh, size: 18), + onPressed: _refreshCacheInfo, + visualDensity: VisualDensity.compact, + tooltip: 'Refresh cache info', + ), + ], + ), + Wrap( + spacing: 6, + runSpacing: 4, + children: [ + OutlinedButton( + onPressed: () async { + await VideoPlayerController.clearCache(); + _addLog('Cache cleared'); + await _refreshCacheInfo(); + }, + child: const Text('Clear'), + ), + OutlinedButton( + onPressed: () async { + await VideoPlayerController.setCacheEnabled( + !_cacheEnabled); + _addLog( + 'Cache ${!_cacheEnabled ? "enabled" : "disabled"}', + ); + await _refreshCacheInfo(); + }, + child: Text(_cacheEnabled ? 'Disable' : 'Enable'), + ), + OutlinedButton( + onPressed: () async { + await VideoPlayerController.setCacheMaxSize( + 100 * 1024 * 1024, + ); + _addLog('Cache max set to 100 MB'); + }, + child: const Text('100 MB'), + ), + OutlinedButton( + onPressed: () async { + await VideoPlayerController.setCacheMaxSize( + 500 * 1024 * 1024, + ); + _addLog('Cache max set to 500 MB'); + }, + child: const Text('500 MB'), + ), + ], + ), + const Divider(), + + // Log section + Row( + children: [ + Text('Event Log', style: titleStyle), + const Spacer(), + TextButton( + onPressed: () => setState(() => _log.clear()), + child: const Text('Clear'), + ), + ], + ), + Container( + width: double.infinity, + height: 140, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(6), + ), + child: _log.isEmpty + ? Text( + 'No events yet...', + style: monoStyle.copyWith(color: Colors.grey), + ) + : ListView.builder( + itemCount: _log.length, + itemBuilder: (BuildContext context, int index) { + return Text( + _log[index], + style: monoStyle.copyWith( + color: Colors.green.shade300, + ), + ); + }, + ), + ), + const SizedBox(height: 24), + ], + ), + ), + ], + ), + ); + } +} + +class _QualityButton extends StatelessWidget { + const _QualityButton({ + required this.label, + required this.detail, + required this.isActive, + required this.onPressed, + }); + + final String label; + final String detail; + final bool isActive; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: isActive ? Colors.blue : null, + foregroundColor: isActive ? Colors.white : null, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + onPressed: onPressed, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), + Text(detail, style: const TextStyle(fontSize: 10)), + ], + ), + ); + } +} + class _PlayerVideoAndPopPage extends StatefulWidget { @override _PlayerVideoAndPopPageState createState() => _PlayerVideoAndPopPageState(); diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 3e66109d7feb..7ca2a09deba6 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -17,12 +17,16 @@ import 'src/closed_caption_file.dart'; export 'package:video_player_platform_interface/video_player_platform_interface.dart' show + AndroidVideoPlayerOptions, DataSourceType, DurationRange, + MediaInfo, + VideoDecoderInfo, VideoFormat, VideoPlayerOptions, VideoPlayerWebOptions, VideoPlayerWebOptionsControls, + VideoQuality, VideoViewType; export 'src/closed_caption_file.dart'; @@ -173,6 +177,13 @@ class VideoPlayerValue { this.rotationCorrection = 0, this.errorDescription, this.isCompleted = false, + this.isPipActive = false, + this.isPlayingInBackground = false, + this.isAutoEnterPipEnabled = false, + this.pipSize, + this.currentQuality, + this.decoderName, + this.isDecoderHardwareAccelerated, }); /// Returns an instance for a video that hasn't been loaded. @@ -190,6 +201,7 @@ class VideoPlayerValue { /// This constant is just to indicate that parameter is not passed to [copyWith] /// workaround for this issue https://github.com/dart-lang/language/issues/2009 static const String _defaultErrorDescription = 'defaultErrorDescription'; + static const Size _defaultPipSize = Size(-1, -1); /// The total duration of the video. /// @@ -239,6 +251,41 @@ class VideoPlayerValue { /// Does not update if video is looping. final bool isCompleted; + /// Whether Picture-in-Picture mode is currently active. + final bool isPipActive; + + /// Whether the video is currently playing in the background. + final bool isPlayingInBackground; + + /// Whether auto-PiP is enabled (enter PiP automatically when app backgrounds). + final bool isAutoEnterPipEnabled; + + /// The PiP window size in dp, reported by the native platform. + /// + /// Non-null only while [isPipActive] is true. Use this instead of + /// [MediaQuery.sizeOf] for layout in PiP mode, since Flutter's viewport + /// metrics may not update in time (or at all) on some Android versions. + final Size? pipSize; + + /// The currently playing video quality (from the last ABR quality change event). + /// + /// This is null if the platform has not yet reported a quality, or for + /// non-adaptive streams. + final platform_interface.VideoQuality? currentQuality; + + /// The name of the currently active video decoder. + /// + /// Null until the decoder is first initialized. Updated whenever the + /// decoder changes (e.g. after calling [VideoPlayerController.setVideoDecoder]). + /// Currently only reported on Android. + final String? decoderName; + + /// Whether the currently active decoder is hardware-accelerated. + /// + /// Null until the decoder is first initialized. + /// Currently only reported on Android. + final bool? isDecoderHardwareAccelerated; + /// The [size] of the currently loaded video. final Size size; @@ -287,6 +334,13 @@ class VideoPlayerValue { int? rotationCorrection, String? errorDescription = _defaultErrorDescription, bool? isCompleted, + bool? isPipActive, + bool? isPlayingInBackground, + bool? isAutoEnterPipEnabled, + Size? pipSize = _defaultPipSize, + platform_interface.VideoQuality? currentQuality, + String? decoderName, + bool? isDecoderHardwareAccelerated, }) { return VideoPlayerValue( duration: duration ?? this.duration, @@ -306,6 +360,13 @@ class VideoPlayerValue { ? errorDescription : this.errorDescription, isCompleted: isCompleted ?? this.isCompleted, + isPipActive: isPipActive ?? this.isPipActive, + isPlayingInBackground: isPlayingInBackground ?? this.isPlayingInBackground, + isAutoEnterPipEnabled: isAutoEnterPipEnabled ?? this.isAutoEnterPipEnabled, + pipSize: pipSize != _defaultPipSize ? pipSize : this.pipSize, + currentQuality: currentQuality ?? this.currentQuality, + decoderName: decoderName ?? this.decoderName, + isDecoderHardwareAccelerated: isDecoderHardwareAccelerated ?? this.isDecoderHardwareAccelerated, ); } @@ -325,7 +386,14 @@ class VideoPlayerValue { 'volume: $volume, ' 'playbackSpeed: $playbackSpeed, ' 'errorDescription: $errorDescription, ' - 'isCompleted: $isCompleted),'; + 'isCompleted: $isCompleted, ' + 'isPipActive: $isPipActive, ' + 'isPlayingInBackground: $isPlayingInBackground, ' + 'isAutoEnterPipEnabled: $isAutoEnterPipEnabled, ' + 'pipSize: $pipSize, ' + 'currentQuality: $currentQuality, ' + 'decoderName: $decoderName, ' + 'isDecoderHardwareAccelerated: $isDecoderHardwareAccelerated)'; } @override @@ -347,7 +415,14 @@ class VideoPlayerValue { size == other.size && rotationCorrection == other.rotationCorrection && isInitialized == other.isInitialized && - isCompleted == other.isCompleted; + isCompleted == other.isCompleted && + isPipActive == other.isPipActive && + isPlayingInBackground == other.isPlayingInBackground && + isAutoEnterPipEnabled == other.isAutoEnterPipEnabled && + pipSize == other.pipSize && + currentQuality == other.currentQuality && + decoderName == other.decoderName && + isDecoderHardwareAccelerated == other.isDecoderHardwareAccelerated; @override int get hashCode => Object.hash( @@ -364,8 +439,7 @@ class VideoPlayerValue { errorDescription, size, rotationCorrection, - isInitialized, - isCompleted, + Object.hash(isInitialized, isCompleted, isPipActive, isPlayingInBackground, isAutoEnterPipEnabled, pipSize, currentQuality, decoderName, isDecoderHardwareAccelerated), ); } @@ -520,7 +594,12 @@ class VideoPlayerController extends ValueNotifier { List? _sortedCaptions; Timer? _timer; + Timer? _pipDismissTimer; bool _isDisposed = false; + // PiP dismiss detection: these two flags track signals that may arrive in + // either order. When both are true, the user dismissed PiP. + bool _sawPipExit = false; + bool _sawLifecyclePausedDuringPip = false; Completer? _creatingCompleter; StreamSubscription? _eventSubscription; _VideoAppLifeCycleObserver? _lifeCycleObserver; @@ -576,6 +655,7 @@ class VideoPlayerController extends ValueNotifier { final creationOptions = platform_interface.VideoCreationOptions( dataSource: dataSourceDescription, viewType: viewType, + androidOptions: videoPlayerOptions?.androidOptions, ); if (videoPlayerOptions?.mixWithOthers != null) { @@ -649,6 +729,35 @@ class VideoPlayerController extends ValueNotifier { } else { value = value.copyWith(isPlaying: event.isPlaying); } + case platform_interface.VideoEventType.pipStateChanged: + final bool isPip = event.isPipActive ?? false; + value = value.copyWith( + isPipActive: isPip, + pipSize: isPip ? event.pipWindowSize : null, + ); + // PiP dismiss detection. The PiP event and lifecycle events may + // arrive in either order. Set our flag and check if the lifecycle + // already reached `paused` while PiP was still active. + if (!isPip) { + _sawPipExit = true; + _checkPipDismissed(); + // Fallback: clear stale flag if no lifecycle event arrives + // (e.g., on iOS where PiP dismiss doesn't cause lifecycle paused). + _pipDismissTimer?.cancel(); + _pipDismissTimer = Timer(const Duration(seconds: 2), () { + _sawPipExit = false; + _sawLifecyclePausedDuringPip = false; + }); + } + case platform_interface.VideoEventType.qualityChanged: + if (event.quality != null) { + value = value.copyWith(currentQuality: event.quality); + } + case platform_interface.VideoEventType.decoderChanged: + value = value.copyWith( + decoderName: event.decoderName, + isDecoderHardwareAccelerated: event.isDecoderHardwareAccelerated, + ); case platform_interface.VideoEventType.unknown: break; } @@ -684,6 +793,7 @@ class VideoPlayerController extends ValueNotifier { if (!_isDisposed) { _isDisposed = true; _timer?.cancel(); + _pipDismissTimer?.cancel(); await _eventSubscription?.cancel(); await _videoPlayerPlatform.dispose(_playerId); } @@ -1027,7 +1137,204 @@ class VideoPlayerController extends ValueNotifier { return _videoPlayerPlatform.isAudioTrackSupportAvailable(); } + /// Returns whether Picture-in-Picture mode is supported on this device. + Future get isPipSupported async { + return _videoPlayerPlatform.isPipSupported(); + } + + /// Enters Picture-in-Picture mode. + Future enterPip() async { + if (_isDisposedOrNotInitialized) { + return; + } + await _videoPlayerPlatform.enterPip(_playerId); + } + + /// Exits Picture-in-Picture mode. + Future exitPip() async { + if (_isDisposedOrNotInitialized) { + return; + } + await _videoPlayerPlatform.exitPip(_playerId); + } + + /// Sets whether PiP should be entered automatically when the app + /// goes to background (Android 12+ only). + Future setAutoEnterPip(bool enabled) async { + if (_isDisposedOrNotInitialized) { + return; + } + await _videoPlayerPlatform.setAutoEnterPip(_playerId, enabled); + value = value.copyWith(isAutoEnterPipEnabled: enabled); + } + + /// Enables background playback for this player. + /// + /// When enabled, audio continues playing when the app is backgrounded. + /// On Android, this starts a foreground service with a media notification. + /// On iOS, this configures the audio session and sets up lock screen controls. + Future enableBackgroundPlayback({ + platform_interface.MediaInfo? mediaInfo, + }) async { + if (_isDisposedOrNotInitialized) { + return; + } + await _videoPlayerPlatform.enableBackgroundPlayback( + _playerId, + mediaInfo: mediaInfo, + ); + value = value.copyWith(isPlayingInBackground: true); + } + + /// Disables background playback for this player. + Future disableBackgroundPlayback() async { + if (_isDisposedOrNotInitialized) { + return; + } + await _videoPlayerPlatform.disableBackgroundPlayback(_playerId); + value = value.copyWith(isPlayingInBackground: false); + } + + // Cache control — static methods since cache is shared across all players. + + /// Sets the maximum cache size in bytes. Default is 500 MB. + /// + /// On Android, this controls the LRU eviction threshold for the + /// Media3 SimpleCache that stores HLS segments. + /// On iOS, this is currently a no-op (stored for future use). + static Future setCacheMaxSize(int maxSizeBytes) { + return _videoPlayerPlatform.setCacheMaxSize(maxSizeBytes); + } + + /// Clears all cached video data. + static Future clearCache() { + return _videoPlayerPlatform.clearCache(); + } + + /// Returns the current cache size in bytes. + /// + /// Returns 0 on iOS (cache not yet implemented). + static Future getCacheSize() { + return _videoPlayerPlatform.getCacheSize(); + } + + /// Returns whether caching is enabled. + /// + /// Returns false on iOS (cache not yet implemented). + static Future isCacheEnabled() { + return _videoPlayerPlatform.isCacheEnabled(); + } + + /// Enables or disables caching. + /// + /// No-op on iOS (cache not yet implemented). + static Future setCacheEnabled(bool enabled) { + return _videoPlayerPlatform.setCacheEnabled(enabled); + } + + // Adaptive Bitrate (ABR) control methods. + + /// Returns the available video quality variants for this player. + /// + /// For HLS/DASH streams, this returns the list of renditions (resolution + + /// bitrate combinations) available in the manifest. For progressive MP4, + /// returns an empty list. + /// + /// On iOS < 15, returns an empty list (AVAssetVariant API not available). + Future> getAvailableQualities() async { + if (_isDisposedOrNotInitialized) { + return []; + } + return _videoPlayerPlatform.getAvailableQualities(_playerId); + } + + /// Returns the currently playing video quality, or null if unknown. + Future getCurrentQuality() async { + if (_isDisposedOrNotInitialized) { + return null; + } + return _videoPlayerPlatform.getCurrentQuality(_playerId); + } + + /// Sets the maximum video bitrate in bits per second. + /// + /// The player will not select a quality variant with a bitrate higher + /// than this value. Set to 0 or a very large number to remove the limit. + /// + /// On Android, this sets ExoPlayer's DefaultTrackSelector maxVideoBitrate. + /// On iOS, this sets AVPlayerItem.preferredPeakBitRate. + Future setMaxBitrate(int maxBitrateBps) async { + if (_isDisposedOrNotInitialized) { + return; + } + await _videoPlayerPlatform.setMaxBitrate(_playerId, maxBitrateBps); + } + + /// Sets the maximum video resolution. + /// + /// The player will not select a quality variant with a resolution larger + /// than the given dimensions. + /// + /// On Android, this sets ExoPlayer's DefaultTrackSelector maxVideoSize. + /// On iOS, this sets AVPlayerItem.preferredMaximumResolution (iOS 11+). + Future setMaxResolution(int width, int height) async { + if (_isDisposedOrNotInitialized) { + return; + } + await _videoPlayerPlatform.setMaxResolution(_playerId, width, height); + } + + // Decoder selection methods + + /// Returns the available video decoders for the current video. + /// + /// The list is filtered by the video's MIME type and indicates whether + /// each decoder is hardware-accelerated or software-only. + /// Currently only supported on Android. + Future> + getAvailableDecoders() async { + if (_isDisposedOrNotInitialized) { + return []; + } + return _videoPlayerPlatform.getAvailableDecoders(_playerId); + } + + /// Returns the name of the currently active video decoder, or null if + /// no decoder has been initialized yet. + /// Currently only supported on Android. + Future getCurrentDecoderName() async { + if (_isDisposedOrNotInitialized) { + return null; + } + return _videoPlayerPlatform.getCurrentDecoderName(_playerId); + } + + /// Forces the player to use a specific video decoder by name. + /// + /// Pass null to revert to automatic decoder selection. + /// This rebuilds the underlying player instance, causing a brief + /// playback interruption (~200-500ms). Position, volume, speed, + /// and looping state are preserved across the switch. + /// Currently only supported on Android. + Future setVideoDecoder(String? decoderName) async { + if (_isDisposedOrNotInitialized) { + return; + } + await _videoPlayerPlatform.setVideoDecoder(_playerId, decoderName); + } + bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized; + + /// Called when either signal (PiP exit event or lifecycle paused) arrives. + /// Pauses playback only when both signals confirm a dismiss. + void _checkPipDismissed() { + if (_sawPipExit && _sawLifecyclePausedDuringPip) { + _pipDismissTimer?.cancel(); + _sawPipExit = false; + _sawLifecyclePausedDuringPip = false; + pause(); + } + } } class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { @@ -1042,7 +1349,31 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { + // PiP dismiss/expand detection. While PiP is active or was recently + // exited, route all lifecycle events through the PiP detection logic + // instead of normal pause/resume handling. + if (_controller.value.isPipActive || _controller._sawPipExit) { + if (state == AppLifecycleState.paused) { + // Lifecycle reached background — this is the "dismissed" signal. + // The PiP exit event may or may not have arrived yet. + _controller._sawLifecyclePausedDuringPip = true; + _controller._checkPipDismissed(); + } else if (state == AppLifecycleState.resumed) { + // Back to foreground — expanded, not dismissed. Clear flags. + _controller._pipDismissTimer?.cancel(); + _controller._sawPipExit = false; + _controller._sawLifecyclePausedDuringPip = false; + } + // Skip `inactive`/`hidden` — they're transitional. + return; + } + + // Normal lifecycle handling (not PiP related). if (state == AppLifecycleState.paused) { + if (_controller.value.isPlayingInBackground || + _controller.value.isAutoEnterPipEnabled) { + return; + } _wasPlayingBeforePause = _controller.value.isPlaying; _controller.pause(); } else if (state == AppLifecycleState.resumed) { diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index c55f4e823c4e..13d2ec78ac14 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -26,10 +26,14 @@ dependencies: flutter: sdk: flutter html: ^0.15.0 - video_player_android: ^2.9.1 - video_player_avfoundation: ^2.9.0 - video_player_platform_interface: ^6.6.0 - video_player_web: ^2.1.0 + video_player_android: + path: ../video_player_android + video_player_avfoundation: + path: ../video_player_avfoundation + video_player_platform_interface: + path: ../video_player_platform_interface + video_player_web: + path: ../video_player_web dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 77608a45d0ac..347a9bfb718a 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -130,6 +130,47 @@ class FakeController extends ValueNotifier return true; } + @override + Future get isPipSupported async => false; + + @override + Future enterPip() async {} + + @override + Future exitPip() async {} + + @override + Future setAutoEnterPip(bool enabled) async {} + + @override + Future enableBackgroundPlayback({MediaInfo? mediaInfo}) async {} + + @override + Future disableBackgroundPlayback() async {} + + @override + Future> getAvailableQualities() async => + []; + + @override + Future getCurrentQuality() async => null; + + @override + Future setMaxBitrate(int maxBitrateBps) async {} + + @override + Future setMaxResolution(int width, int height) async {} + + @override + Future> getAvailableDecoders() async => + []; + + @override + Future getCurrentDecoderName() async => null; + + @override + Future setVideoDecoder(String? decoderName) async {} + String? selectedAudioTrackId; } @@ -1858,9 +1899,16 @@ void main() { 'volume: 0.5, ' 'playbackSpeed: 1.5, ' 'errorDescription: null, ' - 'isCompleted: false),', - ); - }); + 'isCompleted: false, ' + 'isPipActive: false, ' + 'isPlayingInBackground: false, ' + 'isAutoEnterPipEnabled: false, ' + 'pipSize: null, ' + 'currentQuality: null, ' + 'decoderName: null, ' + 'isDecoderHardwareAccelerated: null)', + ); + }); group('copyWith()', () { test('exact copy', () { diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle index 472d36727a96..a45484e52f6f 100644 --- a/packages/video_player/video_player_android/android/build.gradle +++ b/packages/video_player/video_player_android/android/build.gradle @@ -60,6 +60,7 @@ android { implementation("androidx.media3:media3-exoplayer-dash:${exoplayer_version}") implementation("androidx.media3:media3-exoplayer-rtsp:${exoplayer_version}") implementation("androidx.media3:media3-exoplayer-smoothstreaming:${exoplayer_version}") + implementation("androidx.media3:media3-session:${exoplayer_version}") testImplementation("junit:junit:4.13.2") testImplementation("androidx.test:core:1.7.0") testImplementation("org.mockito:mockito-core:5.22.0") diff --git a/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml b/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml index e6e98bfe5a31..29cc10187851 100644 --- a/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml +++ b/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml @@ -1,3 +1,17 @@ + + + + + + + + + + + diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java index 33988786a78a..9458e30adc28 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java @@ -16,6 +16,8 @@ public abstract class ExoPlayerEventListener implements Player.Listener { private boolean isInitialized = false; protected final ExoPlayer exoPlayer; protected final VideoPlayerCallbacks events; + private final int maxPlayerRecoveryAttempts; + private int recoveryAttemptCount = 0; protected enum RotationDegrees { ROTATE_0(0), @@ -44,9 +46,12 @@ public int getDegrees() { } public ExoPlayerEventListener( - @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events) { + @NonNull ExoPlayer exoPlayer, + @NonNull VideoPlayerCallbacks events, + int maxPlayerRecoveryAttempts) { this.exoPlayer = exoPlayer; this.events = events; + this.maxPlayerRecoveryAttempts = maxPlayerRecoveryAttempts; } protected abstract void sendInitialized(); @@ -60,6 +65,7 @@ public void onPlaybackStateChanged(final int playbackState) { break; case Player.STATE_READY: platformState = PlatformPlaybackState.READY; + recoveryAttemptCount = 0; if (!isInitialized) { isInitialized = true; sendInitialized(); @@ -82,11 +88,27 @@ public void onPlayerError(@NonNull final PlaybackException error) { // https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window exoPlayer.seekToDefaultPosition(); exoPlayer.prepare(); + } else if (isTransientNetworkError(error)) { + recoveryAttemptCount++; + if (recoveryAttemptCount <= maxPlayerRecoveryAttempts) { + // Transient network errors (e.g. airplane mode, connection timeout) are + // recoverable. Re-prepare the player so it retries when network returns. + exoPlayer.prepare(); + } else { + events.onError("VideoError", "Video player had error " + error, null); + } } else { events.onError("VideoError", "Video player had error " + error, null); } } + private boolean isTransientNetworkError(@NonNull PlaybackException error) { + int code = error.errorCode; + return code == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED + || code == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT + || code == PlaybackException.ERROR_CODE_IO_UNSPECIFIED; + } + @Override public void onIsPlayingChanged(boolean isPlaying) { events.onIsPlayingStateUpdate(isPlaying); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java index 0c85e0e8267b..2ec28d0a39fa 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java @@ -15,6 +15,7 @@ import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.DefaultHttpDataSource; +import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; import java.util.Map; @@ -77,6 +78,15 @@ MediaSource.Factory getMediaSourceFactory( Context context, DefaultHttpDataSource.Factory initialFactory) { unstableUpdateDataSourceFactory(initialFactory, httpHeaders, userAgent); DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context, initialFactory); + + // Wrap with CacheDataSource if caching is enabled. + if (VideoCacheManager.isEnabled()) { + dataSourceFactory = + new CacheDataSource.Factory() + .setCache(VideoCacheManager.getCache(context)) + .setUpstreamDataSourceFactory(dataSourceFactory); + } + return new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory); } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoCacheManager.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoCacheManager.java new file mode 100644 index 000000000000..94033c2278ae --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoCacheManager.java @@ -0,0 +1,109 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayer; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.database.StandaloneDatabaseProvider; +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor; +import androidx.media3.datasource.cache.SimpleCache; +import java.io.File; + +/** + * Singleton managing a {@link SimpleCache} instance for HLS segment caching. + * + *

Wrapping streaming data sources with a {@link + * androidx.media3.datasource.cache.CacheDataSource.Factory} backed by this cache allows previously + * fetched HLS segments to be served from disk on re-watch, avoiding redundant downloads. + */ +@UnstableApi +final class VideoCacheManager { + private static final String CACHE_DIR_NAME = "video_player_cache"; + private static final long DEFAULT_MAX_CACHE_SIZE = 500L * 1024 * 1024; // 500 MB + + private static SimpleCache cache; + private static StandaloneDatabaseProvider databaseProvider; + private static long maxCacheSize = DEFAULT_MAX_CACHE_SIZE; + private static boolean enabled = true; + + private VideoCacheManager() {} + + /** + * Returns the shared {@link SimpleCache} instance, creating it lazily if needed. + * + * @param context application context. + * @return the cache instance. + */ + @NonNull + static synchronized SimpleCache getCache(@NonNull Context context) { + if (cache == null) { + File cacheDir = new File(context.getCacheDir(), CACHE_DIR_NAME); + if (!cacheDir.exists()) { + cacheDir.mkdirs(); + } + databaseProvider = new StandaloneDatabaseProvider(context); + LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxCacheSize); + cache = new SimpleCache(cacheDir, evictor, databaseProvider); + } + return cache; + } + + /** Returns whether caching is enabled. */ + static synchronized boolean isEnabled() { + return enabled; + } + + /** Enables or disables caching. */ + static synchronized void setEnabled(boolean enable) { + enabled = enable; + } + + /** + * Sets the maximum cache size. Takes effect the next time the cache is created (after a {@link + * #release()}). + * + * @param bytes maximum size in bytes. + */ + static synchronized void setMaxCacheSize(long bytes) { + maxCacheSize = bytes; + // If cache is already running, release and let it be re-created with the new size. + if (cache != null) { + release(); + } + } + + /** Clears all cached data. */ + static synchronized void clearCache(@NonNull Context context) { + if (cache != null) { + release(); + } + // Delete the cache directory on disk. + File cacheDir = new File(context.getCacheDir(), CACHE_DIR_NAME); + SimpleCache.delete(cacheDir, /* databaseProvider= */ null); + } + + /** + * Returns the current cache size in bytes. + * + * @return size in bytes, or 0 if cache is not initialized. + */ + static synchronized long getCacheSize() { + if (cache == null) { + return 0; + } + return cache.getCacheSpace(); + } + + /** Releases the cache instance. Should be called when the plugin is detached. */ + static synchronized void release() { + if (cache != null) { + cache.release(); + cache = null; + } + databaseProvider = null; + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 7cfb5c1c13be..78fa9bf985cc 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -7,6 +7,9 @@ import static androidx.media3.common.Player.REPEAT_MODE_ALL; import static androidx.media3.common.Player.REPEAT_MODE_OFF; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.media3.common.AudioAttributes; @@ -19,9 +22,13 @@ import androidx.media3.common.Tracks; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.analytics.AnalyticsListener; +import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; +import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import io.flutter.view.TextureRegistry.SurfaceProducer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -37,6 +44,19 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi { // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. @UnstableApi @Nullable protected DefaultTrackSelector trackSelector; + // Stored for ExoPlayer rebuild when switching decoders. + @NonNull protected final MediaItem mediaItem; + @NonNull protected final VideoPlayerOptions options; + + // Stored listener references for removal during ExoPlayer rebuild. + @NonNull private ExoPlayerEventListener exoPlayerEventListener; + @NonNull private AnalyticsListener analyticsListener; + + // Decoder tracking. + @Nullable protected String currentVideoDecoderName; + @Nullable protected String forcedDecoderName; + @Nullable private String lastKnownVideoMimeType; + /** A closure-compatible signature since {@link java.util.function.Supplier} is API level 24. */ public interface ExoPlayerProvider { /** @@ -66,6 +86,9 @@ public VideoPlayer( @NonNull ExoPlayerProvider exoPlayerProvider) { this.videoPlayerEvents = events; this.surfaceProducer = surfaceProducer; + this.mediaItem = mediaItem; + this.options = options; + this.maxPlayerRecoveryAttempts = options.maxPlayerRecoveryAttempts; exoPlayer = exoPlayerProvider.get(); // Try to get the track selector from the ExoPlayer if it was built with one @@ -75,7 +98,10 @@ public VideoPlayer( exoPlayer.setMediaItem(mediaItem); exoPlayer.prepare(); - exoPlayer.addListener(createExoPlayerEventListener(exoPlayer, surfaceProducer)); + exoPlayerEventListener = createExoPlayerEventListener(exoPlayer, surfaceProducer); + analyticsListener = createAnalyticsListener(); + exoPlayer.addListener(exoPlayerEventListener); + exoPlayer.addAnalyticsListener(analyticsListener); setAudioAttributes(exoPlayer, options.mixWithOthers); } @@ -83,6 +109,8 @@ public void setDisposeHandler(@Nullable DisposeHandler handler) { disposeHandler = handler; } + protected int maxPlayerRecoveryAttempts = 3; + @NonNull protected abstract ExoPlayerEventListener createExoPlayerEventListener( @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer); @@ -233,6 +261,289 @@ public void selectAudioTrack(long groupIndex, long trackIndex) { trackSelector.buildUponParameters().setOverrideForType(override).build()); } + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. + @UnstableApi + @Override + public @NonNull List getAvailableQualities() { + List qualities = new ArrayList<>(); + Tracks tracks = exoPlayer.getCurrentTracks(); + + for (int groupIndex = 0; groupIndex < tracks.getGroups().size(); groupIndex++) { + Tracks.Group group = tracks.getGroups().get(groupIndex); + if (group.getType() == C.TRACK_TYPE_VIDEO) { + for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { + Format format = group.getTrackFormat(trackIndex); + boolean isSelected = group.isTrackSelected(trackIndex); + + PlatformVideoQuality quality = + new PlatformVideoQuality( + format.width > 0 ? (long) format.width : 0L, + format.height > 0 ? (long) format.height : 0L, + format.bitrate != Format.NO_VALUE ? (long) format.bitrate : 0L, + format.codecs, + isSelected); + qualities.add(quality); + } + } + } + return qualities; + } + + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. + @UnstableApi + @Override + public @Nullable PlatformVideoQuality getCurrentQuality() { + Format format = exoPlayer.getVideoFormat(); + if (format == null) { + return null; + } + PlatformVideoQuality quality = + new PlatformVideoQuality( + format.width > 0 ? (long) format.width : 0L, + format.height > 0 ? (long) format.height : 0L, + format.bitrate != Format.NO_VALUE ? (long) format.bitrate : 0L, + format.codecs, + /* isSelected= */ true); + return quality; + } + + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. + @UnstableApi + @Override + public void setMaxBitrate(long maxBitrateBps) { + if (trackSelector == null) { + return; + } + trackSelector.setParameters( + trackSelector.buildUponParameters().setMaxVideoBitrate((int) maxBitrateBps).build()); + } + + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. + @UnstableApi + @Override + public void setMaxResolution(long width, long height) { + if (trackSelector == null) { + return; + } + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setMaxVideoSize((int) width, (int) height) + .build()); + } + + @UnstableApi + private AnalyticsListener createAnalyticsListener() { + return new AnalyticsListener() { + private int lastReportedWidth = -1; + private int lastReportedHeight = -1; + private int lastReportedBitrate = -1; + + @Override + public void onDownstreamFormatChanged( + @NonNull EventTime eventTime, @NonNull MediaLoadData mediaLoadData) { + if (mediaLoadData.trackFormat == null) { + return; + } + // Accept TRACK_TYPE_VIDEO (demuxed) or TRACK_TYPE_DEFAULT (muxed HLS) + // when the format has video dimensions. + int trackType = mediaLoadData.trackType; + Format format = mediaLoadData.trackFormat; + boolean isVideoFormat = (trackType == C.TRACK_TYPE_VIDEO) + || (trackType == C.TRACK_TYPE_DEFAULT && format.width > 0 && format.height > 0); + if (!isVideoFormat) { + return; + } + int width = format.width > 0 ? format.width : 0; + int height = format.height > 0 ? format.height : 0; + int bitrate = format.bitrate != Format.NO_VALUE ? format.bitrate : 0; + // Skip duplicate events + if (width == lastReportedWidth && height == lastReportedHeight + && bitrate == lastReportedBitrate) { + return; + } + lastReportedWidth = width; + lastReportedHeight = height; + lastReportedBitrate = bitrate; + videoPlayerEvents.onVideoQualityChanged(width, height, bitrate, format.codecs); + } + + @Override + public void onVideoDecoderInitialized( + @NonNull EventTime eventTime, + @NonNull String decoderName, + long initializedTimestampMs, + long initializationDurationMs) { + currentVideoDecoderName = decoderName; + boolean isHw = isHardwareDecoder(decoderName); + videoPlayerEvents.onDecoderChanged(decoderName, isHw); + } + }; + } + + // Decoder selection methods + + /** + * Returns whether a decoder name indicates hardware acceleration. + * On API 29+ uses MediaCodecInfo; below that uses name heuristics. + */ + static boolean isHardwareDecoder(@NonNull String decoderName) { + // Software decoder name prefixes + return !decoderName.startsWith("OMX.google.") + && !decoderName.startsWith("c2.android.") + && !decoderName.startsWith("c2.google."); + } + + static boolean isSoftwareDecoder(@NonNull String decoderName) { + return !isHardwareDecoder(decoderName); + } + + @Override + public @NonNull List getAvailableDecoders() { + List decoders = new ArrayList<>(); + Format videoFormat = exoPlayer.getVideoFormat(); + String mimeType = null; + if (videoFormat != null && videoFormat.sampleMimeType != null) { + mimeType = videoFormat.sampleMimeType; + lastKnownVideoMimeType = mimeType; + } else { + mimeType = lastKnownVideoMimeType; + } + if (mimeType == null) { + return decoders; + } + + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + if (codecInfo.isEncoder()) { + continue; + } + String[] supportedTypes = codecInfo.getSupportedTypes(); + for (String type : supportedTypes) { + if (type.equalsIgnoreCase(mimeType)) { + String name = codecInfo.getName(); + boolean isHw; + boolean isSw; + if (Build.VERSION.SDK_INT >= 29) { + isHw = codecInfo.isHardwareAccelerated(); + isSw = codecInfo.isSoftwareOnly(); + } else { + isHw = isHardwareDecoder(name); + isSw = isSoftwareDecoder(name); + } + boolean isSelected = name.equals(currentVideoDecoderName); + decoders.add(new PlatformVideoDecoder(name, mimeType, isHw, isSw, isSelected)); + break; + } + } + } + return decoders; + } + + @Override + public @Nullable String getCurrentDecoderName() { + return currentVideoDecoderName; + } + + @UnstableApi + @Override + public void setVideoDecoder(@Nullable String decoderName) { + this.forcedDecoderName = decoderName; + + // Capture current playback state before touching the player. + long position = exoPlayer.getCurrentPosition(); + boolean wasPlaying = exoPlayer.isPlaying(); + boolean isLooping = exoPlayer.getRepeatMode() == REPEAT_MODE_ALL; + float volume = exoPlayer.getVolume(); + float speed = exoPlayer.getPlaybackParameters().speed; + + // Remove all listeners BEFORE stopping/releasing to prevent stale + // callbacks (errors, state changes) from reaching Dart during teardown. + exoPlayer.removeListener(exoPlayerEventListener); + exoPlayer.removeAnalyticsListener(analyticsListener); + + // Release old player. + exoPlayer.stop(); + exoPlayer.release(); + + // Build new player with forced decoder. + ExoPlayerProvider provider = createExoPlayerProvider(decoderName); + exoPlayer = provider.get(); + + // Recapture track selector. + if (exoPlayer.getTrackSelector() instanceof DefaultTrackSelector) { + trackSelector = (DefaultTrackSelector) exoPlayer.getTrackSelector(); + } else { + trackSelector = null; + } + + // Restore state. + exoPlayer.setMediaItem(mediaItem); + exoPlayer.prepare(); + exoPlayerEventListener = createExoPlayerEventListener(exoPlayer, surfaceProducer); + analyticsListener = createAnalyticsListener(); + exoPlayer.addListener(exoPlayerEventListener); + exoPlayer.addAnalyticsListener(analyticsListener); + setAudioAttributes(exoPlayer, options.mixWithOthers); + exoPlayer.setRepeatMode(isLooping ? REPEAT_MODE_ALL : REPEAT_MODE_OFF); + exoPlayer.setVolume(volume); + exoPlayer.setPlaybackParameters(new PlaybackParameters(speed)); + exoPlayer.seekTo(position); + + // Re-attach surface for texture-based players (handled by subclass). + onPlayerRebuilt(exoPlayer); + + if (wasPlaying) { + exoPlayer.play(); + } + } + + /** + * Called after the ExoPlayer is rebuilt (e.g. during decoder switch). + * Subclasses can override to re-attach surfaces. + */ + protected void onPlayerRebuilt(@NonNull ExoPlayer newPlayer) { + // Default: no-op. TextureVideoPlayer overrides to re-attach surface. + } + + /** + * Creates an ExoPlayerProvider that optionally forces a specific decoder. + * Subclasses must implement this to build ExoPlayer with the right context. + */ + @NonNull + protected abstract ExoPlayerProvider createExoPlayerProvider(@Nullable String forcedDecoderName); + + /** + * Creates a MediaCodecSelector that prioritizes the given decoder name. + * If forcedDecoderName is null, returns the default selector. + */ + @UnstableApi + @NonNull + public static MediaCodecSelector createSelectorForDecoder(@Nullable String forcedDecoderName) { + if (forcedDecoderName == null) { + return MediaCodecSelector.DEFAULT; + } + return (mimeType, requiresSecureDecoder, requiresTunnelingDecoder) -> { + List defaultList = + MediaCodecSelector.DEFAULT.getDecoderInfos( + mimeType, requiresSecureDecoder, requiresTunnelingDecoder); + // Put the forced decoder first, keep others as fallback + List reordered = new ArrayList<>(); + for (androidx.media3.exoplayer.mediacodec.MediaCodecInfo info : defaultList) { + if (info.name.equals(forcedDecoderName)) { + reordered.add(0, info); + } else { + reordered.add(info); + } + } + return Collections.unmodifiableList(reordered); + }; + } + + public void notifyPipStateChanged(boolean isInPipMode, boolean wasDismissed, int widthDp, int heightDp) { + videoPlayerEvents.onPipStateChanged(isInPipMode, wasDismissed, widthDp, heightDp); + } + public void dispose() { if (disposeHandler != null) { disposeHandler.onDispose(); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerActivity.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerActivity.java new file mode 100644 index 000000000000..8e486931d52c --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerActivity.java @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayer; + +import android.content.res.Configuration; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.videoplayer.pip.PipCallbackHelper; + +/** + * Convenience Activity that bridges PiP callbacks to the video player plugin. + * + *

Extend this instead of {@link FlutterActivity} to get automatic PiP state events. + * If you have a custom Activity, call + * {@link PipCallbackHelper#onPictureInPictureModeChanged(boolean, boolean, int, int)} + * from your own {@code onPictureInPictureModeChanged} override instead. + */ +public class VideoPlayerActivity extends FlutterActivity { + @Override + public void onPictureInPictureModeChanged(boolean isInPipMode, Configuration newConfig) { + super.onPictureInPictureModeChanged(isInPipMode, newConfig); + PipCallbackHelper.onPictureInPictureModeChanged( + isInPipMode, isFinishing(), newConfig.screenWidthDp, newConfig.screenHeightDp); + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java index 4cac902319ec..2fa425501124 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java @@ -26,4 +26,10 @@ public interface VideoPlayerCallbacks { void onIsPlayingStateUpdate(boolean isPlaying); void onAudioTrackChanged(@Nullable String selectedTrackId); + + void onPipStateChanged(boolean isInPipMode, boolean wasDismissed, int widthDp, int heightDp); + + void onVideoQualityChanged(int width, int height, int bitrate, @Nullable String codec); + + void onDecoderChanged(@NonNull String decoderName, boolean isHardwareAccelerated); } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java index a471ec960e63..e4018348d8fd 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java @@ -68,4 +68,19 @@ public void onIsPlayingStateUpdate(boolean isPlaying) { public void onAudioTrackChanged(@Nullable String selectedTrackId) { eventSink.success(new AudioTrackChangedEvent(selectedTrackId)); } + + @Override + public void onPipStateChanged(boolean isInPipMode, boolean wasDismissed, int widthDp, int heightDp) { + eventSink.success(new PipStateEvent(isInPipMode, wasDismissed, (long) widthDp, (long) heightDp)); + } + + @Override + public void onVideoQualityChanged(int width, int height, int bitrate, @Nullable String codec) { + eventSink.success(new VideoQualityChangedEvent((long) width, (long) height, (long) bitrate, codec)); + } + + @Override + public void onDecoderChanged(@NonNull String decoderName, boolean isHardwareAccelerated) { + eventSink.success(new DecoderChangedEvent(decoderName, isHardwareAccelerated)); + } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java index 20f7c5d2dbab..76c13db1ba30 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java @@ -6,4 +6,6 @@ public class VideoPlayerOptions { public boolean mixWithOthers; + public int maxLoadRetries = 5; + public int maxPlayerRecoveryAttempts = 3; } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 49adaf4b7b33..ecb238ff3220 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -4,31 +4,60 @@ package io.flutter.plugins.videoplayer; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; import android.util.LongSparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaSessionService; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugins.videoplayer.pip.PipCallbackHelper; import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugins.videoplayer.pip.PipHandler; import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory; import io.flutter.plugins.videoplayer.platformview.PlatformViewVideoPlayer; +import io.flutter.plugins.videoplayer.service.PlaybackService; import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer; import io.flutter.view.TextureRegistry; +import java.util.HashSet; +import java.util.Set; /** Android platform implementation of the VideoPlayerPlugin. */ -public class VideoPlayerPlugin implements FlutterPlugin, AndroidVideoPlayerApi { +public class VideoPlayerPlugin implements FlutterPlugin, ActivityAware, AndroidVideoPlayerApi { private static final String TAG = "VideoPlayerPlugin"; private final LongSparseArray videoPlayers = new LongSparseArray<>(); private FlutterState flutterState; private final VideoPlayerOptions sharedOptions = new VideoPlayerOptions(); private long nextPlayerIdentifier = 1; + @NonNull private final PipHandler pipHandler = new PipHandler(null); + private final Set backgroundEnabledPlayers = new HashSet<>(); + @Nullable private ServiceConnection serviceConnection; + private boolean serviceBound = false; + @Nullable private ExoPlayer pendingServicePlayer; + @Nullable private PlatformMediaInfo pendingMediaInfo; + @Nullable private ActivityPluginBinding activityBinding; + private final PluginRegistry.UserLeaveHintListener onUserLeaveHintListener = + () -> pipHandler.onUserLeaveHint(); /** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */ - public VideoPlayerPlugin() {} + public VideoPlayerPlugin() { + pipHandler.setPipStateListener((isInPipMode, wasDismissed, widthDp, heightDp) -> { + for (int i = 0; i < videoPlayers.size(); i++) { + videoPlayers.valueAt(i).notifyPipStateChanged(isInPipMode, wasDismissed, widthDp, heightDp); + } + }); + } @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { @@ -56,14 +85,116 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { } flutterState.stopListening(binding.getBinaryMessenger()); flutterState = null; + PipCallbackHelper.setListener(null); onDestroy(); + VideoCacheManager.release(); + } + + // ActivityAware implementation + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + attachToActivity(binding); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + detachFromActivity(); + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + attachToActivity(binding); + } + + @Override + public void onDetachedFromActivity() { + detachFromActivity(); + } + + private void attachToActivity(@NonNull ActivityPluginBinding binding) { + activityBinding = binding; + pipHandler.setActivity(binding.getActivity()); + binding.addOnUserLeaveHintListener(onUserLeaveHintListener); + } + + private void detachFromActivity() { + if (activityBinding != null) { + activityBinding.removeOnUserLeaveHintListener(onUserLeaveHintListener); + activityBinding = null; + } + pipHandler.setActivity(null); } private void disposeAllPlayers() { + // Unbind (and release the MediaSession) BEFORE releasing any ExoPlayers. + // If the session is still alive when the player's thread is killed, queued + // media-button commands (play/pause from the notification) will try to post + // to a dead Handler, causing an ANR. + unbindPlaybackService(); + for (int i = 0; i < videoPlayers.size(); i++) { videoPlayers.valueAt(i).dispose(); } videoPlayers.clear(); + backgroundEnabledPlayers.clear(); + } + + private void bindPlaybackService() { + if (serviceBound || flutterState == null) return; + Context context = flutterState.applicationContext; + Intent intent = new Intent(context, PlaybackService.class); + intent.setAction(MediaSessionService.SERVICE_INTERFACE); + serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + // Pass the pending ExoPlayer to the service for MediaSession support. + if (pendingServicePlayer != null) { + PlaybackService service = PlaybackService.getInstance(); + if (service != null) { + Log.d(TAG, "Service connected, setting player on PlaybackService"); + service.setPlayer(pendingServicePlayer, + pendingMediaInfo != null ? pendingMediaInfo.getTitle() : null, + pendingMediaInfo != null ? pendingMediaInfo.getArtist() : null, + pendingMediaInfo != null ? pendingMediaInfo.getArtworkUrl() : null); + pendingServicePlayer = null; + pendingMediaInfo = null; + } else { + Log.w(TAG, "Service connected but getInstance() returned null"); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + serviceBound = false; + } + }; + // The service must be started (not just bound) to support foreground mode. + // A bound-only service cannot call startForeground(), so Media3 can't post + // the media notification. startService() starts it; Media3 then internally + // calls startForeground() when playback begins. + context.startService(intent); + context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + serviceBound = true; + } + + private void unbindPlaybackService() { + // Always release the MediaSession synchronously before stopping the + // service so that no queued media-button commands reach an + // already-released player. + PlaybackService service = PlaybackService.getInstance(); + if (service != null) { + service.releaseSession(); + } + pendingServicePlayer = null; + pendingMediaInfo = null; + if (!serviceBound || flutterState == null) return; + Context context = flutterState.applicationContext; + if (serviceConnection != null) { + context.unbindService(serviceConnection); + } + context.stopService(new Intent(context, PlaybackService.class)); + serviceBound = false; } public void onDestroy() { @@ -80,10 +211,25 @@ public void initialize() { disposeAllPlayers(); } + private VideoPlayerOptions playerOptionsFromCreationOptions(@NonNull CreationOptions options) { + VideoPlayerOptions playerOptions = new VideoPlayerOptions(); + playerOptions.mixWithOthers = sharedOptions.mixWithOthers; + Long maxLoadRetries = options.getMaxLoadRetries(); + if (maxLoadRetries != null) { + playerOptions.maxLoadRetries = maxLoadRetries.intValue(); + } + Long maxPlayerRecoveryAttempts = options.getMaxPlayerRecoveryAttempts(); + if (maxPlayerRecoveryAttempts != null) { + playerOptions.maxPlayerRecoveryAttempts = maxPlayerRecoveryAttempts.intValue(); + } + return playerOptions; + } + @OptIn(markerClass = UnstableApi.class) @Override public long createForPlatformView(@NonNull CreationOptions options) { final VideoAsset videoAsset = videoAssetWithOptions(options); + final VideoPlayerOptions playerOptions = playerOptionsFromCreationOptions(options); long id = nextPlayerIdentifier++; final String streamInstance = Long.toString(id); @@ -92,7 +238,7 @@ public long createForPlatformView(@NonNull CreationOptions options) { flutterState.applicationContext, VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance), videoAsset, - sharedOptions); + playerOptions); registerPlayerInstance(videoPlayer, id); return id; @@ -102,6 +248,7 @@ public long createForPlatformView(@NonNull CreationOptions options) { @Override public @NonNull TexturePlayerIds createForTextureView(@NonNull CreationOptions options) { final VideoAsset videoAsset = videoAssetWithOptions(options); + final VideoPlayerOptions playerOptions = playerOptionsFromCreationOptions(options); long id = nextPlayerIdentifier++; final String streamInstance = Long.toString(id); @@ -112,7 +259,7 @@ public long createForPlatformView(@NonNull CreationOptions options) { VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance), handle, videoAsset, - sharedOptions); + playerOptions); registerPlayerInstance(videoPlayer, id); return new TexturePlayerIds(id, handle.id()); @@ -146,13 +293,14 @@ public long createForPlatformView(@NonNull CreationOptions options) { } private void registerPlayerInstance(VideoPlayer player, long id) { - // Set up the instance-specific API handler, and make sure it is removed when the player is - // disposed. BinaryMessenger messenger = flutterState.binaryMessenger; final String channelSuffix = Long.toString(id); VideoPlayerInstanceApi.Companion.setUp(messenger, player, channelSuffix); player.setDisposeHandler( - () -> VideoPlayerInstanceApi.Companion.setUp(messenger, null, channelSuffix)); + () -> { + VideoPlayerInstanceApi.Companion.setUp(messenger, null, channelSuffix); + removeBackgroundPlayer(id); + }); videoPlayers.put(id, player); } @@ -160,8 +308,6 @@ private void registerPlayerInstance(VideoPlayer player, long id) { @NonNull private VideoPlayer getPlayer(long playerId) { VideoPlayer player = videoPlayers.get(playerId); - - // Avoid a very ugly un-debuggable NPE that results in returning a null player. if (player == null) { String message = "No player found with playerId <" + playerId + ">"; if (videoPlayers.size() == 0) { @@ -169,7 +315,6 @@ private VideoPlayer getPlayer(long playerId) { } throw new IllegalStateException(message); } - return player; } @@ -178,6 +323,7 @@ public void dispose(long playerId) { VideoPlayer player = getPlayer(playerId); player.dispose(); videoPlayers.remove(playerId); + removeBackgroundPlayer(playerId); } @Override @@ -192,6 +338,96 @@ public void setMixWithOthers(boolean mixWithOthers) { : flutterState.keyForAssetAndPackageName.get(asset, packageName); } + // Background playback methods + @Override + public void enableBackgroundPlayback(long playerId, @Nullable PlatformMediaInfo mediaInfo) { + VideoPlayer player = getPlayer(playerId); + ExoPlayer exoPlayer = player.getExoPlayer(); + backgroundEnabledPlayers.add(playerId); + + // Try to set the player on an already-running service first. + PlaybackService service = PlaybackService.getInstance(); + if (service != null) { + Log.d(TAG, "Service already running, setting player directly"); + service.setPlayer(exoPlayer, + mediaInfo != null ? mediaInfo.getTitle() : null, + mediaInfo != null ? mediaInfo.getArtist() : null, + mediaInfo != null ? mediaInfo.getArtworkUrl() : null); + } else { + // Store references so onServiceConnected can pass them to the service. + pendingServicePlayer = exoPlayer; + pendingMediaInfo = mediaInfo; + Log.d(TAG, "Service not running, starting and storing pending player"); + } + bindPlaybackService(); + } + + @Override + public void disableBackgroundPlayback(long playerId) { + removeBackgroundPlayer(playerId); + } + + private void removeBackgroundPlayer(long playerId) { + backgroundEnabledPlayers.remove(playerId); + if (backgroundEnabledPlayers.isEmpty()) { + unbindPlaybackService(); + } + } + + // PiP methods + @Override + public boolean isPipSupported() { + return pipHandler.isPipSupported(); + } + + @Override + public void enterPip(long playerId) { + pipHandler.enterPip(); + } + + @Override + public boolean isPipActive() { + return pipHandler.isPipActive(); + } + + @Override + public void setAutoEnterPip(boolean enabled) { + pipHandler.setAutoEnterPip(enabled); + } + + // Cache control methods + @OptIn(markerClass = UnstableApi.class) + @Override + public void setCacheMaxSize(long maxSizeBytes) { + VideoCacheManager.setMaxCacheSize(maxSizeBytes); + } + + @OptIn(markerClass = UnstableApi.class) + @Override + public void clearCache() { + if (flutterState != null) { + VideoCacheManager.clearCache(flutterState.applicationContext); + } + } + + @OptIn(markerClass = UnstableApi.class) + @Override + public long getCacheSize() { + return VideoCacheManager.getCacheSize(); + } + + @OptIn(markerClass = UnstableApi.class) + @Override + public boolean isCacheEnabled() { + return VideoCacheManager.isEnabled(); + } + + @OptIn(markerClass = UnstableApi.class) + @Override + public void setCacheEnabled(boolean enabled) { + VideoCacheManager.setEnabled(enabled); + } + private interface KeyForAssetFn { String get(String asset); } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/pip/PipCallbackHelper.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/pip/PipCallbackHelper.java new file mode 100644 index 000000000000..82e62f0444d2 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/pip/PipCallbackHelper.java @@ -0,0 +1,56 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayer.pip; + +import androidx.annotation.Nullable; + +/** + * Static bridge for PiP state change callbacks. + * + *

Activities call {@link #onPictureInPictureModeChanged(boolean, boolean)} from their + * {@code onPictureInPictureModeChanged} override. The plugin sets a listener via + * {@link #setListener(PipStateListener)} to receive the events. + * + *

Users who extend {@code VideoPlayerActivity} get this automatically. Users with custom + * Activities add one line in their {@code onPictureInPictureModeChanged}: + * {@code PipCallbackHelper.onPictureInPictureModeChanged(isInPipMode, isFinishing());} + */ +public class PipCallbackHelper { + /** Listener for PiP state changes. */ + public interface PipStateListener { + void onPipStateChanged(boolean isInPipMode, boolean wasDismissed, int widthDp, int heightDp); + } + + @Nullable private static PipStateListener listener; + private static boolean configured = false; + + /** Sets the listener that receives PiP state changes from the Activity. */ + public static void setListener(@Nullable PipStateListener l) { + listener = l; + configured = (l != null); + } + + /** + * Call from {@code Activity.onPictureInPictureModeChanged(boolean, Configuration)}. + * + * @param isInPipMode whether PiP mode is now active. + * @param isFinishing whether the Activity is finishing (user dismissed PiP via the X button). + * @param widthDp the window width in dp from the new Configuration. + * @param heightDp the window height in dp from the new Configuration. + */ + public static void onPictureInPictureModeChanged( + boolean isInPipMode, boolean isFinishing, int widthDp, int heightDp) { + configured = true; + if (listener != null) { + boolean wasDismissed = !isInPipMode && isFinishing; + listener.onPipStateChanged(isInPipMode, wasDismissed, widthDp, heightDp); + } + } + + /** Returns whether PiP callbacks have been configured (Activity is calling the helper). */ + public static boolean isConfigured() { + return configured; + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/pip/PipHandler.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/pip/PipHandler.java new file mode 100644 index 000000000000..f2518a2e2c35 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/pip/PipHandler.java @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayer.pip; + +import android.app.Activity; +import android.app.PictureInPictureParams; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; +import android.util.Rational; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class PipHandler { + private static final String TAG = "PipHandler"; + + @Nullable + private Activity activity; + private boolean autoEnterEnabled = false; + private Boolean pipSupportedCache = null; + + public PipHandler(@Nullable Activity activity) { + this.activity = activity; + } + + /** + * Sets the listener that receives PiP state changes. + * + *

Registers the listener with {@link PipCallbackHelper} so the Activity's + * {@code onPictureInPictureModeChanged} callback reaches the plugin. + */ + public void setPipStateListener(@NonNull PipCallbackHelper.PipStateListener listener) { + PipCallbackHelper.setListener(listener); + } + + /** Returns whether PiP callbacks have been configured by the Activity. */ + public boolean isPipCallbackConfigured() { + return PipCallbackHelper.isConfigured(); + } + + public void setActivity(@Nullable Activity activity) { + this.activity = activity; + // Invalidate cache when activity changes. + pipSupportedCache = null; + } + + public boolean isPipSupported() { + if (activity == null) return false; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false; + if (pipSupportedCache == null) { + pipSupportedCache = activity.getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); + } + return pipSupportedCache; + } + + public void enterPip() { + if (activity == null || !isPipSupported()) return; + if (!isPipCallbackConfigured()) { + Log.w(TAG, "PiP callbacks not configured. Extend VideoPlayerActivity or call " + + "PipCallbackHelper.onPictureInPictureModeChanged() from your Activity " + + "to receive PiP state events in Dart."); + } + try { + PictureInPictureParams.Builder builder = newPipParamsBuilder(); + activity.enterPictureInPictureMode(builder.build()); + } catch (IllegalStateException e) { + Log.w(TAG, "Failed to enter PiP: " + e.getMessage()); + } + } + + public boolean isPipActive() { + if (activity == null) return false; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false; + return activity.isInPictureInPictureMode(); + } + + public void setAutoEnterPip(boolean enabled) { + autoEnterEnabled = enabled; + if (activity == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return; + try { + PictureInPictureParams.Builder builder = newPipParamsBuilder(); + activity.setPictureInPictureParams(builder.build()); + } catch (IllegalStateException e) { + Log.w(TAG, "Failed to set auto-enter PiP: " + e.getMessage()); + } + } + + public boolean isAutoEnterEnabled() { + return autoEnterEnabled; + } + + public void onUserLeaveHint() { + if (autoEnterEnabled && isPipSupported()) { + enterPip(); + } + } + + private PictureInPictureParams.Builder newPipParamsBuilder() { + PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder() + .setAspectRatio(new Rational(16, 9)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setAutoEnterEnabled(autoEnterEnabled); + builder.setSeamlessResizeEnabled(true); + } + return builder; + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewExoPlayerEventListener.java index 699eab6eb0c1..d73eeeb3558a 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewExoPlayerEventListener.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewExoPlayerEventListener.java @@ -15,8 +15,10 @@ public final class PlatformViewExoPlayerEventListener extends ExoPlayerEventListener { public PlatformViewExoPlayerEventListener( - @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events) { - super(exoPlayer, events); + @NonNull ExoPlayer exoPlayer, + @NonNull VideoPlayerCallbacks events, + int maxPlayerRecoveryAttempts) { + super(exoPlayer, events, maxPlayerRecoveryAttempts); } @OptIn(markerClass = UnstableApi.class) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java index a7c079773b58..f1af503fbeb3 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java @@ -10,7 +10,9 @@ import androidx.annotation.VisibleForTesting; import androidx.media3.common.MediaItem; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; import io.flutter.plugins.videoplayer.ExoPlayerEventListener; import io.flutter.plugins.videoplayer.VideoAsset; import io.flutter.plugins.videoplayer.VideoPlayer; @@ -23,15 +25,23 @@ * displaying the video in the app. */ public class PlatformViewVideoPlayer extends VideoPlayer { + // Stored for ExoPlayer rebuild (decoder switching). + @NonNull private final Context context; + @NonNull private final VideoAsset asset; + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. @UnstableApi @VisibleForTesting public PlatformViewVideoPlayer( + @NonNull Context context, + @NonNull VideoAsset asset, @NonNull VideoPlayerCallbacks events, @NonNull MediaItem mediaItem, @NonNull VideoPlayerOptions options, @NonNull ExoPlayerProvider exoPlayerProvider) { super(events, mediaItem, options, /* surfaceProducer */ null, exoPlayerProvider); + this.context = context; + this.asset = asset; } /** @@ -52,24 +62,56 @@ public static PlatformViewVideoPlayer create( @NonNull VideoAsset asset, @NonNull VideoPlayerOptions options) { return new PlatformViewVideoPlayer( + context, + asset, events, asset.getMediaItem(), options, - () -> { - androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector = - new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context); - ExoPlayer.Builder builder = - new ExoPlayer.Builder(context) - .setTrackSelector(trackSelector) - .setMediaSourceFactory(asset.getMediaSourceFactory(context)); - return builder.build(); - }); + buildExoPlayerProvider(context, asset, options, null)); } @NonNull @Override protected ExoPlayerEventListener createExoPlayerEventListener( @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer) { - return new PlatformViewExoPlayerEventListener(exoPlayer, videoPlayerEvents); + return new PlatformViewExoPlayerEventListener(exoPlayer, videoPlayerEvents, + maxPlayerRecoveryAttempts); + } + + @UnstableApi + @NonNull + @Override + protected ExoPlayerProvider createExoPlayerProvider(@Nullable String forcedDecoderName) { + return buildExoPlayerProvider(context, asset, options, forcedDecoderName); + } + + @UnstableApi + @NonNull + private static ExoPlayerProvider buildExoPlayerProvider( + @NonNull Context context, + @NonNull VideoAsset asset, + @NonNull VideoPlayerOptions options, + @Nullable String forcedDecoderName) { + return () -> { + androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector = + new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context); + androidx.media3.exoplayer.source.MediaSource.Factory mediaSourceFactory = + asset.getMediaSourceFactory(context); + if (mediaSourceFactory + instanceof androidx.media3.exoplayer.source.DefaultMediaSourceFactory) { + ((androidx.media3.exoplayer.source.DefaultMediaSourceFactory) mediaSourceFactory) + .setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(options.maxLoadRetries)); + } + DefaultRenderersFactory renderersFactory = + new DefaultRenderersFactory(context) + .setEnableDecoderFallback(true) + .setMediaCodecSelector(createSelectorForDecoder(forcedDecoderName)); + ExoPlayer.Builder builder = + new ExoPlayer.Builder(context, renderersFactory) + .setTrackSelector(trackSelector) + .setMediaSourceFactory(mediaSourceFactory); + return builder.build(); + }; } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/service/PlaybackService.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/service/PlaybackService.java new file mode 100644 index 000000000000..9352e52e068a --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/service/PlaybackService.java @@ -0,0 +1,113 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayer.service; + +import android.content.Intent; +import android.net.Uri; +import android.os.IBinder; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.session.MediaSession; +import androidx.media3.session.MediaSessionService; + +public class PlaybackService extends MediaSessionService { + private static final String TAG = "PlaybackService"; + @Nullable private static PlaybackService instance; + private MediaSession mediaSession = null; + private ExoPlayer player = null; + + @Override + public void onCreate() { + super.onCreate(); + instance = this; + Log.d(TAG, "PlaybackService created"); + } + + @Nullable + public static PlaybackService getInstance() { + return instance; + } + + public void setPlayer(@NonNull ExoPlayer exoPlayer, + @Nullable String title, + @Nullable String artist, + @Nullable String artworkUrl) { + // Release any existing session before creating a new one. + if (mediaSession != null) { + mediaSession.release(); + } + this.player = exoPlayer; + + // Set media metadata (title, artist, artwork) on the current MediaItem so + // Media3's notification provider displays them. replaceMediaItem() updates + // metadata without interrupting playback. + if (exoPlayer.getMediaItemCount() > 0) { + MediaItem currentItem = exoPlayer.getCurrentMediaItem(); + if (currentItem != null) { + MediaMetadata.Builder metaBuilder = new MediaMetadata.Builder(); + if (title != null) metaBuilder.setTitle(title); + if (artist != null) metaBuilder.setArtist(artist); + if (artworkUrl != null) metaBuilder.setArtworkUri(Uri.parse(artworkUrl)); + MediaItem updated = currentItem.buildUpon() + .setMediaMetadata(metaBuilder.build()) + .build(); + exoPlayer.replaceMediaItem( + exoPlayer.getCurrentMediaItemIndex(), updated); + } + } + + mediaSession = new MediaSession.Builder(this, exoPlayer).build(); + // Explicitly add the session so MediaSessionService manages its notification. + // Without this, the session created after onCreate() is never discovered by + // Media3's internal notification manager (onGetSession is only called when a + // MediaController connects, which may never happen in our flow). + addSession(mediaSession); + Log.d(TAG, "MediaSession created and added, player isPlaying=" + exoPlayer.isPlaying() + + ", hasMediaItems=" + (exoPlayer.getMediaItemCount() > 0)); + } + + @Nullable + @Override + public MediaSession onGetSession(@NonNull MediaSession.ControllerInfo controllerInfo) { + return mediaSession; + } + + @Override + public void onTaskRemoved(@Nullable Intent rootIntent) { + MediaSession session = mediaSession; + if (session != null) { + if (session.getPlayer().getPlayWhenReady()) { + // Keep the service running if the player is playing + return; + } + } + stopSelf(); + } + + /** + * Synchronously releases the MediaSession so it can no longer forward + * commands to the player. Must be called before the ExoPlayer is released + * to avoid sending messages to a dead thread. + */ + public void releaseSession() { + if (mediaSession != null) { + removeSession(mediaSession); + mediaSession.release(); + mediaSession = null; + } + player = null; + } + + @Override + public void onDestroy() { + releaseSession(); + instance = null; + super.onDestroy(); + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureExoPlayerEventListener.java index bcc901b7218f..f40efb34b44c 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureExoPlayerEventListener.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureExoPlayerEventListener.java @@ -19,8 +19,9 @@ public final class TextureExoPlayerEventListener extends ExoPlayerEventListener public TextureExoPlayerEventListener( @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events, - boolean surfaceProducerHandlesCropAndRotation) { - super(exoPlayer, events); + boolean surfaceProducerHandlesCropAndRotation, + int maxPlayerRecoveryAttempts) { + super(exoPlayer, events, maxPlayerRecoveryAttempts); this.surfaceProducerHandlesCropAndRotation = surfaceProducerHandlesCropAndRotation; } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java index e482bdd85020..a36fa60d8acf 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java @@ -12,7 +12,9 @@ import androidx.annotation.VisibleForTesting; import androidx.media3.common.MediaItem; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; import io.flutter.plugins.videoplayer.ExoPlayerEventListener; import io.flutter.plugins.videoplayer.VideoAsset; import io.flutter.plugins.videoplayer.VideoPlayer; @@ -30,6 +32,11 @@ public final class TextureVideoPlayer extends VideoPlayer implements SurfaceProducer.Callback { // True when the ExoPlayer instance has a null surface. private boolean needsSurface = true; + + // Stored for ExoPlayer rebuild (decoder switching). + @NonNull private final Context context; + @NonNull private final VideoAsset asset; + /** * Creates a texture video player. * @@ -50,31 +57,29 @@ public static TextureVideoPlayer create( @NonNull VideoAsset asset, @NonNull VideoPlayerOptions options) { return new TextureVideoPlayer( + context, + asset, events, surfaceProducer, asset.getMediaItem(), options, - () -> { - androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector = - new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context); - ExoPlayer.Builder builder = - new ExoPlayer.Builder(context) - .setTrackSelector(trackSelector) - .setMediaSourceFactory(asset.getMediaSourceFactory(context)); - return builder.build(); - }); + buildExoPlayerProvider(context, asset, options, null)); } // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. @UnstableApi @VisibleForTesting public TextureVideoPlayer( + @NonNull Context context, + @NonNull VideoAsset asset, @NonNull VideoPlayerCallbacks events, @NonNull SurfaceProducer surfaceProducer, @NonNull MediaItem mediaItem, @NonNull VideoPlayerOptions options, @NonNull ExoPlayerProvider exoPlayerProvider) { super(events, mediaItem, options, surfaceProducer, exoPlayerProvider); + this.context = context; + this.asset = asset; surfaceProducer.setCallback(this); @@ -93,7 +98,54 @@ protected ExoPlayerEventListener createExoPlayerEventListener( } boolean surfaceProducerHandlesCropAndRotation = surfaceProducer.handlesCropAndRotation(); return new TextureExoPlayerEventListener( - exoPlayer, videoPlayerEvents, surfaceProducerHandlesCropAndRotation); + exoPlayer, videoPlayerEvents, surfaceProducerHandlesCropAndRotation, + maxPlayerRecoveryAttempts); + } + + @UnstableApi + @NonNull + @Override + protected ExoPlayerProvider createExoPlayerProvider(@Nullable String forcedDecoderName) { + return buildExoPlayerProvider(context, asset, options, forcedDecoderName); + } + + @Override + protected void onPlayerRebuilt(@NonNull ExoPlayer newPlayer) { + // Re-attach surface after ExoPlayer rebuild. + assert surfaceProducer != null; + Surface surface = surfaceProducer.getSurface(); + newPlayer.setVideoSurface(surface); + needsSurface = surface == null; + } + + @UnstableApi + @NonNull + private static ExoPlayerProvider buildExoPlayerProvider( + @NonNull Context context, + @NonNull VideoAsset asset, + @NonNull VideoPlayerOptions options, + @Nullable String forcedDecoderName) { + return () -> { + androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector = + new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context); + androidx.media3.exoplayer.source.MediaSource.Factory mediaSourceFactory = + asset.getMediaSourceFactory(context); + if (mediaSourceFactory + instanceof androidx.media3.exoplayer.source.DefaultMediaSourceFactory) { + ((androidx.media3.exoplayer.source.DefaultMediaSourceFactory) mediaSourceFactory) + .setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(options.maxLoadRetries)); + } + DefaultRenderersFactory renderersFactory = + new DefaultRenderersFactory(context) + .setEnableDecoderFallback(true) + .setMediaCodecSelector(createSelectorForDecoder(forcedDecoderName)); + ExoPlayer.Builder builder = + new ExoPlayer.Builder(context, renderersFactory) + .setTrackSelector(trackSelector) + .setMediaSourceFactory(mediaSourceFactory); + return builder.build(); + }; } @RestrictTo(RestrictTo.Scope.LIBRARY) diff --git a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt index e546c744e561..b9815c725d99 100644 --- a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt +++ b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.5), do not edit directly. +// Autogenerated from Pigeon (v26.1.10), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -12,11 +12,10 @@ import io.flutter.plugin.common.BasicMessageChannel import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMessageCodec import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec import java.io.ByteArrayOutputStream import java.nio.ByteBuffer - private object MessagesPigeonUtils { fun wrapResult(result: Any?): List { @@ -25,53 +24,61 @@ private object MessagesPigeonUtils { fun wrapError(exception: Throwable): List { return if (exception is FlutterError) { - listOf(exception.code, exception.message, exception.details) + listOf( + exception.code, + exception.message, + exception.details + ) } else { listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)) + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) } } - fun deepEquals(a: Any?, b: Any?): Boolean { if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is IntArray && b is IntArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is LongArray && b is LongArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is DoubleArray && b is DoubleArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is Array<*> && b is Array<*>) { - return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } } if (a is List<*> && b is List<*>) { - return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } } if (a is Map<*, *> && b is Map<*, *>) { - return a.size == b.size && - a.all { (b as Map).contains(it.key) && deepEquals(it.value, b[it.key]) } + return a.size == b.size && a.all { + (b as Map).contains(it.key) && + deepEquals(it.value, b[it.key]) + } } return a == b } + } /** * Error class for passing custom error details to Flutter via a thrown PlatformException. - * * @property code The error code. * @property message The error message. * @property details The error details. Must be a datatype supported by the api codec. */ -class FlutterError( - val code: String, - override val message: String? = null, - val details: Any? = null +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null ) : Throwable() /** Pigeon equivalent of video_platform_interface's VideoFormat. */ @@ -106,25 +113,26 @@ enum class PlatformPlaybackState(val raw: Int) { } /** - * Generated class from Pigeon that represents data sent in messages. This class should not be - * extended by any user class outside of the generated file. + * Generated class from Pigeon that represents data sent in messages. + * This class should not be extended by any user class outside of the generated file. */ -sealed class PlatformVideoEvent +sealed class PlatformVideoEvent /** * Sent when the video is initialized and ready to play. * * Generated class from Pigeon that represents data sent in messages. */ -data class InitializationEvent( - /** The video duration in milliseconds. */ - val duration: Long, - /** The width of the video in pixels. */ - val width: Long, - /** The height of the video in pixels. */ - val height: Long, - /** The rotation that should be applied during playback. */ - val rotationCorrection: Long -) : PlatformVideoEvent() { +data class InitializationEvent ( + /** The video duration in milliseconds. */ + val duration: Long, + /** The width of the video in pixels. */ + val width: Long, + /** The height of the video in pixels. */ + val height: Long, + /** The rotation that should be applied during playback. */ + val rotationCorrection: Long +) : PlatformVideoEvent() + { companion object { fun fromList(pigeonVar_list: List): InitializationEvent { val duration = pigeonVar_list[0] as Long @@ -134,16 +142,14 @@ data class InitializationEvent( return InitializationEvent(duration, width, height, rotationCorrection) } } - fun toList(): List { return listOf( - duration, - width, - height, - rotationCorrection, + duration, + width, + height, + rotationCorrection, ) } - override fun equals(other: Any?): Boolean { if (other !is InitializationEvent) { return false @@ -151,8 +157,7 @@ data class InitializationEvent( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -164,20 +169,21 @@ data class InitializationEvent( * * Generated class from Pigeon that represents data sent in messages. */ -data class PlaybackStateChangeEvent(val state: PlatformPlaybackState) : PlatformVideoEvent() { +data class PlaybackStateChangeEvent ( + val state: PlatformPlaybackState +) : PlatformVideoEvent() + { companion object { fun fromList(pigeonVar_list: List): PlaybackStateChangeEvent { val state = pigeonVar_list[0] as PlatformPlaybackState return PlaybackStateChangeEvent(state) } } - fun toList(): List { return listOf( - state, + state, ) } - override fun equals(other: Any?): Boolean { if (other !is PlaybackStateChangeEvent) { return false @@ -185,8 +191,7 @@ data class PlaybackStateChangeEvent(val state: PlatformPlaybackState) : Platform if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -198,20 +203,21 @@ data class PlaybackStateChangeEvent(val state: PlatformPlaybackState) : Platform * * Generated class from Pigeon that represents data sent in messages. */ -data class IsPlayingStateEvent(val isPlaying: Boolean) : PlatformVideoEvent() { +data class IsPlayingStateEvent ( + val isPlaying: Boolean +) : PlatformVideoEvent() + { companion object { fun fromList(pigeonVar_list: List): IsPlayingStateEvent { val isPlaying = pigeonVar_list[0] as Boolean return IsPlayingStateEvent(isPlaying) } } - fun toList(): List { return listOf( - isPlaying, + isPlaying, ) } - override fun equals(other: Any?): Boolean { if (other !is IsPlayingStateEvent) { return false @@ -219,8 +225,7 @@ data class IsPlayingStateEvent(val isPlaying: Boolean) : PlatformVideoEvent() { if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -228,28 +233,27 @@ data class IsPlayingStateEvent(val isPlaying: Boolean) : PlatformVideoEvent() { /** * Sent when audio tracks change. * - * This includes when the selected audio track changes after calling selectAudioTrack. Corresponds - * to ExoPlayer's onTracksChanged. + * This includes when the selected audio track changes after calling selectAudioTrack. + * Corresponds to ExoPlayer's onTracksChanged. * * Generated class from Pigeon that represents data sent in messages. */ -data class AudioTrackChangedEvent( - /** The ID of the newly selected audio track, if any. */ - val selectedTrackId: String? = null -) : PlatformVideoEvent() { +data class AudioTrackChangedEvent ( + /** The ID of the newly selected audio track, if any. */ + val selectedTrackId: String? = null +) : PlatformVideoEvent() + { companion object { fun fromList(pigeonVar_list: List): AudioTrackChangedEvent { val selectedTrackId = pigeonVar_list[0] as String? return AudioTrackChangedEvent(selectedTrackId) } } - fun toList(): List { return listOf( - selectedTrackId, + selectedTrackId, ) } - override fun equals(other: Any?): Boolean { if (other !is AudioTrackChangedEvent) { return false @@ -257,8 +261,97 @@ data class AudioTrackChangedEvent( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Sent when the video quality changes (ABR switch). + * + * Corresponds to ExoPlayer's AnalyticsListener.onDownstreamFormatChanged. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class VideoQualityChangedEvent ( + val width: Long, + val height: Long, + val bitrate: Long, + val codec: String? = null +) : PlatformVideoEvent() + { + companion object { + fun fromList(pigeonVar_list: List): VideoQualityChangedEvent { + val width = pigeonVar_list[0] as Long + val height = pigeonVar_list[1] as Long + val bitrate = pigeonVar_list[2] as Long + val codec = pigeonVar_list[3] as String? + return VideoQualityChangedEvent(width, height, bitrate, codec) + } + } + fun toList(): List { + return listOf( + width, + height, + bitrate, + codec, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is VideoQualityChangedEvent) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Sent when PiP state changes. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class PipStateEvent ( + val isInPipMode: Boolean, + /** + * Whether PiP was dismissed by the user (X button) as opposed to + * expanded back to full screen. Only meaningful when [isInPipMode] is false. + */ + val wasDismissed: Boolean, + /** The window width in dp at the time of the PiP state change. */ + val windowWidth: Long, + /** The window height in dp at the time of the PiP state change. */ + val windowHeight: Long +) : PlatformVideoEvent() + { + companion object { + fun fromList(pigeonVar_list: List): PipStateEvent { + val isInPipMode = pigeonVar_list[0] as Boolean + val wasDismissed = pigeonVar_list[1] as Boolean + val windowWidth = pigeonVar_list[2] as Long + val windowHeight = pigeonVar_list[3] as Long + return PipStateEvent(isInPipMode, wasDismissed, windowWidth, windowHeight) + } } + fun toList(): List { + return listOf( + isInPipMode, + wasDismissed, + windowWidth, + windowHeight, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is PipStateEvent) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -268,20 +361,21 @@ data class AudioTrackChangedEvent( * * Generated class from Pigeon that represents data sent in messages. */ -data class PlatformVideoViewCreationParams(val playerId: Long) { +data class PlatformVideoViewCreationParams ( + val playerId: Long +) + { companion object { fun fromList(pigeonVar_list: List): PlatformVideoViewCreationParams { val playerId = pigeonVar_list[0] as Long return PlatformVideoViewCreationParams(playerId) } } - fun toList(): List { return listOf( - playerId, + playerId, ) } - override fun equals(other: Any?): Boolean { if (other !is PlatformVideoViewCreationParams) { return false @@ -289,38 +383,50 @@ data class PlatformVideoViewCreationParams(val playerId: Long) { if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ -data class CreationOptions( - val uri: String, - val formatHint: PlatformVideoFormat? = null, - val httpHeaders: Map, - val userAgent: String? = null -) { +data class CreationOptions ( + val uri: String, + val formatHint: PlatformVideoFormat? = null, + val httpHeaders: Map, + val userAgent: String? = null, + /** + * Max retries per segment/load error before escalating. + * Null means use ExoPlayer's default (5). + */ + val maxLoadRetries: Long? = null, + /** + * Max player-level recovery attempts for fatal network errors. + * Null means use the default (3). + */ + val maxPlayerRecoveryAttempts: Long? = null +) + { companion object { fun fromList(pigeonVar_list: List): CreationOptions { val uri = pigeonVar_list[0] as String val formatHint = pigeonVar_list[1] as PlatformVideoFormat? val httpHeaders = pigeonVar_list[2] as Map val userAgent = pigeonVar_list[3] as String? - return CreationOptions(uri, formatHint, httpHeaders, userAgent) + val maxLoadRetries = pigeonVar_list[4] as Long? + val maxPlayerRecoveryAttempts = pigeonVar_list[5] as Long? + return CreationOptions(uri, formatHint, httpHeaders, userAgent, maxLoadRetries, maxPlayerRecoveryAttempts) } } - fun toList(): List { return listOf( - uri, - formatHint, - httpHeaders, - userAgent, + uri, + formatHint, + httpHeaders, + userAgent, + maxLoadRetries, + maxPlayerRecoveryAttempts, ) } - override fun equals(other: Any?): Boolean { if (other !is CreationOptions) { return false @@ -328,14 +434,17 @@ data class CreationOptions( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ -data class TexturePlayerIds(val playerId: Long, val textureId: Long) { +data class TexturePlayerIds ( + val playerId: Long, + val textureId: Long +) + { companion object { fun fromList(pigeonVar_list: List): TexturePlayerIds { val playerId = pigeonVar_list[0] as Long @@ -343,14 +452,12 @@ data class TexturePlayerIds(val playerId: Long, val textureId: Long) { return TexturePlayerIds(playerId, textureId) } } - fun toList(): List { return listOf( - playerId, - textureId, + playerId, + textureId, ) } - override fun equals(other: Any?): Boolean { if (other !is TexturePlayerIds) { return false @@ -358,19 +465,19 @@ data class TexturePlayerIds(val playerId: Long, val textureId: Long) { if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ -data class PlaybackState( - /** The current playback position, in milliseconds. */ - val playPosition: Long, - /** The current buffer position, in milliseconds. */ - val bufferPosition: Long -) { +data class PlaybackState ( + /** The current playback position, in milliseconds. */ + val playPosition: Long, + /** The current buffer position, in milliseconds. */ + val bufferPosition: Long +) + { companion object { fun fromList(pigeonVar_list: List): PlaybackState { val playPosition = pigeonVar_list[0] as Long @@ -378,14 +485,12 @@ data class PlaybackState( return PlaybackState(playPosition, bufferPosition) } } - fun toList(): List { return listOf( - playPosition, - bufferPosition, + playPosition, + bufferPosition, ) } - override fun equals(other: Any?): Boolean { if (other !is PlaybackState) { return false @@ -393,8 +498,7 @@ data class PlaybackState( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -404,16 +508,17 @@ data class PlaybackState( * * Generated class from Pigeon that represents data sent in messages. */ -data class AudioTrackMessage( - val id: String, - val label: String, - val language: String, - val isSelected: Boolean, - val bitrate: Long? = null, - val sampleRate: Long? = null, - val channelCount: Long? = null, - val codec: String? = null -) { +data class AudioTrackMessage ( + val id: String, + val label: String, + val language: String, + val isSelected: Boolean, + val bitrate: Long? = null, + val sampleRate: Long? = null, + val channelCount: Long? = null, + val codec: String? = null +) + { companion object { fun fromList(pigeonVar_list: List): AudioTrackMessage { val id = pigeonVar_list[0] as String @@ -424,24 +529,21 @@ data class AudioTrackMessage( val sampleRate = pigeonVar_list[5] as Long? val channelCount = pigeonVar_list[6] as Long? val codec = pigeonVar_list[7] as String? - return AudioTrackMessage( - id, label, language, isSelected, bitrate, sampleRate, channelCount, codec) + return AudioTrackMessage(id, label, language, isSelected, bitrate, sampleRate, channelCount, codec) } } - fun toList(): List { return listOf( - id, - label, - language, - isSelected, - bitrate, - sampleRate, - channelCount, - codec, + id, + label, + language, + isSelected, + bitrate, + sampleRate, + channelCount, + codec, ) } - override fun equals(other: Any?): Boolean { if (other !is AudioTrackMessage) { return false @@ -449,8 +551,7 @@ data class AudioTrackMessage( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -460,17 +561,18 @@ data class AudioTrackMessage( * * Generated class from Pigeon that represents data sent in messages. */ -data class ExoPlayerAudioTrackData( - val groupIndex: Long, - val trackIndex: Long, - val label: String? = null, - val language: String? = null, - val isSelected: Boolean, - val bitrate: Long? = null, - val sampleRate: Long? = null, - val channelCount: Long? = null, - val codec: String? = null -) { +data class ExoPlayerAudioTrackData ( + val groupIndex: Long, + val trackIndex: Long, + val label: String? = null, + val language: String? = null, + val isSelected: Boolean, + val bitrate: Long? = null, + val sampleRate: Long? = null, + val channelCount: Long? = null, + val codec: String? = null +) + { companion object { fun fromList(pigeonVar_list: List): ExoPlayerAudioTrackData { val groupIndex = pigeonVar_list[0] as Long @@ -482,33 +584,22 @@ data class ExoPlayerAudioTrackData( val sampleRate = pigeonVar_list[6] as Long? val channelCount = pigeonVar_list[7] as Long? val codec = pigeonVar_list[8] as String? - return ExoPlayerAudioTrackData( - groupIndex, - trackIndex, - label, - language, - isSelected, - bitrate, - sampleRate, - channelCount, - codec) + return ExoPlayerAudioTrackData(groupIndex, trackIndex, label, language, isSelected, bitrate, sampleRate, channelCount, codec) } } - fun toList(): List { return listOf( - groupIndex, - trackIndex, - label, - language, - isSelected, - bitrate, - sampleRate, - channelCount, - codec, + groupIndex, + trackIndex, + label, + language, + isSelected, + bitrate, + sampleRate, + channelCount, + codec, ) } - override fun equals(other: Any?): Boolean { if (other !is ExoPlayerAudioTrackData) { return false @@ -516,8 +607,7 @@ data class ExoPlayerAudioTrackData( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -527,85 +617,297 @@ data class ExoPlayerAudioTrackData( * * Generated class from Pigeon that represents data sent in messages. */ -data class NativeAudioTrackData( - /** ExoPlayer-based tracks */ - val exoPlayerTracks: List? = null -) { +data class NativeAudioTrackData ( + /** ExoPlayer-based tracks */ + val exoPlayerTracks: List? = null +) + { companion object { fun fromList(pigeonVar_list: List): NativeAudioTrackData { val exoPlayerTracks = pigeonVar_list[0] as List? return NativeAudioTrackData(exoPlayerTracks) } } + fun toList(): List { + return listOf( + exoPlayerTracks, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is NativeAudioTrackData) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class PlatformMediaInfo ( + val title: String, + val artist: String? = null, + val artworkUrl: String? = null, + val durationMs: Long? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): PlatformMediaInfo { + val title = pigeonVar_list[0] as String + val artist = pigeonVar_list[1] as String? + val artworkUrl = pigeonVar_list[2] as String? + val durationMs = pigeonVar_list[3] as Long? + return PlatformMediaInfo(title, artist, artworkUrl, durationMs) + } + } fun toList(): List { return listOf( - exoPlayerTracks, + title, + artist, + artworkUrl, + durationMs, ) } + override fun equals(other: Any?): Boolean { + if (other !is PlatformMediaInfo) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Sent when the active video decoder changes. + * + * Corresponds to ExoPlayer's AnalyticsListener.onVideoDecoderInitialized. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class DecoderChangedEvent ( + val decoderName: String, + val isHardwareAccelerated: Boolean +) : PlatformVideoEvent() + { + companion object { + fun fromList(pigeonVar_list: List): DecoderChangedEvent { + val decoderName = pigeonVar_list[0] as String + val isHardwareAccelerated = pigeonVar_list[1] as Boolean + return DecoderChangedEvent(decoderName, isHardwareAccelerated) + } + } + fun toList(): List { + return listOf( + decoderName, + isHardwareAccelerated, + ) + } override fun equals(other: Any?): Boolean { - if (other !is NativeAudioTrackData) { + if (other !is DecoderChangedEvent) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Describes a video decoder available on the device. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class PlatformVideoDecoder ( + val name: String, + val mimeType: String, + val isHardwareAccelerated: Boolean, + val isSoftwareOnly: Boolean, + val isSelected: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): PlatformVideoDecoder { + val name = pigeonVar_list[0] as String + val mimeType = pigeonVar_list[1] as String + val isHardwareAccelerated = pigeonVar_list[2] as Boolean + val isSoftwareOnly = pigeonVar_list[3] as Boolean + val isSelected = pigeonVar_list[4] as Boolean + return PlatformVideoDecoder(name, mimeType, isHardwareAccelerated, isSoftwareOnly, isSelected) + } + } + fun toList(): List { + return listOf( + name, + mimeType, + isHardwareAccelerated, + isSoftwareOnly, + isSelected, + ) } + override fun equals(other: Any?): Boolean { + if (other !is PlatformVideoDecoder) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } +/** + * Represents a video quality variant (resolution/bitrate combination). + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class PlatformVideoQuality ( + val width: Long, + val height: Long, + val bitrate: Long, + val codec: String? = null, + val isSelected: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): PlatformVideoQuality { + val width = pigeonVar_list[0] as Long + val height = pigeonVar_list[1] as Long + val bitrate = pigeonVar_list[2] as Long + val codec = pigeonVar_list[3] as String? + val isSelected = pigeonVar_list[4] as Boolean + return PlatformVideoQuality(width, height, bitrate, codec, isSelected) + } + } + fun toList(): List { + return listOf( + width, + height, + bitrate, + codec, + isSelected, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is PlatformVideoQuality) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} private open class MessagesPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { 129.toByte() -> { - return (readValue(buffer) as Long?)?.let { PlatformVideoFormat.ofRaw(it.toInt()) } + return (readValue(buffer) as Long?)?.let { + PlatformVideoFormat.ofRaw(it.toInt()) + } } 130.toByte() -> { - return (readValue(buffer) as Long?)?.let { PlatformPlaybackState.ofRaw(it.toInt()) } + return (readValue(buffer) as Long?)?.let { + PlatformPlaybackState.ofRaw(it.toInt()) + } } 131.toByte() -> { - return (readValue(buffer) as? List)?.let { InitializationEvent.fromList(it) } + return (readValue(buffer) as? List)?.let { + InitializationEvent.fromList(it) + } } 132.toByte() -> { - return (readValue(buffer) as? List)?.let { PlaybackStateChangeEvent.fromList(it) } + return (readValue(buffer) as? List)?.let { + PlaybackStateChangeEvent.fromList(it) + } } 133.toByte() -> { - return (readValue(buffer) as? List)?.let { IsPlayingStateEvent.fromList(it) } + return (readValue(buffer) as? List)?.let { + IsPlayingStateEvent.fromList(it) + } } 134.toByte() -> { - return (readValue(buffer) as? List)?.let { AudioTrackChangedEvent.fromList(it) } + return (readValue(buffer) as? List)?.let { + AudioTrackChangedEvent.fromList(it) + } } 135.toByte() -> { return (readValue(buffer) as? List)?.let { - PlatformVideoViewCreationParams.fromList(it) + VideoQualityChangedEvent.fromList(it) } } 136.toByte() -> { - return (readValue(buffer) as? List)?.let { CreationOptions.fromList(it) } + return (readValue(buffer) as? List)?.let { + PipStateEvent.fromList(it) + } } 137.toByte() -> { - return (readValue(buffer) as? List)?.let { TexturePlayerIds.fromList(it) } + return (readValue(buffer) as? List)?.let { + PlatformVideoViewCreationParams.fromList(it) + } } 138.toByte() -> { - return (readValue(buffer) as? List)?.let { PlaybackState.fromList(it) } + return (readValue(buffer) as? List)?.let { + CreationOptions.fromList(it) + } } 139.toByte() -> { - return (readValue(buffer) as? List)?.let { AudioTrackMessage.fromList(it) } + return (readValue(buffer) as? List)?.let { + TexturePlayerIds.fromList(it) + } } 140.toByte() -> { - return (readValue(buffer) as? List)?.let { ExoPlayerAudioTrackData.fromList(it) } + return (readValue(buffer) as? List)?.let { + PlaybackState.fromList(it) + } } 141.toByte() -> { - return (readValue(buffer) as? List)?.let { NativeAudioTrackData.fromList(it) } + return (readValue(buffer) as? List)?.let { + AudioTrackMessage.fromList(it) + } + } + 142.toByte() -> { + return (readValue(buffer) as? List)?.let { + ExoPlayerAudioTrackData.fromList(it) + } + } + 143.toByte() -> { + return (readValue(buffer) as? List)?.let { + NativeAudioTrackData.fromList(it) + } + } + 144.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformMediaInfo.fromList(it) + } + } + 145.toByte() -> { + return (readValue(buffer) as? List)?.let { + DecoderChangedEvent.fromList(it) + } + } + 146.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformVideoDecoder.fromList(it) + } + } + 147.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformVideoQuality.fromList(it) + } } else -> super.readValueOfType(type, buffer) } } - - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { when (value) { is PlatformVideoFormat -> { stream.write(129) @@ -631,34 +933,58 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { stream.write(134) writeValue(stream, value.toList()) } - is PlatformVideoViewCreationParams -> { + is VideoQualityChangedEvent -> { stream.write(135) writeValue(stream, value.toList()) } - is CreationOptions -> { + is PipStateEvent -> { stream.write(136) writeValue(stream, value.toList()) } - is TexturePlayerIds -> { + is PlatformVideoViewCreationParams -> { stream.write(137) writeValue(stream, value.toList()) } - is PlaybackState -> { + is CreationOptions -> { stream.write(138) writeValue(stream, value.toList()) } - is AudioTrackMessage -> { + is TexturePlayerIds -> { stream.write(139) writeValue(stream, value.toList()) } - is ExoPlayerAudioTrackData -> { + is PlaybackState -> { stream.write(140) writeValue(stream, value.toList()) } - is NativeAudioTrackData -> { + is AudioTrackMessage -> { stream.write(141) writeValue(stream, value.toList()) } + is ExoPlayerAudioTrackData -> { + stream.write(142) + writeValue(stream, value.toList()) + } + is NativeAudioTrackData -> { + stream.write(143) + writeValue(stream, value.toList()) + } + is PlatformMediaInfo -> { + stream.write(144) + writeValue(stream, value.toList()) + } + is DecoderChangedEvent -> { + stream.write(145) + writeValue(stream, value.toList()) + } + is PlatformVideoDecoder -> { + stream.write(146) + writeValue(stream, value.toList()) + } + is PlatformVideoQuality -> { + stream.write(147) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -669,47 +995,42 @@ val MessagesPigeonMethodCodec = StandardMethodCodec(MessagesPigeonCodec()) /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface AndroidVideoPlayerApi { fun initialize() - fun createForPlatformView(options: CreationOptions): Long - fun createForTextureView(options: CreationOptions): TexturePlayerIds - fun dispose(playerId: Long) - fun setMixWithOthers(mixWithOthers: Boolean) - fun getLookupKeyForAsset(asset: String, packageName: String?): String + fun enableBackgroundPlayback(playerId: Long, mediaInfo: PlatformMediaInfo?) + fun disableBackgroundPlayback(playerId: Long) + fun isPipSupported(): Boolean + fun enterPip(playerId: Long) + fun isPipActive(): Boolean + fun setAutoEnterPip(enabled: Boolean) + fun setCacheMaxSize(maxSizeBytes: Long) + fun clearCache() + fun getCacheSize(): Long + fun isCacheEnabled(): Boolean + fun setCacheEnabled(enabled: Boolean) companion object { /** The codec used by AndroidVideoPlayerApi. */ - val codec: MessageCodec by lazy { MessagesPigeonCodec() } - /** - * Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the - * `binaryMessenger`. - */ + val codec: MessageCodec by lazy { + MessagesPigeonCodec() + } + /** Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the `binaryMessenger`. */ @JvmOverloads - fun setUp( - binaryMessenger: BinaryMessenger, - api: AndroidVideoPlayerApi?, - messageChannelSuffix: String = "" - ) { - val separatedMessageChannelSuffix = - if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + fun setUp(binaryMessenger: BinaryMessenger, api: AndroidVideoPlayerApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - api.initialize() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.initialize() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -717,21 +1038,16 @@ interface AndroidVideoPlayerApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val optionsArg = args[0] as CreationOptions - val wrapped: List = - try { - listOf(api.createForPlatformView(optionsArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.createForPlatformView(optionsArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -739,21 +1055,16 @@ interface AndroidVideoPlayerApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val optionsArg = args[0] as CreationOptions - val wrapped: List = - try { - listOf(api.createForTextureView(optionsArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.createForTextureView(optionsArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -761,22 +1072,17 @@ interface AndroidVideoPlayerApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val playerIdArg = args[0] as Long - val wrapped: List = - try { - api.dispose(playerIdArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.dispose(playerIdArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -784,22 +1090,17 @@ interface AndroidVideoPlayerApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val mixWithOthersArg = args[0] as Boolean - val wrapped: List = - try { - api.setMixWithOthers(mixWithOthersArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.setMixWithOthers(mixWithOthersArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -807,22 +1108,202 @@ interface AndroidVideoPlayerApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val assetArg = args[0] as String val packageNameArg = args[1] as String? - val wrapped: List = - try { - listOf(api.getLookupKeyForAsset(assetArg, packageNameArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.getLookupKeyForAsset(assetArg, packageNameArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.enableBackgroundPlayback$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val playerIdArg = args[0] as Long + val mediaInfoArg = args[1] as PlatformMediaInfo? + val wrapped: List = try { + api.enableBackgroundPlayback(playerIdArg, mediaInfoArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.disableBackgroundPlayback$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val playerIdArg = args[0] as Long + val wrapped: List = try { + api.disableBackgroundPlayback(playerIdArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.isPipSupported$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.isPipSupported()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.enterPip$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val playerIdArg = args[0] as Long + val wrapped: List = try { + api.enterPip(playerIdArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.isPipActive$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.isPipActive()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setAutoEnterPip$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val enabledArg = args[0] as Boolean + val wrapped: List = try { + api.setAutoEnterPip(enabledArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setCacheMaxSize$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val maxSizeBytesArg = args[0] as Long + val wrapped: List = try { + api.setCacheMaxSize(maxSizeBytesArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.clearCache$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.clearCache() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getCacheSize$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getCacheSize()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.isCacheEnabled$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.isCacheEnabled()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setCacheEnabled$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val enabledArg = args[0] as Boolean + val wrapped: List = try { + api.setCacheEnabled(enabledArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -854,39 +1335,42 @@ interface VideoPlayerInstanceApi { fun getAudioTracks(): NativeAudioTrackData /** Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] */ fun selectAudioTrack(groupIndex: Long, trackIndex: Long) + /** Returns the available video quality variants. */ + fun getAvailableQualities(): List + /** Returns the currently playing video quality, or null if unknown. */ + fun getCurrentQuality(): PlatformVideoQuality? + /** Sets the maximum video bitrate in bits per second. */ + fun setMaxBitrate(maxBitrateBps: Long) + /** Sets the maximum video resolution. */ + fun setMaxResolution(width: Long, height: Long) + /** Returns the available video decoders for the current video's MIME type. */ + fun getAvailableDecoders(): List + /** Returns the name of the currently active video decoder, or null. */ + fun getCurrentDecoderName(): String? + /** Forces a specific video decoder by name, or null for automatic. */ + fun setVideoDecoder(decoderName: String?) companion object { /** The codec used by VideoPlayerInstanceApi. */ - val codec: MessageCodec by lazy { MessagesPigeonCodec() } - /** - * Sets up an instance of `VideoPlayerInstanceApi` to handle messages through the - * `binaryMessenger`. - */ + val codec: MessageCodec by lazy { + MessagesPigeonCodec() + } + /** Sets up an instance of `VideoPlayerInstanceApi` to handle messages through the `binaryMessenger`. */ @JvmOverloads - fun setUp( - binaryMessenger: BinaryMessenger, - api: VideoPlayerInstanceApi?, - messageChannelSuffix: String = "" - ) { - val separatedMessageChannelSuffix = - if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + fun setUp(binaryMessenger: BinaryMessenger, api: VideoPlayerInstanceApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val loopingArg = args[0] as Boolean - val wrapped: List = - try { - api.setLooping(loopingArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.setLooping(loopingArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -894,22 +1378,17 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val volumeArg = args[0] as Double - val wrapped: List = - try { - api.setVolume(volumeArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.setVolume(volumeArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -917,22 +1396,17 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val speedArg = args[0] as Double - val wrapped: List = - try { - api.setPlaybackSpeed(speedArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.setPlaybackSpeed(speedArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -940,20 +1414,15 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - api.play() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.play() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -961,20 +1430,15 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - api.pause() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.pause() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -982,22 +1446,17 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val positionArg = args[0] as Long - val wrapped: List = - try { - api.seekTo(positionArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.seekTo(positionArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1005,19 +1464,14 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - listOf(api.getCurrentPosition()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.getCurrentPosition()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1025,19 +1479,14 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - listOf(api.getBufferedPosition()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.getBufferedPosition()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1045,19 +1494,14 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - listOf(api.getAudioTracks()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.getAudioTracks()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1065,23 +1509,133 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val groupIndexArg = args[0] as Long val trackIndexArg = args[1] as Long - val wrapped: List = - try { - api.selectAudioTrack(groupIndexArg, trackIndexArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.selectAudioTrack(groupIndexArg, trackIndexArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAvailableQualities$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getAvailableQualities()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentQuality$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getCurrentQuality()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setMaxBitrate$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val maxBitrateBpsArg = args[0] as Long + val wrapped: List = try { + api.setMaxBitrate(maxBitrateBpsArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setMaxResolution$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val widthArg = args[0] as Long + val heightArg = args[1] as Long + val wrapped: List = try { + api.setMaxResolution(widthArg, heightArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAvailableDecoders$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getAvailableDecoders()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentDecoderName$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getCurrentDecoderName()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVideoDecoder$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val decoderNameArg = args[0] as String? + val wrapped: List = try { + api.setVideoDecoder(decoderNameArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1092,8 +1646,9 @@ interface VideoPlayerInstanceApi { } } -private class MessagesPigeonStreamHandler(val wrapper: MessagesPigeonEventChannelWrapper) : - EventChannel.StreamHandler { +private class MessagesPigeonStreamHandler( + val wrapper: MessagesPigeonEventChannelWrapper +) : EventChannel.StreamHandler { var pigeonSink: PigeonEventSink? = null override fun onListen(p0: Any?, sink: EventChannel.EventSink) { @@ -1126,26 +1681,21 @@ class PigeonEventSink(private val sink: EventChannel.EventSink) { sink.endOfStream() } } - + abstract class VideoEventsStreamHandler : MessagesPigeonEventChannelWrapper { companion object { - fun register( - messenger: BinaryMessenger, - streamHandler: VideoEventsStreamHandler, - instanceName: String = "" - ) { - var channelName: String = - "dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents" + fun register(messenger: BinaryMessenger, streamHandler: VideoEventsStreamHandler, instanceName: String = "") { + var channelName: String = "dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents" if (instanceName.isNotEmpty()) { channelName += ".$instanceName" } val internalStreamHandler = MessagesPigeonStreamHandler(streamHandler) - EventChannel(messenger, channelName, MessagesPigeonMethodCodec) - .setStreamHandler(internalStreamHandler) + EventChannel(messenger, channelName, MessagesPigeonMethodCodec).setStreamHandler(internalStreamHandler) } } - // Implement methods from MessagesPigeonEventChannelWrapper - override fun onListen(p0: Any?, sink: PigeonEventSink) {} +// Implement methods from MessagesPigeonEventChannelWrapper +override fun onListen(p0: Any?, sink: PigeonEventSink) {} - override fun onCancel(p0: Any?) {} +override fun onCancel(p0: Any?) {} } + diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewExoPlayerEventListenerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewExoPlayerEventListenerTest.java index 516b857b67a5..f643815d3fed 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewExoPlayerEventListenerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewExoPlayerEventListenerTest.java @@ -37,12 +37,12 @@ public final class PlatformViewExoPlayerEventListenerTest { @Before public void setUp() { - eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks); + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks, 3); } @Test public void onPlaybackStateChangedReadySendInitialized() { - eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks); + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks, 3); Format format = new Format.Builder().setWidth(800).setHeight(400).build(); when(mockExoPlayer.getVideoFormat()).thenReturn(format); @@ -54,7 +54,7 @@ public void onPlaybackStateChangedReadySendInitialized() { @Test public void onPlaybackStateChangedReadyInPortraitMode90DegreesSwapsWidthAndHeight() { - eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks); + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks, 3); Format format = new Format.Builder().setWidth(800).setHeight(400).setRotationDegrees(90).build(); @@ -67,7 +67,7 @@ public void onPlaybackStateChangedReadyInPortraitMode90DegreesSwapsWidthAndHeigh @Test public void onPlaybackStateChangedReadyInPortraitMode270DegreesSwapsWidthAndHeight() { - eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks); + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks, 3); Format format = new Format.Builder().setWidth(800).setHeight(400).setRotationDegrees(270).build(); diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureExoPlayerEventListenerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureExoPlayerEventListenerTest.java index e71cf619b885..88e844708355 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureExoPlayerEventListenerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureExoPlayerEventListenerTest.java @@ -38,7 +38,7 @@ public class TextureExoPlayerEventListenerTest { public void onPlaybackStateChangedReadySendInitialized_whenSurfaceProducerHandlesCropAndRotation() { TextureExoPlayerEventListener eventListener = - new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true); + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true, 3); VideoSize size = new VideoSize(800, 400, 0); when(mockExoPlayer.getVideoSize()).thenReturn(size); when(mockExoPlayer.getDuration()).thenReturn(10L); @@ -51,7 +51,7 @@ public class TextureExoPlayerEventListenerTest { public void onPlaybackStateChangedReadySendInitializedWithRotationCorrectionAndWidthAndHeightSwap_whenSurfaceProducerDoesNotHandleCropAndRotation() { TextureExoPlayerEventListener eventListener = - new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false); + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false, 3); VideoSize size = new VideoSize(800, 400, 0); int rotationCorrection = 90; Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build(); @@ -68,7 +68,7 @@ public class TextureExoPlayerEventListenerTest { public void onPlaybackStateChangedReadyInPortraitMode90DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerHandlesCropAndRotation() { TextureExoPlayerEventListener eventListener = - new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true); + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true, 3); VideoSize size = new VideoSize(800, 400, 0); when(mockExoPlayer.getVideoSize()).thenReturn(size); @@ -82,7 +82,7 @@ public class TextureExoPlayerEventListenerTest { public void onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_whenSurfaceProducerDoesNotHandleCropAndRotation() { TextureExoPlayerEventListener eventListener = - new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false); + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false, 3); VideoSize size = new VideoSize(800, 400, 0); int rotationCorrection = 90; Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build(); @@ -99,7 +99,7 @@ public class TextureExoPlayerEventListenerTest { public void onPlaybackStateChangedReadyInPortraitMode270DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerHandlesCropAndRotation() { TextureExoPlayerEventListener eventListener = - new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true); + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true, 3); VideoSize size = new VideoSize(800, 400, 0); when(mockExoPlayer.getVideoSize()).thenReturn(size); when(mockExoPlayer.getDuration()).thenReturn(10L); @@ -112,7 +112,7 @@ public class TextureExoPlayerEventListenerTest { public void onPlaybackStateChangedReadyInPortraitMode270DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerDoesNotHandleCropAndRotation() { TextureExoPlayerEventListener eventListener = - new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false); + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false, 3); VideoSize size = new VideoSize(800, 400, 0); int rotationCorrection = 270; Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build(); diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java index 6631d35a899f..100eadfd5024 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java @@ -8,6 +8,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import android.content.Context; import android.view.Surface; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; @@ -42,6 +43,7 @@ public final class TextureVideoPlayerTest { private static final String FAKE_ASSET_URL = "https://flutter.dev/movie.mp4"; private FakeVideoAsset fakeVideoAsset; + @Mock private Context mockContext; @Mock private VideoPlayerCallbacks mockEvents; @Mock private TextureRegistry.SurfaceProducer mockProducer; @Mock private ExoPlayer mockExoPlayer; @@ -64,7 +66,8 @@ private VideoPlayer createVideoPlayer() { private TextureVideoPlayer createVideoPlayer(VideoPlayerOptions options) { return new TextureVideoPlayer( - mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options, () -> mockExoPlayer); + mockContext, fakeVideoAsset, mockEvents, mockProducer, + fakeVideoAsset.getMediaItem(), options, () -> mockExoPlayer); } @Test diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index 92c2ff5f1566..dc444337908d 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -82,7 +82,7 @@ private TestVideoPlayer( protected ExoPlayerEventListener createExoPlayerEventListener( @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer) { // Use platform view implementation for testing. - return new PlatformViewExoPlayerEventListener(exoPlayer, mockEvents); + return new PlatformViewExoPlayerEventListener(exoPlayer, mockEvents, 3); } } diff --git a/packages/video_player/video_player_android/example/lib/mini_controller.dart b/packages/video_player/video_player_android/example/lib/mini_controller.dart index d8b275df7739..88760422e606 100644 --- a/packages/video_player/video_player_android/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_android/example/lib/mini_controller.dart @@ -292,6 +292,9 @@ class MiniController extends ValueNotifier { case VideoEventType.isPlayingStateUpdate: value = value.copyWith(isPlaying: event.isPlaying); case VideoEventType.unknown: + case VideoEventType.pipStateChanged: + case VideoEventType.qualityChanged: + case VideoEventType.decoderChanged: break; } } diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml index 07c5b497d5d2..079e64d56f73 100644 --- a/packages/video_player/video_player_android/example/pubspec.yaml +++ b/packages/video_player/video_player_android/example/pubspec.yaml @@ -18,7 +18,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - video_player_platform_interface: ^6.6.0 + video_player_platform_interface: + path: ../../video_player_platform_interface dev_dependencies: espresso: ^0.4.0 diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index 84249bd41afd..2ff48233ab0f 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -114,6 +114,9 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { httpHeaders: httpHeaders, userAgent: userAgent, formatHint: formatHint, + maxLoadRetries: options.androidOptions?.maxLoadRetries, + maxPlayerRecoveryAttempts: + options.androidOptions?.maxPlayerRecoveryAttempts, ); final int playerId; @@ -266,6 +269,151 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { return true; } + @override + Future isPipSupported() async { + return _api.isPipSupported(); + } + + @override + Future enterPip(int playerId) { + return _api.enterPip(playerId); + } + + @override + Future exitPip(int playerId) async { + // Android PiP is exited by tapping the full-screen button in PiP window + // or by the system. There's no programmatic exit API needed. + } + + @override + Future isPipActive(int playerId) async { + return _api.isPipActive(); + } + + @override + Future setAutoEnterPip(int playerId, bool enabled) { + return _api.setAutoEnterPip(enabled); + } + + @override + Future enableBackgroundPlayback(int playerId, {MediaInfo? mediaInfo}) { + final PlatformMediaInfo? pigeonMediaInfo = mediaInfo != null + ? PlatformMediaInfo( + title: mediaInfo.title, + artist: mediaInfo.artist, + artworkUrl: mediaInfo.artworkUrl, + durationMs: mediaInfo.durationMs, + ) + : null; + return _api.enableBackgroundPlayback(playerId, pigeonMediaInfo); + } + + @override + Future disableBackgroundPlayback(int playerId) { + return _api.disableBackgroundPlayback(playerId); + } + + // Cache control methods + + @override + Future setCacheMaxSize(int maxSizeBytes) { + return _api.setCacheMaxSize(maxSizeBytes); + } + + @override + Future clearCache() { + return _api.clearCache(); + } + + @override + Future getCacheSize() async { + return _api.getCacheSize(); + } + + @override + Future isCacheEnabled() async { + return _api.isCacheEnabled(); + } + + @override + Future setCacheEnabled(bool enabled) { + return _api.setCacheEnabled(enabled); + } + + // ABR control methods + + @override + Future> getAvailableQualities(int playerId) async { + final List qualities = + await _playerWith(id: playerId).getAvailableQualities(); + return qualities + .map( + (PlatformVideoQuality q) => VideoQuality( + width: q.width, + height: q.height, + bitrate: q.bitrate, + codec: q.codec, + isSelected: q.isSelected, + ), + ) + .toList(); + } + + @override + Future getCurrentQuality(int playerId) async { + final PlatformVideoQuality? q = + await _playerWith(id: playerId).getCurrentQuality(); + if (q == null) { + return null; + } + return VideoQuality( + width: q.width, + height: q.height, + bitrate: q.bitrate, + codec: q.codec, + isSelected: q.isSelected, + ); + } + + @override + Future setMaxBitrate(int playerId, int maxBitrateBps) { + return _playerWith(id: playerId).setMaxBitrate(maxBitrateBps); + } + + @override + Future setMaxResolution(int playerId, int width, int height) { + return _playerWith(id: playerId).setMaxResolution(width, height); + } + + // Decoder selection methods + + @override + Future> getAvailableDecoders(int playerId) async { + final List decoders = + await _playerWith(id: playerId).getAvailableDecoders(); + return decoders + .map( + (PlatformVideoDecoder d) => VideoDecoderInfo( + name: d.name, + mimeType: d.mimeType, + isHardwareAccelerated: d.isHardwareAccelerated, + isSoftwareOnly: d.isSoftwareOnly, + isSelected: d.isSelected, + ), + ) + .toList(); + } + + @override + Future getCurrentDecoderName(int playerId) { + return _playerWith(id: playerId).getCurrentDecoderName(); + } + + @override + Future setVideoDecoder(int playerId, String? decoderName) { + return _playerWith(id: playerId).setVideoDecoder(decoderName); + } + _PlayerInstance _playerWith({required int id}) { final _PlayerInstance? player = _players[id]; return player ?? (throw StateError('No active player with ID $id.')); @@ -310,6 +458,7 @@ class _PlayerInstance { StreamController(); late final StreamSubscription _eventSubscription; bool _isDisposed = false; + bool _isInitialized = false; Timer? _bufferPollingTimer; int _lastBufferPosition = -1; bool _isBuffering = false; @@ -353,6 +502,34 @@ class _PlayerInstance { return _api.getAudioTracks(); } + Future> getAvailableQualities() { + return _api.getAvailableQualities(); + } + + Future getCurrentQuality() { + return _api.getCurrentQuality(); + } + + Future setMaxBitrate(int maxBitrateBps) { + return _api.setMaxBitrate(maxBitrateBps); + } + + Future setMaxResolution(int width, int height) { + return _api.setMaxResolution(width, height); + } + + Future> getAvailableDecoders() { + return _api.getAvailableDecoders(); + } + + Future getCurrentDecoderName() { + return _api.getCurrentDecoderName(); + } + + Future setVideoDecoder(String? decoderName) { + return _api.setVideoDecoder(decoderName); + } + Future selectAudioTrack(String trackId) async { // Parse the trackId to get groupIndex and trackIndex final List parts = trackId.split('_'); @@ -407,6 +584,8 @@ class _PlayerInstance { if (!_isDisposed) { _updateBufferPosition(position); } + }).catchError((_) { + // Can fail briefly during ExoPlayer rebuild (decoder switch). }); } } @@ -428,6 +607,11 @@ class _PlayerInstance { void _onStreamEvent(PlatformVideoEvent event) { switch (event) { case InitializationEvent _: + if (_isInitialized) { + // Suppress duplicate init events (e.g. after decoder switch rebuild). + break; + } + _isInitialized = true; _eventStreamController.add( VideoEvent( eventType: VideoEventType.initialized, @@ -442,9 +626,13 @@ class _PlayerInstance { _bufferPollingTimer = Timer.periodic(const Duration(seconds: 1), ( Timer timer, ) async { - final int position = await _api.getBufferedPosition(); - if (!_isDisposed) { - _updateBufferPosition(position); + try { + final int position = await _api.getBufferedPosition(); + if (!_isDisposed) { + _updateBufferPosition(position); + } + } catch (_) { + // Can fail briefly during ExoPlayer rebuild (decoder switch). } }); case IsPlayingStateEvent _: @@ -487,6 +675,36 @@ class _PlayerInstance { !_audioTrackSelectionCompleter!.isCompleted) { _audioTrackSelectionCompleter!.complete(); } + case PipStateEvent _: + _eventStreamController.add( + VideoEvent( + eventType: VideoEventType.pipStateChanged, + isPipActive: event.isInPipMode, + wasDismissed: event.wasDismissed, + pipWindowSize: Size(event.windowWidth.toDouble(), event.windowHeight.toDouble()), + ), + ); + case VideoQualityChangedEvent _: + _eventStreamController.add( + VideoEvent( + eventType: VideoEventType.qualityChanged, + quality: VideoQuality( + width: event.width, + height: event.height, + bitrate: event.bitrate, + codec: event.codec, + isSelected: true, + ), + ), + ); + case DecoderChangedEvent _: + _eventStreamController.add( + VideoEvent( + eventType: VideoEventType.decoderChanged, + decoderName: event.decoderName, + isDecoderHardwareAccelerated: event.isHardwareAccelerated, + ), + ); } } diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index 1aca7dc531dd..3c3e8161eecb 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.5), do not edit directly. +// Autogenerated from Pigeon (v26.1.10), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -11,39 +11,65 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +Object? _extractReplyValueOrThrow( + List? replyList, + String channelName, { + required bool isNullValid, +}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && - a.indexed.every( - ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), - ); + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key]), - ); + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); } return a == b; } + /// Pigeon equivalent of video_platform_interface's VideoFormat. -enum PlatformVideoFormat { dash, hls, ss } +enum PlatformVideoFormat { + dash, + hls, + ss, +} /// Pigeon equivalent of Player's playback state. /// https://developer.android.com/media/media3/exoplayer/listening-to-player-events#playback-state -enum PlatformPlaybackState { idle, buffering, ready, ended, unknown } +enum PlatformPlaybackState { + idle, + buffering, + ready, + ended, + unknown, +} -sealed class PlatformVideoEvent {} +sealed class PlatformVideoEvent { +} /// Sent when the video is initialized and ready to play. class InitializationEvent extends PlatformVideoEvent { @@ -67,12 +93,16 @@ class InitializationEvent extends PlatformVideoEvent { int rotationCorrection; List _toList() { - return [duration, width, height, rotationCorrection]; + return [ + duration, + width, + height, + rotationCorrection, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static InitializationEvent decode(Object result) { result as List; @@ -98,35 +128,40 @@ class InitializationEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Sent when the video state changes. /// /// Corresponds to ExoPlayer's onPlaybackStateChanged. class PlaybackStateChangeEvent extends PlatformVideoEvent { - PlaybackStateChangeEvent({required this.state}); + PlaybackStateChangeEvent({ + required this.state, + }); PlatformPlaybackState state; List _toList() { - return [state]; + return [ + state, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlaybackStateChangeEvent decode(Object result) { result as List; - return PlaybackStateChangeEvent(state: result[0]! as PlatformPlaybackState); + return PlaybackStateChangeEvent( + state: result[0]! as PlatformPlaybackState, + ); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlaybackStateChangeEvent || - other.runtimeType != runtimeType) { + if (other is! PlaybackStateChangeEvent || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -137,28 +172,34 @@ class PlaybackStateChangeEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Sent when the video starts or stops playing. /// /// Corresponds to ExoPlayer's onIsPlayingChanged. class IsPlayingStateEvent extends PlatformVideoEvent { - IsPlayingStateEvent({required this.isPlaying}); + IsPlayingStateEvent({ + required this.isPlaying, + }); bool isPlaying; List _toList() { - return [isPlaying]; + return [ + isPlaying, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static IsPlayingStateEvent decode(Object result) { result as List; - return IsPlayingStateEvent(isPlaying: result[0]! as bool); + return IsPlayingStateEvent( + isPlaying: result[0]! as bool, + ); } @override @@ -175,7 +216,8 @@ class IsPlayingStateEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Sent when audio tracks change. @@ -183,22 +225,27 @@ class IsPlayingStateEvent extends PlatformVideoEvent { /// This includes when the selected audio track changes after calling selectAudioTrack. /// Corresponds to ExoPlayer's onTracksChanged. class AudioTrackChangedEvent extends PlatformVideoEvent { - AudioTrackChangedEvent({this.selectedTrackId}); + AudioTrackChangedEvent({ + this.selectedTrackId, + }); /// The ID of the newly selected audio track, if any. String? selectedTrackId; List _toList() { - return [selectedTrackId]; + return [ + selectedTrackId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static AudioTrackChangedEvent decode(Object result) { result as List; - return AudioTrackChangedEvent(selectedTrackId: result[0] as String?); + return AudioTrackChangedEvent( + selectedTrackId: result[0] as String?, + ); } @override @@ -215,33 +262,158 @@ class AudioTrackChangedEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Sent when the video quality changes (ABR switch). +/// +/// Corresponds to ExoPlayer's AnalyticsListener.onDownstreamFormatChanged. +class VideoQualityChangedEvent extends PlatformVideoEvent { + VideoQualityChangedEvent({ + required this.width, + required this.height, + required this.bitrate, + this.codec, + }); + + int width; + + int height; + + int bitrate; + + String? codec; + + List _toList() { + return [ + width, + height, + bitrate, + codec, + ]; + } + + Object encode() { + return _toList(); } + + static VideoQualityChangedEvent decode(Object result) { + result as List; + return VideoQualityChangedEvent( + width: result[0]! as int, + height: result[1]! as int, + bitrate: result[2]! as int, + codec: result[3] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! VideoQualityChangedEvent || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Sent when PiP state changes. +class PipStateEvent extends PlatformVideoEvent { + PipStateEvent({ + required this.isInPipMode, + required this.wasDismissed, + required this.windowWidth, + required this.windowHeight, + }); + + bool isInPipMode; + + /// Whether PiP was dismissed by the user (X button) as opposed to + /// expanded back to full screen. Only meaningful when [isInPipMode] is false. + bool wasDismissed; + + /// The window width in dp at the time of the PiP state change. + int windowWidth; + + /// The window height in dp at the time of the PiP state change. + int windowHeight; + + List _toList() { + return [ + isInPipMode, + wasDismissed, + windowWidth, + windowHeight, + ]; + } + + Object encode() { + return _toList(); } + + static PipStateEvent decode(Object result) { + result as List; + return PipStateEvent( + isInPipMode: result[0]! as bool, + wasDismissed: result[1]! as bool, + windowWidth: result[2]! as int, + windowHeight: result[3]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PipStateEvent || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; } /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { - PlatformVideoViewCreationParams({required this.playerId}); + PlatformVideoViewCreationParams({ + required this.playerId, + }); int playerId; List _toList() { - return [playerId]; + return [ + playerId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlatformVideoViewCreationParams decode(Object result) { result as List; - return PlatformVideoViewCreationParams(playerId: result[0]! as int); + return PlatformVideoViewCreationParams( + playerId: result[0]! as int, + ); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlatformVideoViewCreationParams || - other.runtimeType != runtimeType) { + if (other is! PlatformVideoViewCreationParams || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -252,7 +424,8 @@ class PlatformVideoViewCreationParams { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class CreationOptions { @@ -261,6 +434,8 @@ class CreationOptions { this.formatHint, required this.httpHeaders, this.userAgent, + this.maxLoadRetries, + this.maxPlayerRecoveryAttempts, }); String uri; @@ -271,22 +446,37 @@ class CreationOptions { String? userAgent; + /// Max retries per segment/load error before escalating. + /// Null means use ExoPlayer's default (5). + int? maxLoadRetries; + + /// Max player-level recovery attempts for fatal network errors. + /// Null means use the default (3). + int? maxPlayerRecoveryAttempts; + List _toList() { - return [uri, formatHint, httpHeaders, userAgent]; + return [ + uri, + formatHint, + httpHeaders, + userAgent, + maxLoadRetries, + maxPlayerRecoveryAttempts, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static CreationOptions decode(Object result) { result as List; return CreationOptions( uri: result[0]! as String, formatHint: result[1] as PlatformVideoFormat?, - httpHeaders: (result[2] as Map?)! - .cast(), + httpHeaders: (result[2] as Map?)!.cast(), userAgent: result[3] as String?, + maxLoadRetries: result[4] as int?, + maxPlayerRecoveryAttempts: result[5] as int?, ); } @@ -304,23 +494,29 @@ class CreationOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class TexturePlayerIds { - TexturePlayerIds({required this.playerId, required this.textureId}); + TexturePlayerIds({ + required this.playerId, + required this.textureId, + }); int playerId; int textureId; List _toList() { - return [playerId, textureId]; + return [ + playerId, + textureId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static TexturePlayerIds decode(Object result) { result as List; @@ -344,11 +540,15 @@ class TexturePlayerIds { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class PlaybackState { - PlaybackState({required this.playPosition, required this.bufferPosition}); + PlaybackState({ + required this.playPosition, + required this.bufferPosition, + }); /// The current playback position, in milliseconds. int playPosition; @@ -357,12 +557,14 @@ class PlaybackState { int bufferPosition; List _toList() { - return [playPosition, bufferPosition]; + return [ + playPosition, + bufferPosition, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlaybackState decode(Object result) { result as List; @@ -386,7 +588,8 @@ class PlaybackState { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Represents an audio track in a video. @@ -432,8 +635,7 @@ class AudioTrackMessage { } Object encode() { - return _toList(); - } + return _toList(); } static AudioTrackMessage decode(Object result) { result as List; @@ -463,7 +665,8 @@ class AudioTrackMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Raw audio track data from ExoPlayer Format objects. @@ -513,8 +716,7 @@ class ExoPlayerAudioTrackData { } Object encode() { - return _toList(); - } + return _toList(); } static ExoPlayerAudioTrackData decode(Object result) { result as List; @@ -545,29 +747,32 @@ class ExoPlayerAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Container for raw audio track data from Android ExoPlayer. class NativeAudioTrackData { - NativeAudioTrackData({this.exoPlayerTracks}); + NativeAudioTrackData({ + this.exoPlayerTracks, + }); /// ExoPlayer-based tracks List? exoPlayerTracks; List _toList() { - return [exoPlayerTracks]; + return [ + exoPlayerTracks, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static NativeAudioTrackData decode(Object result) { result as List; return NativeAudioTrackData( - exoPlayerTracks: (result[0] as List?) - ?.cast(), + exoPlayerTracks: (result[0] as List?)?.cast(), ); } @@ -585,279 +790,684 @@ class NativeAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is PlatformVideoFormat) { - buffer.putUint8(129); - writeValue(buffer, value.index); - } else if (value is PlatformPlaybackState) { - buffer.putUint8(130); - writeValue(buffer, value.index); - } else if (value is InitializationEvent) { - buffer.putUint8(131); - writeValue(buffer, value.encode()); - } else if (value is PlaybackStateChangeEvent) { - buffer.putUint8(132); - writeValue(buffer, value.encode()); - } else if (value is IsPlayingStateEvent) { - buffer.putUint8(133); - writeValue(buffer, value.encode()); - } else if (value is AudioTrackChangedEvent) { - buffer.putUint8(134); - writeValue(buffer, value.encode()); - } else if (value is PlatformVideoViewCreationParams) { - buffer.putUint8(135); - writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { - buffer.putUint8(136); - writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { - buffer.putUint8(137); - writeValue(buffer, value.encode()); - } else if (value is PlaybackState) { - buffer.putUint8(138); - writeValue(buffer, value.encode()); - } else if (value is AudioTrackMessage) { - buffer.putUint8(139); - writeValue(buffer, value.encode()); - } else if (value is ExoPlayerAudioTrackData) { - buffer.putUint8(140); - writeValue(buffer, value.encode()); - } else if (value is NativeAudioTrackData) { - buffer.putUint8(141); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } +class PlatformMediaInfo { + PlatformMediaInfo({ + required this.title, + this.artist, + this.artworkUrl, + this.durationMs, + }); + + String title; + + String? artist; + + String? artworkUrl; + + int? durationMs; + + List _toList() { + return [ + title, + artist, + artworkUrl, + durationMs, + ]; + } + + Object encode() { + return _toList(); } + + static PlatformMediaInfo decode(Object result) { + result as List; + return PlatformMediaInfo( + title: result[0]! as String, + artist: result[1] as String?, + artworkUrl: result[2] as String?, + durationMs: result[3] as int?, + ); } @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - final value = readValue(buffer) as int?; - return value == null ? null : PlatformVideoFormat.values[value]; - case 130: - final value = readValue(buffer) as int?; - return value == null ? null : PlatformPlaybackState.values[value]; - case 131: - return InitializationEvent.decode(readValue(buffer)!); - case 132: - return PlaybackStateChangeEvent.decode(readValue(buffer)!); - case 133: - return IsPlayingStateEvent.decode(readValue(buffer)!); - case 134: - return AudioTrackChangedEvent.decode(readValue(buffer)!); - case 135: - return PlatformVideoViewCreationParams.decode(readValue(buffer)!); - case 136: - return CreationOptions.decode(readValue(buffer)!); - case 137: - return TexturePlayerIds.decode(readValue(buffer)!); - case 138: - return PlaybackState.decode(readValue(buffer)!); - case 139: - return AudioTrackMessage.decode(readValue(buffer)!); - case 140: - return ExoPlayerAudioTrackData.decode(readValue(buffer)!); - case 141: - return NativeAudioTrackData.decode(readValue(buffer)!); - default: - return super.readValueOfType(type, buffer); + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformMediaInfo || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; } + return _deepEquals(encode(), other.encode()); } -} -const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec( - _PigeonCodec(), -); + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} -class AndroidVideoPlayerApi { - /// Constructor for [AndroidVideoPlayerApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - AndroidVideoPlayerApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; +/// Sent when the active video decoder changes. +/// +/// Corresponds to ExoPlayer's AnalyticsListener.onVideoDecoderInitialized. +class DecoderChangedEvent extends PlatformVideoEvent { + DecoderChangedEvent({ + required this.decoderName, + required this.isHardwareAccelerated, + }); - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + String decoderName; - final String pigeonVar_messageChannelSuffix; + bool isHardwareAccelerated; - Future initialize() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + List _toList() { + return [ + decoderName, + isHardwareAccelerated, + ]; } - Future createForPlatformView(CreationOptions options) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [options], + Object encode() { + return _toList(); } + + static DecoderChangedEvent decode(Object result) { + result as List; + return DecoderChangedEvent( + decoderName: result[0]! as String, + isHardwareAccelerated: result[1]! as bool, ); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as int?)!; - } } - Future createForTextureView(CreationOptions options) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! DecoderChangedEvent || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Describes a video decoder available on the device. +class PlatformVideoDecoder { + PlatformVideoDecoder({ + required this.name, + required this.mimeType, + required this.isHardwareAccelerated, + required this.isSoftwareOnly, + required this.isSelected, + }); + + String name; + + String mimeType; + + bool isHardwareAccelerated; + + bool isSoftwareOnly; + + bool isSelected; + + List _toList() { + return [ + name, + mimeType, + isHardwareAccelerated, + isSoftwareOnly, + isSelected, + ]; + } + + Object encode() { + return _toList(); } + + static PlatformVideoDecoder decode(Object result) { + result as List; + return PlatformVideoDecoder( + name: result[0]! as String, + mimeType: result[1]! as String, + isHardwareAccelerated: result[2]! as bool, + isSoftwareOnly: result[3]! as bool, + isSelected: result[4]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformVideoDecoder || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Represents a video quality variant (resolution/bitrate combination). +class PlatformVideoQuality { + PlatformVideoQuality({ + required this.width, + required this.height, + required this.bitrate, + this.codec, + required this.isSelected, + }); + + int width; + + int height; + + int bitrate; + + String? codec; + + bool isSelected; + + List _toList() { + return [ + width, + height, + bitrate, + codec, + isSelected, + ]; + } + + Object encode() { + return _toList(); } + + static PlatformVideoQuality decode(Object result) { + result as List; + return PlatformVideoQuality( + width: result[0]! as int, + height: result[1]! as int, + bitrate: result[2]! as int, + codec: result[3] as String?, + isSelected: result[4]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformVideoQuality || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is PlatformVideoFormat) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is PlatformPlaybackState) { + buffer.putUint8(130); + writeValue(buffer, value.index); + } else if (value is InitializationEvent) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is PlaybackStateChangeEvent) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is IsPlayingStateEvent) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is AudioTrackChangedEvent) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is VideoQualityChangedEvent) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is PipStateEvent) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is PlatformVideoViewCreationParams) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is CreationOptions) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else if (value is TexturePlayerIds) { + buffer.putUint8(139); + writeValue(buffer, value.encode()); + } else if (value is PlaybackState) { + buffer.putUint8(140); + writeValue(buffer, value.encode()); + } else if (value is AudioTrackMessage) { + buffer.putUint8(141); + writeValue(buffer, value.encode()); + } else if (value is ExoPlayerAudioTrackData) { + buffer.putUint8(142); + writeValue(buffer, value.encode()); + } else if (value is NativeAudioTrackData) { + buffer.putUint8(143); + writeValue(buffer, value.encode()); + } else if (value is PlatformMediaInfo) { + buffer.putUint8(144); + writeValue(buffer, value.encode()); + } else if (value is DecoderChangedEvent) { + buffer.putUint8(145); + writeValue(buffer, value.encode()); + } else if (value is PlatformVideoDecoder) { + buffer.putUint8(146); + writeValue(buffer, value.encode()); + } else if (value is PlatformVideoQuality) { + buffer.putUint8(147); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformVideoFormat.values[value]; + case 130: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformPlaybackState.values[value]; + case 131: + return InitializationEvent.decode(readValue(buffer)!); + case 132: + return PlaybackStateChangeEvent.decode(readValue(buffer)!); + case 133: + return IsPlayingStateEvent.decode(readValue(buffer)!); + case 134: + return AudioTrackChangedEvent.decode(readValue(buffer)!); + case 135: + return VideoQualityChangedEvent.decode(readValue(buffer)!); + case 136: + return PipStateEvent.decode(readValue(buffer)!); + case 137: + return PlatformVideoViewCreationParams.decode(readValue(buffer)!); + case 138: + return CreationOptions.decode(readValue(buffer)!); + case 139: + return TexturePlayerIds.decode(readValue(buffer)!); + case 140: + return PlaybackState.decode(readValue(buffer)!); + case 141: + return AudioTrackMessage.decode(readValue(buffer)!); + case 142: + return ExoPlayerAudioTrackData.decode(readValue(buffer)!); + case 143: + return NativeAudioTrackData.decode(readValue(buffer)!); + case 144: + return PlatformMediaInfo.decode(readValue(buffer)!); + case 145: + return DecoderChangedEvent.decode(readValue(buffer)!); + case 146: + return PlatformVideoDecoder.decode(readValue(buffer)!); + case 147: + return PlatformVideoQuality.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec()); + +class AndroidVideoPlayerApi { + /// Constructor for [AndroidVideoPlayerApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + AndroidVideoPlayerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future initialize() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [options], + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future createForPlatformView(CreationOptions options) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as TexturePlayerIds?)!; - } + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as int; } - Future dispose(int playerId) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; + Future createForTextureView(CreationOptions options) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [playerId], + final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as TexturePlayerIds; + } + + Future dispose(int playerId) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } Future setMixWithOthers(bool mixWithOthers) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [mixWithOthers], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } Future getLookupKeyForAsset(String asset, String? packageName) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [asset, packageName], + final Future pigeonVar_sendFuture = pigeonVar_channel.send([asset, packageName]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as String; + } + + Future enableBackgroundPlayback(int playerId, PlatformMediaInfo? mediaInfo) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.enableBackgroundPlayback$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId, mediaInfo]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as String?)!; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future disableBackgroundPlayback(int playerId) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.disableBackgroundPlayback$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future isPipSupported() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.isPipSupported$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as bool; + } + + Future enterPip(int playerId) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.enterPip$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future isPipActive() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.isPipActive$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as bool; + } + + Future setAutoEnterPip(bool enabled) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setAutoEnterPip$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future setCacheMaxSize(int maxSizeBytes) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setCacheMaxSize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([maxSizeBytes]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future clearCache() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.clearCache$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future getCacheSize() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getCacheSize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as int; + } + + Future isCacheEnabled() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.isCacheEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as bool; + } + + Future setCacheEnabled(bool enabled) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setCacheEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } } @@ -865,13 +1475,9 @@ class VideoPlayerInstanceApi { /// Constructor for [VideoPlayerInstanceApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - VideoPlayerInstanceApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + VideoPlayerInstanceApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -880,86 +1486,64 @@ class VideoPlayerInstanceApi { /// Sets whether to automatically loop playback of the video. Future setLooping(bool looping) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [looping], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } /// Sets the volume, with 0.0 being muted and 1.0 being full volume. Future setVolume(double volume) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [volume], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } /// Sets the playback speed as a multiple of normal speed. Future setPlaybackSpeed(double speed) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [speed], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } /// Begins playback if the video is not currently playing. Future play() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -967,23 +1551,18 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } /// Pauses playback if the video is currently playing. Future pause() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -991,49 +1570,37 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } /// Seeks to the given playback position, in milliseconds. Future seekTo(int position) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [position], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } /// Returns the current playback position, in milliseconds. Future getCurrentPosition() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1041,28 +1608,19 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as int?)!; - } + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as int; } /// Returns the current buffer position, in milliseconds. Future getBufferedPosition() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1070,28 +1628,19 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as int?)!; - } + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as int; } /// Gets the available audio tracks for the video. Future getAudioTracks() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1099,60 +1648,181 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as NativeAudioTrackData?)!; - } + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as NativeAudioTrackData; } /// Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] Future selectAudioTrack(int groupIndex, int trackIndex) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [groupIndex, trackIndex], + final Future pigeonVar_sendFuture = pigeonVar_channel.send([groupIndex, trackIndex]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + /// Returns the available video quality variants. + Future> getAvailableQualities() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAvailableQualities$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return (pigeonVar_replyValue as List).cast(); + } + + /// Returns the currently playing video quality, or null if unknown. + Future getCurrentQuality() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentQuality$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + return pigeonVar_replyValue as PlatformVideoQuality?; + } + + /// Sets the maximum video bitrate in bits per second. + Future setMaxBitrate(int maxBitrateBps) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setMaxBitrate$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([maxBitrateBps]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + /// Sets the maximum video resolution. + Future setMaxResolution(int width, int height) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setMaxResolution$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([width, height]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + /// Returns the available video decoders for the current video's MIME type. + Future> getAvailableDecoders() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAvailableDecoders$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return (pigeonVar_replyValue as List).cast(); + } + + /// Returns the name of the currently active video decoder, or null. + Future getCurrentDecoderName() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentDecoderName$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + return pigeonVar_replyValue as String?; + } + + /// Forces a specific video decoder by name, or null for automatic. + Future setVideoDecoder(String? decoderName) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVideoDecoder$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([decoderName]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } } -Stream videoEvents({String instanceName = ''}) { +Stream videoEvents( {String instanceName = ''}) { if (instanceName.isNotEmpty) { instanceName = '.$instanceName'; } - final EventChannel videoEventsChannel = EventChannel( - 'dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents$instanceName', - pigeonMethodCodec, - ); + final EventChannel videoEventsChannel = + EventChannel('dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents$instanceName', pigeonMethodCodec); return videoEventsChannel.receiveBroadcastStream().map((dynamic event) { return event as PlatformVideoEvent; }); } + diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index 8666b074969a..a986a0fa5677 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -60,6 +60,31 @@ class AudioTrackChangedEvent extends PlatformVideoEvent { late final String? selectedTrackId; } +/// Sent when the video quality changes (ABR switch). +/// +/// Corresponds to ExoPlayer's AnalyticsListener.onDownstreamFormatChanged. +class VideoQualityChangedEvent extends PlatformVideoEvent { + late final int width; + late final int height; + late final int bitrate; + late final String? codec; +} + +/// Sent when PiP state changes. +class PipStateEvent extends PlatformVideoEvent { + late final bool isInPipMode; + + /// Whether PiP was dismissed by the user (X button) as opposed to + /// expanded back to full screen. Only meaningful when [isInPipMode] is false. + late final bool wasDismissed; + + /// The window width in dp at the time of the PiP state change. + late final int windowWidth; + + /// The window height in dp at the time of the PiP state change. + late final int windowHeight; +} + /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { const PlatformVideoViewCreationParams({required this.playerId}); @@ -73,6 +98,14 @@ class CreationOptions { PlatformVideoFormat? formatHint; Map httpHeaders; String? userAgent; + + /// Max retries per segment/load error before escalating. + /// Null means use ExoPlayer's default (5). + int? maxLoadRetries; + + /// Max player-level recovery attempts for fatal network errors. + /// Null means use the default (3). + int? maxPlayerRecoveryAttempts; } class TexturePlayerIds { @@ -148,6 +181,53 @@ class NativeAudioTrackData { List? exoPlayerTracks; } +class PlatformMediaInfo { + PlatformMediaInfo({required this.title}); + String title; + String? artist; + String? artworkUrl; + int? durationMs; +} + +/// Sent when the active video decoder changes. +/// +/// Corresponds to ExoPlayer's AnalyticsListener.onVideoDecoderInitialized. +class DecoderChangedEvent extends PlatformVideoEvent { + late final String decoderName; + late final bool isHardwareAccelerated; +} + +/// Describes a video decoder available on the device. +class PlatformVideoDecoder { + PlatformVideoDecoder({ + required this.name, + required this.mimeType, + required this.isHardwareAccelerated, + required this.isSoftwareOnly, + required this.isSelected, + }); + String name; + String mimeType; + bool isHardwareAccelerated; + bool isSoftwareOnly; + bool isSelected; +} + +/// Represents a video quality variant (resolution/bitrate combination). +class PlatformVideoQuality { + PlatformVideoQuality({ + required this.width, + required this.height, + required this.bitrate, + required this.isSelected, + }); + int width; + int height; + int bitrate; + String? codec; + bool isSelected; +} + @HostApi() abstract class AndroidVideoPlayerApi { void initialize(); @@ -159,6 +239,19 @@ abstract class AndroidVideoPlayerApi { void dispose(int playerId); void setMixWithOthers(bool mixWithOthers); String getLookupKeyForAsset(String asset, String? packageName); + void enableBackgroundPlayback(int playerId, PlatformMediaInfo? mediaInfo); + void disableBackgroundPlayback(int playerId); + bool isPipSupported(); + void enterPip(int playerId); + bool isPipActive(); + void setAutoEnterPip(bool enabled); + + // Cache control methods + void setCacheMaxSize(int maxSizeBytes); + void clearCache(); + int getCacheSize(); + bool isCacheEnabled(); + void setCacheEnabled(bool enabled); } @HostApi() @@ -192,6 +285,31 @@ abstract class VideoPlayerInstanceApi { /// Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] void selectAudioTrack(int groupIndex, int trackIndex); + + // ABR (Adaptive Bitrate) control methods + + /// Returns the available video quality variants. + List getAvailableQualities(); + + /// Returns the currently playing video quality, or null if unknown. + PlatformVideoQuality? getCurrentQuality(); + + /// Sets the maximum video bitrate in bits per second. + void setMaxBitrate(int maxBitrateBps); + + /// Sets the maximum video resolution. + void setMaxResolution(int width, int height); + + // Decoder selection methods + + /// Returns the available video decoders for the current video's MIME type. + List getAvailableDecoders(); + + /// Returns the name of the currently active video decoder, or null. + String? getCurrentDecoderName(); + + /// Forces a specific video decoder by name, or null for automatic. + void setVideoDecoder(String? decoderName); } @EventChannelApi() diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 359ba7466e27..fad416d069f7 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -20,7 +20,8 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^6.6.0 + video_player_platform_interface: + path: ../video_player_platform_interface dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPBackgroundAudioHandler.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPBackgroundAudioHandler.m new file mode 100644 index 000000000000..9358bc7111e9 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPBackgroundAudioHandler.m @@ -0,0 +1,332 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "./include/video_player_avfoundation/FVPBackgroundAudioHandler.h" + +#if TARGET_OS_IOS +@import MediaPlayer; +@import UIKit; +#endif + +@implementation FVPBackgroundAudioHandler { + AVPlayer *_player; + NSString *_title; + NSString *_artist; + BOOL _isEnabled; + id _timeObserver; + id _playTarget; + id _pauseTarget; + id _seekTarget; + NSNumber *_cachedDuration; +#if TARGET_OS_IOS + UIBackgroundTaskIdentifier _backgroundTask; + MPMediaItemArtwork *_cachedArtwork; + NSString *_artworkUrl; +#endif +} + +- (instancetype)initWithPlayer:(AVPlayer *)player { + self = [super init]; + if (self) { + _player = player; + _isEnabled = NO; +#if TARGET_OS_IOS + _backgroundTask = UIBackgroundTaskInvalid; +#endif + } + return self; +} + +- (BOOL)isEnabled { + return _isEnabled; +} + +- (void)enableWithTitle:(nullable NSString *)title + artist:(nullable NSString *)artist + artworkUrl:(nullable NSString *)artworkUrl + durationMs:(nullable NSNumber *)durationMs { +#if TARGET_OS_IOS + // Remove any existing handlers first to prevent leaks. + if (_isEnabled) { + [self removeCommandTargets]; + [self removeAppLifecycleObservers]; + } + + _isEnabled = YES; + _title = title ?: @"Video"; + _artist = artist; + _cachedDuration = nil; + _cachedArtwork = nil; + _artworkUrl = artworkUrl; + + if (artworkUrl.length > 0) { + [self loadArtworkFromUrl:artworkUrl]; + } + + NSLog(@"video_player: [BG] enableWithTitle called — title=%@, player.rate=%f", title, _player.rate); + + // Ensure audio session category is Playback (not Ambient/SoloAmbient) so audio + // continues when the app is backgrounded. Re-set here in case another plugin or + // player changed the category since initialize was called. + AVAudioSession *session = [AVAudioSession sharedInstance]; + NSError *categoryError = nil; + [session setCategory:AVAudioSessionCategoryPlayback + mode:AVAudioSessionModeDefault + options:0 + error:&categoryError]; + if (categoryError) { + NSLog(@"video_player: [BG] Failed to set audio session category: %@", categoryError); + } + + // Explicitly activate the audio session so playback persists through background transitions. + NSError *sessionError = nil; + [session setActive:YES error:&sessionError]; + if (sessionError) { + NSLog(@"video_player: [BG] Failed to activate audio session: %@", sessionError); + } + + // Signal to iOS that this app is an active media app. + [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; + + NSLog(@"video_player: [BG] Audio session ready — category=%@, active=YES, player.rate=%f", + session.category, _player.rate); + + // Observe app lifecycle to keep playback alive across background transitions. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleInterruption:) + name:AVAudioSessionInterruptionNotification + object:session]; + + // Set up remote command center + MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; + + _playTarget = [commandCenter.playCommand + addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) { + [self->_player play]; + return MPRemoteCommandHandlerStatusSuccess; + }]; + + _pauseTarget = [commandCenter.pauseCommand + addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) { + [self->_player pause]; + return MPRemoteCommandHandlerStatusSuccess; + }]; + + _seekTarget = [commandCenter.changePlaybackPositionCommand + addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) { + MPChangePlaybackPositionCommandEvent *posEvent = + (MPChangePlaybackPositionCommandEvent *)event; + [self->_player seekToTime:CMTimeMakeWithSeconds(posEvent.positionTime, NSEC_PER_SEC)]; + return MPRemoteCommandHandlerStatusSuccess; + }]; + + // Update now playing info periodically + __weak typeof(self) weakSelf = self; + _timeObserver = + [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) + queue:dispatch_get_main_queue() + usingBlock:^(CMTime time) { + [weakSelf updateNowPlayingInfo]; + }]; + + [self updateNowPlayingInfo]; +#endif +} + +- (void)disable { +#if TARGET_OS_IOS + _isEnabled = NO; + + [self removeAppLifecycleObservers]; + [self removeCommandTargets]; + [self endBackgroundTask]; + + if (_timeObserver) { + [_player removeTimeObserver:_timeObserver]; + _timeObserver = nil; + } + + [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nil; + [[UIApplication sharedApplication] endReceivingRemoteControlEvents]; +#endif +} + +#if TARGET_OS_IOS + +#pragma mark - App Lifecycle + +- (void)handleEnterBackground:(NSNotification *)notification { + if (!_isEnabled) return; + + NSLog(@"video_player: [BG] App entered background — player.rate=%f, starting background task", _player.rate); + + // Start a background task to buy time for the audio session to take over. + // Without this, iOS may suspend the process before AVPlayer establishes + // its background audio rendering pipeline. + [self endBackgroundTask]; + _backgroundTask = [[UIApplication sharedApplication] + beginBackgroundTaskWithExpirationHandler:^{ + NSLog(@"video_player: [BG] Background task expired"); + [self endBackgroundTask]; + }]; + + // Re-assert playback. When the app transitions to background, the system may + // momentarily pause the AVPlayer. Calling play again ensures the audio + // rendering pipeline stays active, which is what tells iOS to keep the app alive. + if (_player.rate == 0 && _player.currentItem) { + NSLog(@"video_player: [BG] Player was paused, re-starting playback"); + [_player play]; + } + + // Schedule a follow-up to ensure playback is still active after the transition settles. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + if (self->_isEnabled && self->_player.rate == 0 && self->_player.currentItem) { + NSLog(@"video_player: [BG] Player still paused after 0.5s, re-starting"); + [self->_player play]; + } + NSLog(@"video_player: [BG] Background settled — player.rate=%f", self->_player.rate); + }); +} + +- (void)handleEnterForeground:(NSNotification *)notification { + if (!_isEnabled) return; + + NSLog(@"video_player: [BG] App entering foreground — player.rate=%f", _player.rate); + [self endBackgroundTask]; +} + +- (void)handleInterruption:(NSNotification *)notification { + if (!_isEnabled) return; + + NSDictionary *info = notification.userInfo; + AVAudioSessionInterruptionType type = + [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; + + if (type == AVAudioSessionInterruptionTypeBegan) { + NSLog(@"video_player: [BG] Audio session interrupted (began)"); + } else if (type == AVAudioSessionInterruptionTypeEnded) { + NSLog(@"video_player: [BG] Audio session interruption ended, resuming playback"); + AVAudioSessionInterruptionOptions options = + [info[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue]; + if (options & AVAudioSessionInterruptionOptionShouldResume) { + [_player play]; + } + } +} + +- (void)endBackgroundTask { + if (_backgroundTask != UIBackgroundTaskInvalid) { + [[UIApplication sharedApplication] endBackgroundTask:_backgroundTask]; + _backgroundTask = UIBackgroundTaskInvalid; + } +} + +- (void)removeAppLifecycleObservers { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationWillEnterForegroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVAudioSessionInterruptionNotification + object:nil]; +} + +#pragma mark - Remote Command Targets + +- (void)removeCommandTargets { + MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; + if (_playTarget) { + [commandCenter.playCommand removeTarget:_playTarget]; + _playTarget = nil; + } + if (_pauseTarget) { + [commandCenter.pauseCommand removeTarget:_pauseTarget]; + _pauseTarget = nil; + } + if (_seekTarget) { + [commandCenter.changePlaybackPositionCommand removeTarget:_seekTarget]; + _seekTarget = nil; + } +} + +- (void)loadArtworkFromUrl:(NSString *)urlString { + NSURL *url = [NSURL URLWithString:urlString]; + if (!url) return; + + __weak typeof(self) weakSelf = self; + NSURLSessionDataTask *task = [[NSURLSession sharedSession] + dataTaskWithURL:url + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error || !data) { + NSLog(@"video_player: [BG] Failed to load artwork: %@", error); + return; + } + UIImage *image = [UIImage imageWithData:data]; + if (!image) return; + + dispatch_async(dispatch_get_main_queue(), ^{ + typeof(self) strongSelf = weakSelf; + if (!strongSelf || !strongSelf->_isEnabled) return; + // Only apply if the URL hasn't changed since the request started. + if (![strongSelf->_artworkUrl isEqualToString:urlString]) return; + strongSelf->_cachedArtwork = [[MPMediaItemArtwork alloc] + initWithBoundsSize:image.size + requestHandler:^UIImage *(CGSize size) { + return image; + }]; + [strongSelf updateNowPlayingInfo]; + }); + }]; + [task resume]; +} +#endif + +- (void)updateNowPlayingInfo { +#if TARGET_OS_IOS + if (!_isEnabled) return; + if (!_player.currentItem) return; + + NSMutableDictionary *info = [NSMutableDictionary dictionary]; + info[MPMediaItemPropertyTitle] = _title ?: @"Video"; + if (_artist) { + info[MPMediaItemPropertyArtist] = _artist; + } + + if (!_cachedDuration) { + CMTime duration = _player.currentItem.asset.duration; + if (CMTIME_IS_VALID(duration) && !CMTIME_IS_INDEFINITE(duration)) { + _cachedDuration = @(CMTimeGetSeconds(duration)); + } + } + if (_cachedDuration) { + info[MPMediaItemPropertyPlaybackDuration] = _cachedDuration; + } + + if (_cachedArtwork) { + info[MPMediaItemPropertyArtwork] = _cachedArtwork; + } + + info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(CMTimeGetSeconds(_player.currentTime)); + info[MPNowPlayingInfoPropertyPlaybackRate] = @(_player.rate); + + [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = info; +#endif +} + +- (void)dealloc { + [self disable]; +} + +@end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPEventBridge.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPEventBridge.m index 0df3569da9bd..c43fb73b1986 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPEventBridge.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPEventBridge.m @@ -103,6 +103,21 @@ - (void)videoPlayerDidSetPlaying:(BOOL)playing { [self sendOrQueue:@{@"event" : @"isPlayingStateUpdate", @"isPlaying" : @(playing)}]; } +- (void)videoPlayerDidChangePipState:(BOOL)isPipActive { + [self sendOrQueue:@{@"event" : @"pipStateChanged", @"isPipActive" : @(isPipActive)}]; +} + +- (void)videoPlayerDidChangeQualityWithWidth:(NSInteger)width + height:(NSInteger)height + bitrate:(NSInteger)bitrate { + [self sendOrQueue:@{ + @"event" : @"qualityChanged", + @"width" : @(width), + @"height" : @(height), + @"bitrate" : @(bitrate), + }]; +} + - (void)videoPlayerWasDisposed { [self.eventChannel setStreamHandler:nil]; } diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPPipController.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPPipController.m new file mode 100644 index 000000000000..dc58aeaea0e1 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPPipController.m @@ -0,0 +1,156 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "./include/video_player_avfoundation/FVPPipController.h" + +static void *pipPossibleContext = &pipPossibleContext; + +@implementation FVPPipController { + AVPictureInPictureController *_pipController; + AVPlayerLayer *_playerLayer; + BOOL _pendingStart; + BOOL _observingPossible; + BOOL _manualStart; +} + ++ (BOOL)isPipSupported { +#if TARGET_OS_IOS + if (@available(iOS 15.0, *)) { + return [AVPictureInPictureController isPictureInPictureSupported]; + } + return NO; +#elif TARGET_OS_OSX + return NO; +#else + return NO; +#endif +} + +- (instancetype)initWithPlayerLayer:(AVPlayerLayer *)playerLayer { + self = [super init]; + if (self) { + _playerLayer = playerLayer; +#if TARGET_OS_IOS + if ([AVPictureInPictureController isPictureInPictureSupported]) { + _pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:playerLayer]; + _pipController.delegate = self; + [_pipController addObserver:self + forKeyPath:@"pictureInPicturePossible" + options:NSKeyValueObservingOptionNew + context:pipPossibleContext]; + _observingPossible = YES; + } +#endif + } + return self; +} + +- (void)dealloc { + if (_observingPossible) { + [_pipController removeObserver:self forKeyPath:@"pictureInPicturePossible" context:pipPossibleContext]; + } +} + +- (BOOL)isPipActive { +#if TARGET_OS_IOS + return _pipController.isPictureInPictureActive; +#else + return NO; +#endif +} + +- (void)startPip { +#if TARGET_OS_IOS + if (_pipController && !_pipController.isPictureInPictureActive) { + _manualStart = YES; + if (_pipController.isPictureInPicturePossible) { + [_pipController startPictureInPicture]; + } else { + _pendingStart = YES; + } + } +#endif +} + +- (void)stopPip { +#if TARGET_OS_IOS + _pendingStart = NO; + _manualStart = NO; + if (_pipController && _pipController.isPictureInPictureActive) { + [_pipController stopPictureInPicture]; + } +#endif +} + +- (void)setCanStartAutomatically:(BOOL)canStart { +#if TARGET_OS_IOS + if (@available(iOS 14.2, *)) { + _pipController.canStartPictureInPictureAutomaticallyFromInline = canStart; + } +#endif +} + +#pragma mark - KVO + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if (context == pipPossibleContext) { + if (_pendingStart && _pipController.isPictureInPicturePossible) { + _pendingStart = NO; + [_pipController startPictureInPicture]; + } + } +} + +#pragma mark - AVPictureInPictureControllerDelegate + +- (void)pictureInPictureControllerWillStartPictureInPicture: + (AVPictureInPictureController *)pictureInPictureController { + // Will start +} + +- (void)pictureInPictureControllerDidStartPictureInPicture: + (AVPictureInPictureController *)pictureInPictureController { +#if TARGET_OS_IOS + if (_manualStart) { + _manualStart = NO; + // Move the app to background so PiP floats over the home screen, + // matching the Android PiP behavior. + SEL suspendSel = NSSelectorFromString(@"suspend"); + if ([[UIApplication sharedApplication] respondsToSelector:suspendSel]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [[UIApplication sharedApplication] performSelector:suspendSel]; +#pragma clang diagnostic pop + } + } +#endif + [self.delegate pipControllerDidStartPip]; +} + +- (void)pictureInPictureControllerWillStopPictureInPicture: + (AVPictureInPictureController *)pictureInPictureController { + // Will stop +} + +- (void)pictureInPictureControllerDidStopPictureInPicture: + (AVPictureInPictureController *)pictureInPictureController { + [self.delegate pipControllerDidStopPip]; +} + +- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController + failedToStartPictureInPictureWithError:(NSError *)error { + _manualStart = NO; + [self.delegate pipControllerFailedToStartWithError:error]; +} + +- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController + restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: + (void (^)(BOOL))completionHandler { + completionHandler(YES); +} + +@end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m index 1419da4d7743..cbc863fc49cc 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m @@ -53,13 +53,15 @@ - (instancetype)initWithPlayerItem:(NSObject *)item // video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An // invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams // for issue #1, and restore the correct width and height for issue #2. - _playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; #if TARGET_OS_IOS CALayer *flutterLayer = viewProvider.viewController.view.layer; #else CALayer *flutterLayer = viewProvider.view.layer; #endif [flutterLayer addSublayer:self.playerLayer]; + // A non-zero frame is required for AVPictureInPictureController to accept the layer. + // PiP uses the video's natural size from AVPlayerItem, not these bounds. + self.playerLayer.frame = CGRectMake(0, 0, 1, 1); } return self; } diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 2270120378d5..5b0f468e9b9f 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -8,6 +8,8 @@ #import #import "./include/video_player_avfoundation/AVAssetTrackUtils.h" +#import "./include/video_player_avfoundation/FVPBackgroundAudioHandler.h" +#import "./include/video_player_avfoundation/FVPPipController.h" static void *timeRangeContext = &timeRangeContext; static void *statusContext = &statusContext; @@ -69,6 +71,18 @@ static void FVPRemoveKeyValueObservers(NSObject *observer, @implementation FVPVideoPlayer { // Whether or not player and player item listeners have ever been registered. BOOL _listenersRegistered; + // The last known indicated bitrate from the access log, used to detect ABR quality changes. + double _lastIndicatedBitrate; +} + +@synthesize playerLayer = _playerLayer; + +// Lazily create the player layer only when PiP support is actually needed. +- (AVPlayerLayer *)playerLayer { + if (!_playerLayer) { + _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; + } + return _playerLayer; } - (instancetype)initWithPlayerItem:(NSObject *)item @@ -171,6 +185,18 @@ - (void)disposeWithError:(FlutterError *_Nullable *_Nonnull)error { FVPRemoveKeyValueObservers(self, FVPGetPlayerObservations(), self.player); } + // Clean up PiP controller. + if (_pipController) { + [_pipController stopPip]; + _pipController = nil; + } + + // Clean up background audio handler. + if (_backgroundAudioHandler) { + [_backgroundAudioHandler disable]; + _backgroundAudioHandler = nil; + } + [self.player replaceCurrentItemWithPlayerItem:nil]; if (_onDisposed) { @@ -194,6 +220,10 @@ - (void)setEventListener:(NSObject *)eventListener { selector:@selector(itemDidPlayToEndTime:) name:AVPlayerItemDidPlayToEndTimeNotification object:item]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(accessLogEntryAdded:) + name:AVPlayerItemNewAccessLogEntryNotification + object:item]; _listenersRegistered = YES; } } @@ -207,6 +237,67 @@ - (void)itemDidPlayToEndTime:(NSNotification *)notification { } } +- (void)accessLogEntryAdded:(NSNotification *)notification { + AVPlayerItem *item = (AVPlayerItem *)notification.object; + AVPlayerItemAccessLog *accessLog = item.accessLog; + NSArray *events = accessLog.events; + AVPlayerItemAccessLogEvent *lastEvent = events.lastObject; + + NSLog(@"[ABR] accessLogEntryAdded: totalEntries=%lu lastEvent=%@", + (unsigned long)events.count, lastEvent ? @"present" : @"nil"); + + if (!lastEvent) { + return; + } + + double indicatedBitrate = lastEvent.indicatedBitrate; + double observedBitrate = lastEvent.observedBitrate; + NSLog(@"[ABR] indicatedBitrate=%.0f observedBitrate=%.0f lastIndicatedBitrate=%.0f " + @"switchBitrate=%.0f", + indicatedBitrate, observedBitrate, _lastIndicatedBitrate, + lastEvent.switchBitrate); + + // Only emit event when the bitrate actually changes (ABR switch). + if (indicatedBitrate > 0 && indicatedBitrate != _lastIndicatedBitrate) { + _lastIndicatedBitrate = indicatedBitrate; + + // Look up the variant resolution by matching indicatedBitrate to asset variants. + // presentationSize is the render/display size and may not match the variant resolution. + NSInteger width = 0; + NSInteger height = 0; + if (@available(iOS 15.0, macOS 12.0, *)) { + AVURLAsset *urlAsset = (AVURLAsset *)item.asset; + if ([urlAsset isKindOfClass:[AVURLAsset class]]) { + double closestDelta = INFINITY; + for (AVAssetVariant *variant in urlAsset.variants) { + if (variant.videoAttributes) { + double delta = fabs(variant.peakBitRate - indicatedBitrate); + if (delta < closestDelta) { + closestDelta = delta; + CGSize res = variant.videoAttributes.presentationSize; + width = (NSInteger)res.width; + height = (NSInteger)res.height; + } + } + } + } + } + // Fallback to presentationSize if variant lookup didn't find a match. + if (width == 0 || height == 0) { + width = (NSInteger)item.presentationSize.width; + height = (NSInteger)item.presentationSize.height; + } + + NSLog(@"[ABR] Dispatching quality event: %ldx%ld @ %.0f bps", + (long)width, (long)height, indicatedBitrate); + [self.eventListener videoPlayerDidChangeQualityWithWidth:width + height:height + bitrate:(NSInteger)indicatedBitrate]; + } else { + NSLog(@"[ABR] Skipped: indicatedBitrate=%.0f (same as last or <= 0)", indicatedBitrate); + } +} + const int64_t TIME_UNSET = -9223372036854775807; NS_INLINE int64_t FVPCMTimeToMillis(CMTime time) { @@ -508,6 +599,115 @@ - (void)selectAudioTrackAtIndex:(NSInteger)trackIndex } } +#pragma mark - ABR (Adaptive Bitrate) Control + +- (nullable NSArray *)getAvailableQualities: + (FlutterError *_Nullable *_Nonnull)error { + NSMutableArray *qualities = [[NSMutableArray alloc] init]; + + if (@available(iOS 15.0, macOS 12.0, *)) { + AVURLAsset *urlAsset = (AVURLAsset *)_player.currentItem.asset; + if ([urlAsset isKindOfClass:[AVURLAsset class]] && + [urlAsset respondsToSelector:@selector(variants)]) { + NSArray *variants = urlAsset.variants; + for (AVAssetVariant *variant in variants) { + // Only include variants with video attributes. + if (variant.videoAttributes) { + CGSize resolution = variant.videoAttributes.presentationSize; + double peakBitRate = variant.peakBitRate; + FVPPlatformVideoQuality *quality = [FVPPlatformVideoQuality + makeWithWidth:(NSInteger)resolution.width + height:(NSInteger)resolution.height + bitrate:(NSInteger)peakBitRate + codec:nil + isSelected:NO]; + [qualities addObject:quality]; + } + } + } + } + // On older iOS, return empty list — no API to enumerate HLS variants. + return qualities; +} + +- (nullable FVPPlatformVideoQuality *)getCurrentQuality: + (FlutterError *_Nullable *_Nonnull)error { + AVPlayerItemAccessLog *accessLog = _player.currentItem.accessLog; + AVPlayerItemAccessLogEvent *lastEvent = accessLog.events.lastObject; + if (!lastEvent) { + return nil; + } + + double bitrate = lastEvent.indicatedBitrate; + NSInteger width = 0; + NSInteger height = 0; + + // Look up the variant resolution by matching indicatedBitrate. + if (@available(iOS 15.0, macOS 12.0, *)) { + AVURLAsset *urlAsset = (AVURLAsset *)_player.currentItem.asset; + if ([urlAsset isKindOfClass:[AVURLAsset class]]) { + double closestDelta = INFINITY; + for (AVAssetVariant *variant in urlAsset.variants) { + if (variant.videoAttributes) { + double delta = fabs(variant.peakBitRate - bitrate); + if (delta < closestDelta) { + closestDelta = delta; + CGSize res = variant.videoAttributes.presentationSize; + width = (NSInteger)res.width; + height = (NSInteger)res.height; + } + } + } + } + } + if (width == 0 || height == 0) { + width = (NSInteger)_player.currentItem.presentationSize.width; + height = (NSInteger)_player.currentItem.presentationSize.height; + } + + FVPPlatformVideoQuality *quality = [FVPPlatformVideoQuality makeWithWidth:width + height:height + bitrate:(NSInteger)bitrate + codec:nil + isSelected:YES]; + return quality; +} + +- (void)setMaxBitrate:(NSInteger)maxBitrateBps + error:(FlutterError *_Nullable *_Nonnull)error { + NSLog(@"[ABR] setMaxBitrate: %ld bps (resetting lastIndicatedBitrate from %.0f)", + (long)maxBitrateBps, _lastIndicatedBitrate); + // Reset so the next access log entry always triggers an event, + // even if the bitrate matches a previously seen value (e.g. A→B→A). + _lastIndicatedBitrate = 0; + _player.currentItem.preferredPeakBitRate = (double)maxBitrateBps; +} + +- (void)setMaxResolutionWidth:(NSInteger)width + height:(NSInteger)height + error:(FlutterError *_Nullable *_Nonnull)error { + NSLog(@"[ABR] setMaxResolution: %ldx%ld (resetting lastIndicatedBitrate from %.0f)", + (long)width, (long)height, _lastIndicatedBitrate); + _lastIndicatedBitrate = 0; + if (@available(iOS 11.0, macOS 10.13, *)) { + _player.currentItem.preferredMaximumResolution = CGSizeMake(width, height); + } +} + +#pragma mark - FVPPipControllerDelegate + +- (void)pipControllerDidStartPip { + [self.eventListener videoPlayerDidChangePipState:YES]; +} + +- (void)pipControllerDidStopPip { + [self.eventListener videoPlayerDidChangePipState:NO]; +} + +- (void)pipControllerFailedToStartWithError:(NSError *)error { + NSLog(@"PiP failed to start: %@", error.localizedDescription); +} + #pragma mark - Private - (int64_t)duration { diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m index a420e8397401..ead605351ddc 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m @@ -9,10 +9,12 @@ #import "./include/video_player_avfoundation/FVPAVFactory.h" #import "./include/video_player_avfoundation/FVPAssetProvider.h" +#import "./include/video_player_avfoundation/FVPBackgroundAudioHandler.h" #import "./include/video_player_avfoundation/FVPDisplayLink.h" #import "./include/video_player_avfoundation/FVPEventBridge.h" #import "./include/video_player_avfoundation/FVPFrameUpdater.h" #import "./include/video_player_avfoundation/FVPNativeVideoViewFactory.h" +#import "./include/video_player_avfoundation/FVPPipController.h" #import "./include/video_player_avfoundation/FVPTextureBasedVideoPlayer.h" #import "./include/video_player_avfoundation/FVPVideoPlayer.h" // Relative path is needed for messages.g.h. See @@ -159,6 +161,17 @@ - (int64_t)configurePlayer:(FVPVideoPlayer *)player channelSuffix]]; player.eventListener = eventBridge; + // Eagerly create PiP controller so it's ready when enterPip is called. + // AVPictureInPictureController needs time for isPictureInPicturePossible to + // become YES. Creating it lazily in enterPip causes the first tap to fail + // because startPictureInPicture must be called from a user action context. +#if TARGET_OS_IOS + if ([FVPPipController isPipSupported] && !player.pipController) { + player.pipController = [[FVPPipController alloc] initWithPlayerLayer:player.playerLayer]; + player.pipController.delegate = player; + } +#endif + return playerIdentifier; } @@ -303,6 +316,109 @@ - (nullable NSString *)fileURLForAssetWithName:(NSString *)asset return [NSURL fileURLWithPath:path].absoluteString; } +- (nullable FVPVideoPlayer *)playerForId:(NSInteger)playerId + error:(FlutterError *_Nullable *_Nonnull)error { + FVPVideoPlayer *player = self.playersByIdentifier[@(playerId)]; + if (!player) { + *error = [FlutterError errorWithCode:@"video_player" message:@"Player not found" details:nil]; + } + return player; +} + +- (nullable NSNumber *)isPipSupported:(FlutterError *_Nullable *_Nonnull)error { + return @([FVPPipController isPipSupported]); +} + +- (void)enterPipForPlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error { + FVPVideoPlayer *player = [self playerForId:playerId error:error]; + if (!player) return; + if (!player.pipController) { + player.pipController = [[FVPPipController alloc] initWithPlayerLayer:player.playerLayer]; + player.pipController.delegate = player; + } + [player.pipController startPip]; +} + +- (void)exitPipForPlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error { + FVPVideoPlayer *player = [self playerForId:playerId error:error]; + if (!player) return; + [player.pipController stopPip]; +} + +- (nullable NSNumber *)isPipActiveForPlayer:(NSInteger)playerId + error:(FlutterError *_Nullable *_Nonnull)error { + FVPVideoPlayer *player = [self playerForId:playerId error:error]; + if (!player) return nil; + return @(player.pipController.isPipActive); +} + +- (void)enableBackgroundPlaybackForPlayer:(NSInteger)playerId + mediaInfo:(nullable FVPPlatformMediaInfo *)mediaInfo + error:(FlutterError *_Nullable *_Nonnull)error { + NSLog(@"video_player: [BG-PLUGIN] enableBackgroundPlayback called for player %ld", (long)playerId); + FVPVideoPlayer *player = [self playerForId:playerId error:error]; + if (!player) { + NSLog(@"video_player: [BG-PLUGIN] ERROR: player %ld not found!", (long)playerId); + return; + } + NSLog(@"video_player: [BG-PLUGIN] player found, rate=%f, currentItem=%@", + player.player.rate, player.player.currentItem); + if (!player.backgroundAudioHandler) { + player.backgroundAudioHandler = + [[FVPBackgroundAudioHandler alloc] initWithPlayer:player.player]; + NSLog(@"video_player: [BG-PLUGIN] Created new FVPBackgroundAudioHandler"); + } + [player.backgroundAudioHandler enableWithTitle:mediaInfo.title + artist:mediaInfo.artist + artworkUrl:mediaInfo.artworkUrl + durationMs:mediaInfo.durationMs]; + NSLog(@"video_player: [BG-PLUGIN] enableWithTitle completed, handler.isEnabled=%d", + player.backgroundAudioHandler.isEnabled); +} + +- (void)disableBackgroundPlaybackForPlayer:(NSInteger)playerId + error:(FlutterError *_Nullable *_Nonnull)error { + FVPVideoPlayer *player = [self playerForId:playerId error:error]; + if (!player) return; + [player.backgroundAudioHandler disable]; +} + +- (void)setAutoPipForPlayer:(NSInteger)playerId + enabled:(BOOL)enabled + error:(FlutterError *_Nullable *_Nonnull)error { + FVPVideoPlayer *player = [self playerForId:playerId error:error]; + if (!player) return; + if (!player.pipController) { + player.pipController = [[FVPPipController alloc] initWithPlayerLayer:player.playerLayer]; + player.pipController.delegate = player; + } + [player.pipController setCanStartAutomatically:enabled]; +} + +// Cache control methods — no-ops on iOS until the HLS reverse-proxy cache phase. + +- (void)setCacheMaxSize:(NSInteger)maxSizeBytes + error:(FlutterError *_Nullable *_Nonnull)error { + // No-op on iOS. Store value for future use. +} + +- (void)clearCache:(FlutterError *_Nullable *_Nonnull)error { + // No-op on iOS. +} + +- (nullable NSNumber *)getCacheSize:(FlutterError *_Nullable *_Nonnull)error { + return @(0); +} + +- (nullable NSNumber *)isCacheEnabled:(FlutterError *_Nullable *_Nonnull)error { + return @(NO); +} + +- (void)setCacheEnabled:(BOOL)enabled + error:(FlutterError *_Nullable *_Nonnull)error { + // No-op on iOS. +} + /// Returns the AVPlayerItem corresponding to the given player creation options. - (nonnull NSObject *)playerItemWithCreationOptions: (nonnull FVPCreationOptions *)options { diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPBackgroundAudioHandler.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPBackgroundAudioHandler.h new file mode 100644 index 000000000000..e8be6a3edd83 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPBackgroundAudioHandler.h @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import AVFoundation; + +#if TARGET_OS_OSX +@import FlutterMacOS; +#else +@import Flutter; +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface FVPBackgroundAudioHandler : NSObject +@property(nonatomic, readonly) BOOL isEnabled; + +- (instancetype)initWithPlayer:(AVPlayer *)player; +- (void)enableWithTitle:(nullable NSString *)title + artist:(nullable NSString *)artist + artworkUrl:(nullable NSString *)artworkUrl + durationMs:(nullable NSNumber *)durationMs; +- (void)disable; +- (void)updateNowPlayingInfo; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPPipController.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPPipController.h new file mode 100644 index 000000000000..89ccd8bfd439 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPPipController.h @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import AVFoundation; +@import AVKit; + +#if TARGET_OS_OSX +@import FlutterMacOS; +#else +@import Flutter; +#endif + +NS_ASSUME_NONNULL_BEGIN + +@protocol FVPPipControllerDelegate +- (void)pipControllerDidStartPip; +- (void)pipControllerDidStopPip; +- (void)pipControllerFailedToStartWithError:(NSError *)error; +@end + +@interface FVPPipController : NSObject +@property(nonatomic, weak, nullable) id delegate; +@property(nonatomic, readonly) BOOL isPipActive; + ++ (BOOL)isPipSupported; +- (instancetype)initWithPlayerLayer:(AVPlayerLayer *)playerLayer; +- (void)startPip; +- (void)stopPip; +- (void)setCanStartAutomatically:(BOOL)canStart; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h index cb51d28b542e..c24450f1c6e4 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h @@ -13,14 +13,6 @@ NS_ASSUME_NONNULL_BEGIN @interface FVPTextureBasedVideoPlayer () -/// The AVPlayerLayer used to display the video content. -/// This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 -/// (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some -/// video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An -/// invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams -/// for issue #1, and restore the correct width and height for issue #2. -@property(readonly, nonatomic) AVPlayerLayer *playerLayer; - /// Called when the texture is unregistered. /// This method is used to clean up resources associated with the texture. - (void)onTextureUnregistered:(NSObject *)texture; diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoEventListener.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoEventListener.h index a267adbb902b..4d63bcfe0114 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoEventListener.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoEventListener.h @@ -27,6 +27,12 @@ - (void)videoPlayerDidUpdateBufferRegions:(NSArray *> *)regions; /// Called when the player starts or stops playing. - (void)videoPlayerDidSetPlaying:(BOOL)playing; +/// Called when PiP state changes. +- (void)videoPlayerDidChangePipState:(BOOL)isPipActive; +/// Called when the video quality changes (ABR switch). +- (void)videoPlayerDidChangeQualityWithWidth:(NSInteger)width + height:(NSInteger)height + bitrate:(NSInteger)bitrate; /// Called when the video player has been disposed on the Dart side. - (void)videoPlayerWasDisposed; @end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h index 02954d7a3680..737addc54f85 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h @@ -15,6 +15,10 @@ @import Flutter; #endif +#import "FVPPipController.h" + +@class FVPBackgroundAudioHandler; + NS_ASSUME_NONNULL_BEGIN /// FVPVideoPlayer manages video playback using AVPlayer. @@ -22,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN /// This class contains all functionalities needed to manage video playback in platform views and is /// typically used alongside FVPNativeVideoViewFactory. If you need to display a video using a /// texture, use FVPTextureBasedVideoPlayer instead. -@interface FVPVideoPlayer : NSObject +@interface FVPVideoPlayer : NSObject /// The AVPlayer instance used for video playback. @property(nonatomic, readonly) AVPlayer *player; /// Indicates whether the video player has been disposed. @@ -35,6 +39,12 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, nullable) NSObject *eventListener; /// A block that will be called when dispose is called. @property(nonatomic, nullable, copy) void (^onDisposed)(void); +/// The PiP controller for Picture-in-Picture support. +@property(nonatomic, strong, nullable) FVPPipController *pipController; +/// The background audio handler for background playback support. +@property(nonatomic, strong, nullable) FVPBackgroundAudioHandler *backgroundAudioHandler; +/// The AVPlayerLayer used for rendering and PiP support. Created lazily on first access. +@property(nonatomic, strong, readonly) AVPlayerLayer *playerLayer; /// Initializes a new instance of FVPVideoPlayer with the given AVPlayerItem, AV factory, and view /// provider. diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h index 3b2dd3952245..58787a5bbb1d 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// Autogenerated from Pigeon (v26.1.10), do not edit directly. // See also: https://pub.dev/packages/pigeon @import Foundation; @@ -17,46 +17,78 @@ NS_ASSUME_NONNULL_BEGIN @class FVPCreationOptions; @class FVPTexturePlayerIds; @class FVPMediaSelectionAudioTrackData; +@class FVPPlatformMediaInfo; +@class FVPPlatformVideoQuality; /// Information passed to the platform view creation. @interface FVPPlatformVideoViewCreationParams : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger)playerId; -@property(nonatomic, assign) NSInteger playerId; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId; +@property(nonatomic, assign) NSInteger playerId; @end @interface FVPCreationOptions : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders; -@property(nonatomic, copy) NSString *uri; -@property(nonatomic, copy) NSDictionary *httpHeaders; + httpHeaders:(NSDictionary *)httpHeaders; +@property(nonatomic, copy) NSString * uri; +@property(nonatomic, copy) NSDictionary * httpHeaders; @end @interface FVPTexturePlayerIds : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId; -@property(nonatomic, assign) NSInteger playerId; -@property(nonatomic, assign) NSInteger textureId; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId + textureId:(NSInteger )textureId; +@property(nonatomic, assign) NSInteger playerId; +@property(nonatomic, assign) NSInteger textureId; @end /// Raw audio track data from AVMediaSelectionOption (for HLS streams). @interface FVPMediaSelectionAudioTrackData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithIndex:(NSInteger)index - displayName:(nullable NSString *)displayName - languageCode:(nullable NSString *)languageCode - isSelected:(BOOL)isSelected - commonMetadataTitle:(nullable NSString *)commonMetadataTitle; -@property(nonatomic, assign) NSInteger index; -@property(nonatomic, copy, nullable) NSString *displayName; -@property(nonatomic, copy, nullable) NSString *languageCode; -@property(nonatomic, assign) BOOL isSelected; -@property(nonatomic, copy, nullable) NSString *commonMetadataTitle; ++ (instancetype)makeWithIndex:(NSInteger )index + displayName:(nullable NSString *)displayName + languageCode:(nullable NSString *)languageCode + isSelected:(BOOL )isSelected + commonMetadataTitle:(nullable NSString *)commonMetadataTitle; +@property(nonatomic, assign) NSInteger index; +@property(nonatomic, copy, nullable) NSString * displayName; +@property(nonatomic, copy, nullable) NSString * languageCode; +@property(nonatomic, assign) BOOL isSelected; +@property(nonatomic, copy, nullable) NSString * commonMetadataTitle; +@end + +@interface FVPPlatformMediaInfo : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTitle:(NSString *)title + artist:(nullable NSString *)artist + artworkUrl:(nullable NSString *)artworkUrl + durationMs:(nullable NSNumber *)durationMs; +@property(nonatomic, copy) NSString * title; +@property(nonatomic, copy, nullable) NSString * artist; +@property(nonatomic, copy, nullable) NSString * artworkUrl; +@property(nonatomic, strong, nullable) NSNumber * durationMs; +@end + +/// Represents a video quality variant (resolution/bitrate combination). +@interface FVPPlatformVideoQuality : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithWidth:(NSInteger )width + height:(NSInteger )height + bitrate:(NSInteger )bitrate + codec:(nullable NSString *)codec + isSelected:(BOOL )isSelected; +@property(nonatomic, assign) NSInteger width; +@property(nonatomic, assign) NSInteger height; +@property(nonatomic, assign) NSInteger bitrate; +@property(nonatomic, copy, nullable) NSString * codec; +@property(nonatomic, assign) BOOL isSelected; @end /// The codec used by all APIs. @@ -65,25 +97,33 @@ NSObject *FVPGetMessagesCodec(void); @protocol FVPAVFoundationVideoPlayerApi - (void)initialize:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params error:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable FVPTexturePlayerIds *) - createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable FVPTexturePlayerIds *)createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions error:(FlutterError *_Nullable *_Nonnull)error; - (void)setMixWithOthers:(BOOL)mixWithOthers error:(FlutterError *_Nullable *_Nonnull)error; -- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset - package:(nullable NSString *)package - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset package:(nullable NSString *)package error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)isPipSupported:(FlutterError *_Nullable *_Nonnull)error; +- (void)enterPipForPlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error; +- (void)exitPipForPlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)isPipActiveForPlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error; +- (void)enableBackgroundPlaybackForPlayer:(NSInteger)playerId mediaInfo:(nullable FVPPlatformMediaInfo *)mediaInfo error:(FlutterError *_Nullable *_Nonnull)error; +- (void)disableBackgroundPlaybackForPlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setAutoPipForPlayer:(NSInteger)playerId enabled:(BOOL)enabled error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setCacheMaxSize:(NSInteger)maxSizeBytes error:(FlutterError *_Nullable *_Nonnull)error; +- (void)clearCache:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)getCacheSize:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)isCacheEnabled:(FlutterError *_Nullable *_Nonnull)error; +- (void)setCacheEnabled:(BOOL)enabled error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void SetUpFVPAVFoundationVideoPlayerApi( - id binaryMessenger, - NSObject *_Nullable api); +extern void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *_Nullable api); + +extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); -extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix( - id binaryMessenger, - NSObject *_Nullable api, NSString *messageChannelSuffix); @protocol FVPVideoPlayerInstanceApi - (void)setLooping:(BOOL)looping error:(FlutterError *_Nullable *_Nonnull)error; @@ -96,17 +136,17 @@ extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix( - (void)pauseWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)disposeWithError:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable NSArray *)getAudioTracks: - (FlutterError *_Nullable *_Nonnull)error; -- (void)selectAudioTrackAtIndex:(NSInteger)trackIndex - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSArray *)getAudioTracks:(FlutterError *_Nullable *_Nonnull)error; +- (void)selectAudioTrackAtIndex:(NSInteger)trackIndex error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSArray *)getAvailableQualities:(FlutterError *_Nullable *_Nonnull)error; +- (nullable FVPPlatformVideoQuality *)getCurrentQuality:(FlutterError *_Nullable *_Nonnull)error; +- (void)setMaxBitrate:(NSInteger)maxBitrateBps error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setMaxResolutionWidth:(NSInteger)width height:(NSInteger)height error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, - NSObject *_Nullable api); +extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *_Nullable api); -extern void SetUpFVPVideoPlayerInstanceApiWithSuffix( - id binaryMessenger, NSObject *_Nullable api, - NSString *messageChannelSuffix); +extern void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m index abb8efbad50d..b624639736fb 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// Autogenerated from Pigeon (v26.1.10), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "./include/video_player_avfoundation/messages.g.h" @@ -50,16 +50,26 @@ + (nullable FVPMediaSelectionAudioTrackData *)nullableFromList:(NSArray *)li - (NSArray *)toList; @end +@interface FVPPlatformMediaInfo () ++ (FVPPlatformMediaInfo *)fromList:(NSArray *)list; ++ (nullable FVPPlatformMediaInfo *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FVPPlatformVideoQuality () ++ (FVPPlatformVideoQuality *)fromList:(NSArray *)list; ++ (nullable FVPPlatformVideoQuality *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @implementation FVPPlatformVideoViewCreationParams -+ (instancetype)makeWithPlayerId:(NSInteger)playerId { - FVPPlatformVideoViewCreationParams *pigeonResult = - [[FVPPlatformVideoViewCreationParams alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId { + FVPPlatformVideoViewCreationParams* pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = playerId; return pigeonResult; } + (FVPPlatformVideoViewCreationParams *)fromList:(NSArray *)list { - FVPPlatformVideoViewCreationParams *pigeonResult = - [[FVPPlatformVideoViewCreationParams alloc] init]; + FVPPlatformVideoViewCreationParams *pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } @@ -75,8 +85,8 @@ + (nullable FVPPlatformVideoViewCreationParams *)nullableFromList:(NSArray * @implementation FVPCreationOptions + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders { - FVPCreationOptions *pigeonResult = [[FVPCreationOptions alloc] init]; + httpHeaders:(NSDictionary *)httpHeaders { + FVPCreationOptions* pigeonResult = [[FVPCreationOptions alloc] init]; pigeonResult.uri = uri; pigeonResult.httpHeaders = httpHeaders; return pigeonResult; @@ -99,8 +109,9 @@ + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list { @end @implementation FVPTexturePlayerIds -+ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId { - FVPTexturePlayerIds *pigeonResult = [[FVPTexturePlayerIds alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId + textureId:(NSInteger )textureId { + FVPTexturePlayerIds* pigeonResult = [[FVPTexturePlayerIds alloc] init]; pigeonResult.playerId = playerId; pigeonResult.textureId = textureId; return pigeonResult; @@ -123,12 +134,12 @@ + (nullable FVPTexturePlayerIds *)nullableFromList:(NSArray *)list { @end @implementation FVPMediaSelectionAudioTrackData -+ (instancetype)makeWithIndex:(NSInteger)index - displayName:(nullable NSString *)displayName - languageCode:(nullable NSString *)languageCode - isSelected:(BOOL)isSelected - commonMetadataTitle:(nullable NSString *)commonMetadataTitle { - FVPMediaSelectionAudioTrackData *pigeonResult = [[FVPMediaSelectionAudioTrackData alloc] init]; ++ (instancetype)makeWithIndex:(NSInteger )index + displayName:(nullable NSString *)displayName + languageCode:(nullable NSString *)languageCode + isSelected:(BOOL )isSelected + commonMetadataTitle:(nullable NSString *)commonMetadataTitle { + FVPMediaSelectionAudioTrackData* pigeonResult = [[FVPMediaSelectionAudioTrackData alloc] init]; pigeonResult.index = index; pigeonResult.displayName = displayName; pigeonResult.languageCode = languageCode; @@ -159,19 +170,93 @@ + (nullable FVPMediaSelectionAudioTrackData *)nullableFromList:(NSArray *)li } @end +@implementation FVPPlatformMediaInfo ++ (instancetype)makeWithTitle:(NSString *)title + artist:(nullable NSString *)artist + artworkUrl:(nullable NSString *)artworkUrl + durationMs:(nullable NSNumber *)durationMs { + FVPPlatformMediaInfo* pigeonResult = [[FVPPlatformMediaInfo alloc] init]; + pigeonResult.title = title; + pigeonResult.artist = artist; + pigeonResult.artworkUrl = artworkUrl; + pigeonResult.durationMs = durationMs; + return pigeonResult; +} ++ (FVPPlatformMediaInfo *)fromList:(NSArray *)list { + FVPPlatformMediaInfo *pigeonResult = [[FVPPlatformMediaInfo alloc] init]; + pigeonResult.title = GetNullableObjectAtIndex(list, 0); + pigeonResult.artist = GetNullableObjectAtIndex(list, 1); + pigeonResult.artworkUrl = GetNullableObjectAtIndex(list, 2); + pigeonResult.durationMs = GetNullableObjectAtIndex(list, 3); + return pigeonResult; +} ++ (nullable FVPPlatformMediaInfo *)nullableFromList:(NSArray *)list { + return (list) ? [FVPPlatformMediaInfo fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + self.title ?: [NSNull null], + self.artist ?: [NSNull null], + self.artworkUrl ?: [NSNull null], + self.durationMs ?: [NSNull null], + ]; +} +@end + +@implementation FVPPlatformVideoQuality ++ (instancetype)makeWithWidth:(NSInteger )width + height:(NSInteger )height + bitrate:(NSInteger )bitrate + codec:(nullable NSString *)codec + isSelected:(BOOL )isSelected { + FVPPlatformVideoQuality* pigeonResult = [[FVPPlatformVideoQuality alloc] init]; + pigeonResult.width = width; + pigeonResult.height = height; + pigeonResult.bitrate = bitrate; + pigeonResult.codec = codec; + pigeonResult.isSelected = isSelected; + return pigeonResult; +} ++ (FVPPlatformVideoQuality *)fromList:(NSArray *)list { + FVPPlatformVideoQuality *pigeonResult = [[FVPPlatformVideoQuality alloc] init]; + pigeonResult.width = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.height = [GetNullableObjectAtIndex(list, 1) integerValue]; + pigeonResult.bitrate = [GetNullableObjectAtIndex(list, 2) integerValue]; + pigeonResult.codec = GetNullableObjectAtIndex(list, 3); + pigeonResult.isSelected = [GetNullableObjectAtIndex(list, 4) boolValue]; + return pigeonResult; +} ++ (nullable FVPPlatformVideoQuality *)nullableFromList:(NSArray *)list { + return (list) ? [FVPPlatformVideoQuality fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.width), + @(self.height), + @(self.bitrate), + self.codec ?: [NSNull null], + @(self.isSelected), + ]; +} +@end + @interface FVPMessagesPigeonCodecReader : FlutterStandardReader @end @implementation FVPMessagesPigeonCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 129: + case 129: return [FVPPlatformVideoViewCreationParams fromList:[self readValue]]; - case 130: + case 130: return [FVPCreationOptions fromList:[self readValue]]; - case 131: + case 131: return [FVPTexturePlayerIds fromList:[self readValue]]; - case 132: + case 132: return [FVPMediaSelectionAudioTrackData fromList:[self readValue]]; + case 133: + return [FVPPlatformMediaInfo fromList:[self readValue]]; + case 134: + return [FVPPlatformVideoQuality fromList:[self readValue]]; default: return [super readValueOfType:type]; } @@ -194,6 +279,12 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[FVPMediaSelectionAudioTrackData class]]) { [self writeByte:132]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPPlatformMediaInfo class]]) { + [self writeByte:133]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPPlatformVideoQuality class]]) { + [self writeByte:134]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -215,35 +306,25 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { static FlutterStandardMessageCodec *sSharedObject = nil; static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ - FVPMessagesPigeonCodecReaderWriter *readerWriter = - [[FVPMessagesPigeonCodecReaderWriter alloc] init]; + FVPMessagesPigeonCodecReaderWriter *readerWriter = [[FVPMessagesPigeonCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; }); return sSharedObject; } -void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, - NSObject *api) { +void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *api) { SetUpFVPAVFoundationVideoPlayerApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, - NSObject *api, - NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 - ? [NSString stringWithFormat:@".%@", messageChannelSuffix] - : @""; +void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.initialize", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(initialize:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", - api); + NSCAssert([api respondsToSelector:@selector(initialize:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api initialize:&error]; @@ -254,19 +335,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString - stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.createForPlatformView", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(createPlatformViewPlayerWithOptions:error:)", - api); + NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createPlatformViewPlayerWithOptions:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_params = GetNullableObjectAtIndex(args, 0); @@ -279,25 +354,18 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString - stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.createForTextureView", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(createTexturePlayerWithOptions:error:)", - api); + NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createTexturePlayerWithOptions:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_creationOptions = GetNullableObjectAtIndex(args, 0); FlutterError *error; - FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions - error:&error]; + FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions error:&error]; callback(wrapResult(output, error)); }]; } else { @@ -305,18 +373,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.setMixWithOthers", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(setMixWithOthers:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(setMixWithOthers:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_mixWithOthers = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -329,18 +392,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.getAssetUrl", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(fileURLForAssetWithName:package:error:)", - api); + NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(fileURLForAssetWithName:package:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSString *arg_asset = GetNullableObjectAtIndex(args, 0); @@ -353,31 +411,243 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.isPipSupported", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(isPipSupported:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(isPipSupported:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSNumber *output = [api isPipSupported:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.enterPip", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(enterPipForPlayer:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(enterPipForPlayer:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 0) integerValue]; + FlutterError *error; + [api enterPipForPlayer:arg_playerId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.exitPip", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(exitPipForPlayer:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(exitPipForPlayer:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 0) integerValue]; + FlutterError *error; + [api exitPipForPlayer:arg_playerId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.isPipActive", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(isPipActiveForPlayer:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(isPipActiveForPlayer:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 0) integerValue]; + FlutterError *error; + NSNumber *output = [api isPipActiveForPlayer:arg_playerId error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.enableBackgroundPlayback", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(enableBackgroundPlaybackForPlayer:mediaInfo:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(enableBackgroundPlaybackForPlayer:mediaInfo:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 0) integerValue]; + FVPPlatformMediaInfo *arg_mediaInfo = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api enableBackgroundPlaybackForPlayer:arg_playerId mediaInfo:arg_mediaInfo error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.disableBackgroundPlayback", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(disableBackgroundPlaybackForPlayer:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(disableBackgroundPlaybackForPlayer:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 0) integerValue]; + FlutterError *error; + [api disableBackgroundPlaybackForPlayer:arg_playerId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setAutoPip", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setAutoPipForPlayer:enabled:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(setAutoPipForPlayer:enabled:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 0) integerValue]; + BOOL arg_enabled = [GetNullableObjectAtIndex(args, 1) boolValue]; + FlutterError *error; + [api setAutoPipForPlayer:arg_playerId enabled:arg_enabled error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setCacheMaxSize", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setCacheMaxSize:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(setCacheMaxSize:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_maxSizeBytes = [GetNullableObjectAtIndex(args, 0) integerValue]; + FlutterError *error; + [api setCacheMaxSize:arg_maxSizeBytes error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.clearCache", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(clearCache:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(clearCache:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + [api clearCache:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getCacheSize", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getCacheSize:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(getCacheSize:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSNumber *output = [api getCacheSize:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.isCacheEnabled", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(isCacheEnabled:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(isCacheEnabled:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSNumber *output = [api isCacheEnabled:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setCacheEnabled", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setCacheEnabled:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(setCacheEnabled:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + BOOL arg_enabled = [GetNullableObjectAtIndex(args, 0) boolValue]; + FlutterError *error; + [api setCacheEnabled:arg_enabled error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } } -void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, - NSObject *api) { +void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *api) { SetUpFVPVideoPlayerInstanceApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, - NSObject *api, - NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 - ? [NSString stringWithFormat:@".%@", messageChannelSuffix] - : @""; +void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setLooping", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(setLooping:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setLooping:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_looping = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -390,18 +660,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setVolume", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(setVolume:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setVolume:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_volume = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -414,18 +679,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setPlaybackSpeed", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " - @"@selector(setPlaybackSpeed:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setPlaybackSpeed:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_speed = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -438,17 +698,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.play", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(playWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(playWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api playWithError:&error]; @@ -459,16 +715,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.getPosition", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(position:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); + NSCAssert([api respondsToSelector:@selector(position:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; NSNumber *output = [api position:&error]; @@ -479,42 +732,32 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.seekTo", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(seekTo:completion:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", - api); + NSCAssert([api respondsToSelector:@selector(seekTo:completion:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSInteger arg_position = [GetNullableObjectAtIndex(args, 0) integerValue]; - [api seekTo:arg_position - completion:^(FlutterError *_Nullable error) { - callback(wrapResult(nil, error)); - }]; + [api seekTo:arg_position completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; }]; } else { [channel setMessageHandler:nil]; } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.pause", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(pauseWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(pauseWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api pauseWithError:&error]; @@ -525,18 +768,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.dispose", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(disposeWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(disposeWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api disposeWithError:&error]; @@ -547,17 +785,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.getAudioTracks", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(getAudioTracks:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getAudioTracks:)", - api); + NSCAssert([api respondsToSelector:@selector(getAudioTracks:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getAudioTracks:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; NSArray *output = [api getAudioTracks:&error]; @@ -568,18 +802,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.selectAudioTrack", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(selectAudioTrackAtIndex:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " - @"@selector(selectAudioTrackAtIndex:error:)", - api); + NSCAssert([api respondsToSelector:@selector(selectAudioTrackAtIndex:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(selectAudioTrackAtIndex:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSInteger arg_trackIndex = [GetNullableObjectAtIndex(args, 0) integerValue]; @@ -591,4 +820,77 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAvailableQualities", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getAvailableQualities:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getAvailableQualities:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSArray *output = [api getAvailableQualities:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getCurrentQuality", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getCurrentQuality:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getCurrentQuality:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + FVPPlatformVideoQuality *output = [api getCurrentQuality:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setMaxBitrate", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setMaxBitrate:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setMaxBitrate:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_maxBitrateBps = [GetNullableObjectAtIndex(args, 0) integerValue]; + FlutterError *error; + [api setMaxBitrate:arg_maxBitrateBps error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setMaxResolution", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setMaxResolutionWidth:height:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setMaxResolutionWidth:height:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_width = [GetNullableObjectAtIndex(args, 0) integerValue]; + NSInteger arg_height = [GetNullableObjectAtIndex(args, 1) integerValue]; + FlutterError *error; + [api setMaxResolutionWidth:arg_width height:arg_height error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/video_player/video_player_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist b/packages/video_player/video_player_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist index 1f6b98f117b2..6fe4034356ac 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/video_player/video_player_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist @@ -24,7 +24,5 @@ arm64 - MinimumOSVersion - 13.0 diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 54ab55a992ce..afc7696ebf38 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist b/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist index 4e29652e6d2e..23a952e51002 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist @@ -52,5 +52,9 @@ UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + audio + diff --git a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart index bfc6b1dd496f..d4609f15b406 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart @@ -279,6 +279,9 @@ class MiniController extends ValueNotifier { case VideoEventType.isPlayingStateUpdate: value = value.copyWith(isPlaying: event.isPlaying); case VideoEventType.unknown: + case VideoEventType.pipStateChanged: + case VideoEventType.qualityChanged: + case VideoEventType.decoderChanged: break; } } diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index 4a9153c7d267..7995d6bdacd5 100644 --- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -16,7 +16,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - video_player_platform_interface: ^6.6.0 + video_player_platform_interface: + path: ../../video_player_platform_interface dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index 6684d9c4c658..9a2034a89607 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -212,6 +212,121 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { return true; } + @override + Future isPipSupported() async { + return _api.isPipSupported(); + } + + @override + Future enterPip(int playerId) { + return _api.enterPip(playerId); + } + + @override + Future exitPip(int playerId) { + return _api.exitPip(playerId); + } + + @override + Future isPipActive(int playerId) async { + return _api.isPipActive(playerId); + } + + @override + Future setAutoEnterPip(int playerId, bool enabled) { + return _api.setAutoPip(playerId, enabled); + } + + @override + Future enableBackgroundPlayback(int playerId, {MediaInfo? mediaInfo}) { + final PlatformMediaInfo? pigeonMediaInfo = mediaInfo != null + ? PlatformMediaInfo( + title: mediaInfo.title, + artist: mediaInfo.artist, + artworkUrl: mediaInfo.artworkUrl, + durationMs: mediaInfo.durationMs, + ) + : null; + return _api.enableBackgroundPlayback(playerId, pigeonMediaInfo); + } + + @override + Future disableBackgroundPlayback(int playerId) { + return _api.disableBackgroundPlayback(playerId); + } + + // Cache control methods (iOS no-ops until future HLS cache phase) + + @override + Future setCacheMaxSize(int maxSizeBytes) { + return _api.setCacheMaxSize(maxSizeBytes); + } + + @override + Future clearCache() { + return _api.clearCache(); + } + + @override + Future getCacheSize() async { + return _api.getCacheSize(); + } + + @override + Future isCacheEnabled() async { + return _api.isCacheEnabled(); + } + + @override + Future setCacheEnabled(bool enabled) { + return _api.setCacheEnabled(enabled); + } + + // ABR control methods + + @override + Future> getAvailableQualities(int playerId) async { + final List qualities = + await _playerWith(id: playerId).getAvailableQualities(); + return qualities + .map( + (PlatformVideoQuality q) => VideoQuality( + width: q.width, + height: q.height, + bitrate: q.bitrate, + codec: q.codec, + isSelected: q.isSelected, + ), + ) + .toList(); + } + + @override + Future getCurrentQuality(int playerId) async { + final PlatformVideoQuality? q = + await _playerWith(id: playerId).getCurrentQuality(); + if (q == null) { + return null; + } + return VideoQuality( + width: q.width, + height: q.height, + bitrate: q.bitrate, + codec: q.codec, + isSelected: q.isSelected, + ); + } + + @override + Future setMaxBitrate(int playerId, int maxBitrateBps) { + return _playerWith(id: playerId).setMaxBitrate(maxBitrateBps); + } + + @override + Future setMaxResolution(int playerId, int width, int height) { + return _playerWith(id: playerId).setMaxResolution(width, height); + } + @override Widget buildView(int playerId) { return buildViewWithOptions(VideoViewOptions(playerId: playerId)); @@ -289,6 +404,18 @@ class _PlayerInstance { Future selectAudioTrack(int trackIndex) => _api.selectAudioTrack(trackIndex); + Future> getAvailableQualities() => + _api.getAvailableQualities(); + + Future getCurrentQuality() => + _api.getCurrentQuality(); + + Future setMaxBitrate(int maxBitrateBps) => + _api.setMaxBitrate(maxBitrateBps); + + Future setMaxResolution(int width, int height) => + _api.setMaxResolution(width, height); + Stream get videoEvents { _eventSubscription ??= _eventChannel.receiveBroadcastStream().listen( _onStreamEvent, @@ -331,6 +458,19 @@ class _PlayerInstance { eventType: VideoEventType.isPlayingStateUpdate, isPlaying: map['isPlaying'] as bool, ), + 'pipStateChanged' => VideoEvent( + eventType: VideoEventType.pipStateChanged, + isPipActive: map['isPipActive'] as bool, + ), + 'qualityChanged' => VideoEvent( + eventType: VideoEventType.qualityChanged, + quality: VideoQuality( + width: map['width'] as int, + height: map['height'] as int, + bitrate: map['bitrate'] as int, + isSelected: true, + ), + ), _ => VideoEvent(eventType: VideoEventType.unknown), }); } diff --git a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart index 24644d8f42d0..ad28a9fd920b 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// Autogenerated from Pigeon (v26.1.10), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -11,55 +11,74 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +Object? _extractReplyValueOrThrow( + List? replyList, + String channelName, { + required bool isNullValid, +}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && - a.indexed.every( - ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), - ); + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key]), - ); + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); } return a == b; } + /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { - PlatformVideoViewCreationParams({required this.playerId}); + PlatformVideoViewCreationParams({ + required this.playerId, + }); int playerId; List _toList() { - return [playerId]; + return [ + playerId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlatformVideoViewCreationParams decode(Object result) { result as List; - return PlatformVideoViewCreationParams(playerId: result[0]! as int); + return PlatformVideoViewCreationParams( + playerId: result[0]! as int, + ); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlatformVideoViewCreationParams || - other.runtimeType != runtimeType) { + if (other is! PlatformVideoViewCreationParams || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -70,30 +89,35 @@ class PlatformVideoViewCreationParams { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class CreationOptions { - CreationOptions({required this.uri, required this.httpHeaders}); + CreationOptions({ + required this.uri, + required this.httpHeaders, + }); String uri; Map httpHeaders; List _toList() { - return [uri, httpHeaders]; + return [ + uri, + httpHeaders, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static CreationOptions decode(Object result) { result as List; return CreationOptions( uri: result[0]! as String, - httpHeaders: (result[1] as Map?)! - .cast(), + httpHeaders: (result[1] as Map?)!.cast(), ); } @@ -111,23 +135,29 @@ class CreationOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class TexturePlayerIds { - TexturePlayerIds({required this.playerId, required this.textureId}); + TexturePlayerIds({ + required this.playerId, + required this.textureId, + }); int playerId; int textureId; List _toList() { - return [playerId, textureId]; + return [ + playerId, + textureId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static TexturePlayerIds decode(Object result) { result as List; @@ -151,7 +181,8 @@ class TexturePlayerIds { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Raw audio track data from AVMediaSelectionOption (for HLS streams). @@ -185,8 +216,7 @@ class MediaSelectionAudioTrackData { } Object encode() { - return _toList(); - } + return _toList(); } static MediaSelectionAudioTrackData decode(Object result) { result as List; @@ -202,8 +232,7 @@ class MediaSelectionAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! MediaSelectionAudioTrackData || - other.runtimeType != runtimeType) { + if (other is! MediaSelectionAudioTrackData || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -214,9 +243,129 @@ class MediaSelectionAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } +class PlatformMediaInfo { + PlatformMediaInfo({ + required this.title, + this.artist, + this.artworkUrl, + this.durationMs, + }); + + String title; + + String? artist; + + String? artworkUrl; + + int? durationMs; + + List _toList() { + return [ + title, + artist, + artworkUrl, + durationMs, + ]; + } + + Object encode() { + return _toList(); } + + static PlatformMediaInfo decode(Object result) { + result as List; + return PlatformMediaInfo( + title: result[0]! as String, + artist: result[1] as String?, + artworkUrl: result[2] as String?, + durationMs: result[3] as int?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformMediaInfo || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Represents a video quality variant (resolution/bitrate combination). +class PlatformVideoQuality { + PlatformVideoQuality({ + required this.width, + required this.height, + required this.bitrate, + this.codec, + required this.isSelected, + }); + + int width; + + int height; + + int bitrate; + + String? codec; + + bool isSelected; + + List _toList() { + return [ + width, + height, + bitrate, + codec, + isSelected, + ]; + } + + Object encode() { + return _toList(); } + + static PlatformVideoQuality decode(Object result) { + result as List; + return PlatformVideoQuality( + width: result[0]! as int, + height: result[1]! as int, + bitrate: result[2]! as int, + codec: result[3] as String?, + isSelected: result[4]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformVideoQuality || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -224,18 +373,24 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlatformVideoViewCreationParams) { + } else if (value is PlatformVideoViewCreationParams) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { + } else if (value is CreationOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { + } else if (value is TexturePlayerIds) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is MediaSelectionAudioTrackData) { + } else if (value is MediaSelectionAudioTrackData) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is PlatformMediaInfo) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is PlatformVideoQuality) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -252,6 +407,10 @@ class _PigeonCodec extends StandardMessageCodec { return TexturePlayerIds.decode(readValue(buffer)!); case 132: return MediaSelectionAudioTrackData.decode(readValue(buffer)!); + case 133: + return PlatformMediaInfo.decode(readValue(buffer)!); + case 134: + return PlatformVideoQuality.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -262,13 +421,9 @@ class AVFoundationVideoPlayerApi { /// Constructor for [AVFoundationVideoPlayerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AVFoundationVideoPlayerApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + AVFoundationVideoPlayerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -276,8 +431,7 @@ class AVFoundationVideoPlayerApi { final String pigeonVar_messageChannelSuffix; Future initialize() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -285,129 +439,308 @@ class AVFoundationVideoPlayerApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } Future createForPlatformView(CreationOptions params) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [params], + final Future pigeonVar_sendFuture = pigeonVar_channel.send([params]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as int; + } + + Future createForTextureView(CreationOptions creationOptions) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([creationOptions]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as int?)!; - } + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as TexturePlayerIds; } - Future createForTextureView( - CreationOptions creationOptions, - ) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + Future setMixWithOthers(bool mixWithOthers) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [creationOptions], + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future getAssetUrl(String asset, String? package) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([asset, package]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as TexturePlayerIds?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + return pigeonVar_replyValue as String?; } - Future setMixWithOthers(bool mixWithOthers) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; + Future isPipSupported() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.isPipSupported$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [mixWithOthers], + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as bool; + } + + Future enterPip(int playerId) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.enterPip$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } - Future getAssetUrl(String asset, String? package) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; + Future exitPip(int playerId) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.exitPip$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [asset, package], + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future isPipActive(int playerId) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.isPipActive$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return (pigeonVar_replyList[0] as String?); - } + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as bool; + } + + Future enableBackgroundPlayback(int playerId, PlatformMediaInfo? mediaInfo) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.enableBackgroundPlayback$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId, mediaInfo]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future disableBackgroundPlayback(int playerId) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.disableBackgroundPlayback$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future setAutoPip(int playerId, bool enabled) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setAutoPip$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId, enabled]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future setCacheMaxSize(int maxSizeBytes) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setCacheMaxSize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([maxSizeBytes]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future clearCache() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.clearCache$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future getCacheSize() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getCacheSize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as int; + } + + Future isCacheEnabled() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.isCacheEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as bool; + } + + Future setCacheEnabled(bool enabled) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setCacheEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } } @@ -415,13 +748,9 @@ class VideoPlayerInstanceApi { /// Constructor for [VideoPlayerInstanceApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - VideoPlayerInstanceApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + VideoPlayerInstanceApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -429,83 +758,61 @@ class VideoPlayerInstanceApi { final String pigeonVar_messageChannelSuffix; Future setLooping(bool looping) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [looping], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } Future setVolume(double volume) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [volume], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } Future setPlaybackSpeed(double speed) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [speed], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } Future play() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -513,22 +820,17 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } Future getPosition() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -536,52 +838,36 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as int?)!; - } + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return pigeonVar_replyValue as int; } Future seekTo(int position) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [position], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } Future pause() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -589,22 +875,17 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } Future dispose() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -612,22 +893,17 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } Future> getAudioTracks() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -635,47 +911,105 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as List?)! - .cast(); - } + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return (pigeonVar_replyValue as List).cast(); } Future selectAudioTrack(int trackIndex) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([trackIndex]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future> getAvailableQualities() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAvailableQualities$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [trackIndex], + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + !; + return (pigeonVar_replyValue as List).cast(); + } + + Future getCurrentQuality() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getCurrentQuality$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + return pigeonVar_replyValue as PlatformVideoQuality?; + } + + Future setMaxBitrate(int maxBitrateBps) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setMaxBitrate$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([maxBitrateBps]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future setMaxResolution(int width, int height) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setMaxResolution$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([width, height]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } } diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index f49b46005307..4ab39c28806c 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -56,6 +56,29 @@ class MediaSelectionAudioTrackData { String? commonMetadataTitle; } +class PlatformMediaInfo { + PlatformMediaInfo({required this.title}); + String title; + String? artist; + String? artworkUrl; + int? durationMs; +} + +/// Represents a video quality variant (resolution/bitrate combination). +class PlatformVideoQuality { + PlatformVideoQuality({ + required this.width, + required this.height, + required this.bitrate, + required this.isSelected, + }); + int width; + int height; + int bitrate; + String? codec; + bool isSelected; +} + @HostApi() abstract class AVFoundationVideoPlayerApi { @ObjCSelector('initialize') @@ -71,6 +94,32 @@ abstract class AVFoundationVideoPlayerApi { void setMixWithOthers(bool mixWithOthers); @ObjCSelector('fileURLForAssetWithName:package:') String? getAssetUrl(String asset, String? package); + @ObjCSelector('isPipSupported') + bool isPipSupported(); + @ObjCSelector('enterPipForPlayer:') + void enterPip(int playerId); + @ObjCSelector('exitPipForPlayer:') + void exitPip(int playerId); + @ObjCSelector('isPipActiveForPlayer:') + bool isPipActive(int playerId); + @ObjCSelector('enableBackgroundPlaybackForPlayer:mediaInfo:') + void enableBackgroundPlayback(int playerId, PlatformMediaInfo? mediaInfo); + @ObjCSelector('disableBackgroundPlaybackForPlayer:') + void disableBackgroundPlayback(int playerId); + @ObjCSelector('setAutoPipForPlayer:enabled:') + void setAutoPip(int playerId, bool enabled); + + // Cache control methods (no-ops on iOS until future HLS cache phase) + @ObjCSelector('setCacheMaxSize:') + void setCacheMaxSize(int maxSizeBytes); + @ObjCSelector('clearCache') + void clearCache(); + @ObjCSelector('getCacheSize') + int getCacheSize(); + @ObjCSelector('isCacheEnabled') + bool isCacheEnabled(); + @ObjCSelector('setCacheEnabled:') + void setCacheEnabled(bool enabled); } @HostApi() @@ -93,4 +142,14 @@ abstract class VideoPlayerInstanceApi { List getAudioTracks(); @ObjCSelector('selectAudioTrackAtIndex:') void selectAudioTrack(int trackIndex); + + // ABR (Adaptive Bitrate) control methods + @ObjCSelector('getAvailableQualities') + List getAvailableQualities(); + @ObjCSelector('getCurrentQuality') + PlatformVideoQuality? getCurrentQuality(); + @ObjCSelector('setMaxBitrate:') + void setMaxBitrate(int maxBitrateBps); + @ObjCSelector('setMaxResolutionWidth:height:') + void setMaxResolution(int width, int height); } diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index f14fefb73326..2e76d4a5c1ca 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -24,7 +24,8 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^6.6.0 + video_player_platform_interface: + path: ../video_player_platform_interface dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 1cec5f42c218..5bba8f771ac9 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -153,6 +153,134 @@ abstract class VideoPlayerPlatform extends PlatformInterface { bool isAudioTrackSupportAvailable() { return false; } + + /// Returns whether Picture-in-Picture mode is supported on this device. + Future isPipSupported() { + throw UnimplementedError('isPipSupported() has not been implemented.'); + } + + /// Enters Picture-in-Picture mode for the given player. + Future enterPip(int playerId) { + throw UnimplementedError('enterPip() has not been implemented.'); + } + + /// Exits Picture-in-Picture mode for the given player. + Future exitPip(int playerId) { + throw UnimplementedError('exitPip() has not been implemented.'); + } + + /// Returns whether Picture-in-Picture mode is currently active. + Future isPipActive(int playerId) { + throw UnimplementedError('isPipActive() has not been implemented.'); + } + + /// Sets whether PiP should be entered automatically when the app + /// goes to background (Android 12+ only). + Future setAutoEnterPip(int playerId, bool enabled) { + throw UnimplementedError('setAutoEnterPip() has not been implemented.'); + } + + /// Enables background playback for the given player. + /// + /// When enabled, audio continues playing when the app is backgrounded. + /// On Android, this starts a foreground service with a media notification. + /// On iOS, this configures the audio session and sets up lock screen controls. + Future enableBackgroundPlayback(int playerId, {MediaInfo? mediaInfo}) { + throw UnimplementedError( + 'enableBackgroundPlayback() has not been implemented.', + ); + } + + /// Disables background playback for the given player. + Future disableBackgroundPlayback(int playerId) { + throw UnimplementedError( + 'disableBackgroundPlayback() has not been implemented.', + ); + } + + // Cache control methods + + /// Sets the maximum cache size in bytes. + Future setCacheMaxSize(int maxSizeBytes) { + throw UnimplementedError( + 'setCacheMaxSize() has not been implemented.', + ); + } + + /// Clears all cached video data. + Future clearCache() { + throw UnimplementedError('clearCache() has not been implemented.'); + } + + /// Returns the current cache size in bytes. + Future getCacheSize() { + throw UnimplementedError('getCacheSize() has not been implemented.'); + } + + /// Returns whether caching is enabled. + Future isCacheEnabled() { + throw UnimplementedError('isCacheEnabled() has not been implemented.'); + } + + /// Enables or disables caching. + Future setCacheEnabled(bool enabled) { + throw UnimplementedError('setCacheEnabled() has not been implemented.'); + } + + // Adaptive Bitrate control methods + + /// Returns the available video quality variants for the given player. + Future> getAvailableQualities(int playerId) { + throw UnimplementedError( + 'getAvailableQualities() has not been implemented.', + ); + } + + /// Returns the current video quality for the given player. + Future getCurrentQuality(int playerId) { + throw UnimplementedError( + 'getCurrentQuality() has not been implemented.', + ); + } + + /// Sets the maximum video bitrate in bits per second. + Future setMaxBitrate(int playerId, int maxBitrateBps) { + throw UnimplementedError('setMaxBitrate() has not been implemented.'); + } + + /// Sets the maximum video resolution. + Future setMaxResolution(int playerId, int width, int height) { + throw UnimplementedError('setMaxResolution() has not been implemented.'); + } + + // Decoder selection methods + + /// Returns the available video decoders for the given player. + /// + /// The list is filtered by the current video's MIME type. Each entry + /// indicates whether the decoder is hardware-accelerated or software-only. + Future> getAvailableDecoders(int playerId) { + throw UnimplementedError( + 'getAvailableDecoders() has not been implemented.', + ); + } + + /// Returns the name of the currently active video decoder, or null if + /// no decoder has been initialized yet. + Future getCurrentDecoderName(int playerId) { + throw UnimplementedError( + 'getCurrentDecoderName() has not been implemented.', + ); + } + + /// Forces the player to use a specific video decoder by name. + /// + /// Pass null to revert to automatic decoder selection. + /// This rebuilds the underlying player instance, causing a brief + /// playback interruption (~200-500ms). + Future setVideoDecoder(int playerId, String? decoderName) { + throw UnimplementedError('setVideoDecoder() has not been implemented.'); + } } class _PlaceholderImplementation extends VideoPlayerPlatform {} @@ -273,6 +401,12 @@ class VideoEvent { this.rotationCorrection, this.buffered, this.isPlaying, + this.isPipActive, + this.wasDismissed, + this.pipWindowSize, + this.quality, + this.decoderName, + this.isDecoderHardwareAccelerated, }); /// The type of the event. @@ -303,6 +437,40 @@ class VideoEvent { /// Only used if [eventType] is [VideoEventType.isPlayingStateUpdate]. final bool? isPlaying; + /// Whether Picture-in-Picture mode is currently active. + /// + /// Only used if [eventType] is [VideoEventType.pipStateChanged]. + final bool? isPipActive; + + /// Whether PiP was dismissed by the user (e.g. the X button) as opposed to + /// expanded back to full screen. + /// + /// Only used if [eventType] is [VideoEventType.pipStateChanged] and + /// [isPipActive] is false. + final bool? wasDismissed; + + /// The window size (in dp) at the time of a PiP state change. + /// + /// Only used if [eventType] is [VideoEventType.pipStateChanged]. + /// When [isPipActive] is true, this is the PiP window size. + /// When [isPipActive] is false, this is the restored window size. + final Size? pipWindowSize; + + /// The current video quality after an ABR switch. + /// + /// Only used if [eventType] is [VideoEventType.qualityChanged]. + final VideoQuality? quality; + + /// The name of the active video decoder. + /// + /// Only used if [eventType] is [VideoEventType.decoderChanged]. + final String? decoderName; + + /// Whether the active decoder is hardware-accelerated. + /// + /// Only used if [eventType] is [VideoEventType.decoderChanged]. + final bool? isDecoderHardwareAccelerated; + @override bool operator ==(Object other) { return identical(this, other) || @@ -313,7 +481,13 @@ class VideoEvent { size == other.size && rotationCorrection == other.rotationCorrection && listEquals(buffered, other.buffered) && - isPlaying == other.isPlaying; + isPlaying == other.isPlaying && + isPipActive == other.isPipActive && + wasDismissed == other.wasDismissed && + pipWindowSize == other.pipWindowSize && + quality == other.quality && + decoderName == other.decoderName && + isDecoderHardwareAccelerated == other.isDecoderHardwareAccelerated; } @override @@ -324,6 +498,12 @@ class VideoEvent { rotationCorrection, buffered, isPlaying, + isPipActive, + wasDismissed, + pipWindowSize, + quality, + decoderName, + isDecoderHardwareAccelerated, ); } @@ -355,6 +535,15 @@ enum VideoEventType { /// phone calls, or other app media such as music players. isPlayingStateUpdate, + /// The PiP state has changed. + pipStateChanged, + + /// The video quality has changed (ABR switch). + qualityChanged, + + /// The video decoder has changed. + decoderChanged, + /// An unknown event has been received. unknown, } @@ -436,6 +625,7 @@ class VideoPlayerOptions { this.mixWithOthers = false, this.allowBackgroundPlayback = false, this.webOptions, + this.androidOptions, }); /// Set this to true to keep playing video in background, when app goes in background. @@ -451,6 +641,31 @@ class VideoPlayerOptions { /// Additional web controls final VideoPlayerWebOptions? webOptions; + + /// Additional Android controls + final AndroidVideoPlayerOptions? androidOptions; +} + +/// Android-specific video player options for configuring ExoPlayer behavior. +@immutable +class AndroidVideoPlayerOptions { + /// Creates Android-specific video player options. + const AndroidVideoPlayerOptions({ + this.maxLoadRetries = 5, + this.maxPlayerRecoveryAttempts = 3, + }); + + /// Max retries per segment/load error before escalating + /// (ExoPlayer LoadErrorHandlingPolicy). + /// + /// Default 5 matches ExoPlayer's DefaultLoadErrorHandlingPolicy default. + final int maxLoadRetries; + + /// Max player-level recovery attempts for fatal network errors + /// (e.g. connection lost). + /// + /// After exhausting these, the error is surfaced to the app. + final int maxPlayerRecoveryAttempts; } /// [VideoPlayerWebOptions] can be optionally used to set additional web settings @@ -553,6 +768,7 @@ class VideoCreationOptions { const VideoCreationOptions({ required this.dataSource, required this.viewType, + this.androidOptions, }); /// The data source used to create the player. @@ -560,6 +776,9 @@ class VideoCreationOptions { /// The type of view to be used for displaying the video player final VideoViewType viewType; + + /// Android-specific options for configuring ExoPlayer behavior. + final AndroidVideoPlayerOptions? androidOptions; } /// Represents an audio track in a video with its metadata. @@ -652,3 +871,136 @@ class VideoAudioTrack { 'channelCount: $channelCount, ' 'codec: $codec)'; } + +/// Represents a video quality variant (resolution/bitrate combination) +/// from an adaptive bitrate stream. +@immutable +class VideoQuality { + /// Constructs a [VideoQuality]. + const VideoQuality({ + required this.width, + required this.height, + required this.bitrate, + required this.isSelected, + this.codec, + }); + + /// Width in pixels. + final int width; + + /// Height in pixels. + final int height; + + /// Bitrate in bits per second. + final int bitrate; + + /// Video codec (e.g., 'avc1.64001f', 'hev1.1.6.L93.B0'). + final String? codec; + + /// Whether this quality variant is currently selected. + final bool isSelected; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VideoQuality && + runtimeType == other.runtimeType && + width == other.width && + height == other.height && + bitrate == other.bitrate && + codec == other.codec && + isSelected == other.isSelected; + + @override + int get hashCode => Object.hash(width, height, bitrate, codec, isSelected); + + @override + String toString() => + 'VideoQuality(${width}x$height @ ${bitrate}bps, codec: $codec, selected: $isSelected)'; +} + +/// Describes a video decoder available on the device. +@immutable +class VideoDecoderInfo { + /// Constructs a [VideoDecoderInfo]. + const VideoDecoderInfo({ + required this.name, + required this.mimeType, + required this.isHardwareAccelerated, + required this.isSoftwareOnly, + required this.isSelected, + }); + + /// The codec name (e.g. 'OMX.qcom.video.decoder.avc', + /// 'c2.android.avc.decoder'). + final String name; + + /// The MIME type this decoder handles (e.g. 'video/avc'). + final String mimeType; + + /// Whether this decoder is hardware-accelerated. + final bool isHardwareAccelerated; + + /// Whether this decoder is software-only. + final bool isSoftwareOnly; + + /// Whether this decoder is currently active. + final bool isSelected; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VideoDecoderInfo && + runtimeType == other.runtimeType && + name == other.name && + mimeType == other.mimeType && + isHardwareAccelerated == other.isHardwareAccelerated && + isSoftwareOnly == other.isSoftwareOnly && + isSelected == other.isSelected; + + @override + int get hashCode => + Object.hash(name, mimeType, isHardwareAccelerated, isSoftwareOnly, isSelected); + + @override + String toString() => + 'VideoDecoderInfo($name, $mimeType, hw: $isHardwareAccelerated, sw: $isSoftwareOnly, selected: $isSelected)'; +} + +/// Media information for lock screen / notification display during +/// background playback. +@immutable +class MediaInfo { + /// Constructs a [MediaInfo]. + const MediaInfo({ + required this.title, + this.artist, + this.artworkUrl, + this.durationMs, + }); + + /// The title of the media. + final String title; + + /// The artist of the media. + final String? artist; + + /// URL to the artwork image. + final String? artworkUrl; + + /// Duration of the media in milliseconds. + final int? durationMs; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MediaInfo && + runtimeType == other.runtimeType && + title == other.title && + artist == other.artist && + artworkUrl == other.artworkUrl && + durationMs == other.durationMs; + + @override + int get hashCode => Object.hash(title, artist, artworkUrl, durationMs); +} diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index ae7ab08cb17d..26383a272a0f 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -8,7 +8,8 @@ environment: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^6.3.0 + video_player_platform_interface: + path: ../../video_player_platform_interface video_player_web: path: ../ web: ^1.0.0 diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index b3b4739896d7..019c57b578b4 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -21,7 +21,8 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - video_player_platform_interface: ^6.4.0 + video_player_platform_interface: + path: ../video_player_platform_interface web: ">=0.5.1 <2.0.0" dev_dependencies: