From 6bae48f0d1c1cf943449b83f185af6a11f033a38 Mon Sep 17 00:00:00 2001 From: Alan Hughes <30924086+alanjhughes@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:19:07 +0000 Subject: [PATCH 01/17] [ios][dev-launcher] rework local permissions UI (#43252) --- .../radar-icon.imageset/Contents.json | 21 ++ .../radar-icon.imageset/radar-icon.png | Bin 0 -> 39432 bytes .../ios/SwiftUI/DevLauncherViewModel.swift | 67 +++++- .../ios/SwiftUI/DevLauncherViews.swift | 2 +- .../ios/SwiftUI/HomeTabView.swift | 41 ++++ .../SwiftUI/LocalNetworkPermissionView.swift | 216 ++++++++++++++---- .../ios/SwiftUI/SettingsTabView.swift | 135 +++-------- 7 files changed, 326 insertions(+), 156 deletions(-) create mode 100644 packages/expo-dev-launcher/ios/Assets.xcassets/radar-icon.imageset/Contents.json create mode 100644 packages/expo-dev-launcher/ios/Assets.xcassets/radar-icon.imageset/radar-icon.png diff --git a/packages/expo-dev-launcher/ios/Assets.xcassets/radar-icon.imageset/Contents.json b/packages/expo-dev-launcher/ios/Assets.xcassets/radar-icon.imageset/Contents.json new file mode 100644 index 00000000000000..38f83cf75926b5 --- /dev/null +++ b/packages/expo-dev-launcher/ios/Assets.xcassets/radar-icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "radar-icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/expo-dev-launcher/ios/Assets.xcassets/radar-icon.imageset/radar-icon.png b/packages/expo-dev-launcher/ios/Assets.xcassets/radar-icon.imageset/radar-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6f6a9c6469e9af70ac8f2f71700b9bf521285f77 GIT binary patch literal 39432 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 literal 0 HcmV?d00001 diff --git a/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViewModel.swift b/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViewModel.swift index 26400e30654014..ccdd546fe22de8 100644 --- a/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViewModel.swift +++ b/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViewModel.swift @@ -14,6 +14,8 @@ private let networkPermissionGrantedKey = "expo.devlauncher.hasGrantedNetworkPer enum LocalNetworkPermissionStatus: Equatable, Sendable { case unknown + case checking + case granted case denied } @@ -217,17 +219,69 @@ class DevLauncherViewModel: ObservableObject { func markNetworkPermissionGranted() { UserDefaults.standard.set(true, forKey: networkPermissionGrantedKey) - } - - func resetPermissionFlowState() { - UserDefaults.standard.removeObject(forKey: networkPermissionGrantedKey) - permissionStatus = .unknown + permissionStatus = .granted } var hasGrantedNetworkPermission: Bool { UserDefaults.standard.bool(forKey: networkPermissionGrantedKey) } + func refreshPermissionStatus() { + permissionStatus = .checking + Task { + let hasAccess = await checkLocalNetworkAccess() + permissionStatus = hasAccess ? .granted : .denied + } + } + + func checkLocalNetworkAccess() async -> Bool { + let serviceType = BONJOUR_TYPE + let queue = DispatchQueue(label: "expo.devlauncher.permissioncheck") + + return await withCheckedContinuation { continuation in + var done = false + + let listener = try? NWListener(using: .tcp, on: .any) + listener?.service = NWListener.Service(type: serviceType) + listener?.stateUpdateHandler = { _ in } + listener?.newConnectionHandler = { $0.cancel() } + listener?.start(queue: queue) + + let browser = NWBrowser(for: .bonjour(type: serviceType, domain: nil), using: .tcp) + browser.browseResultsChangedHandler = { results, _ in + guard !done else { return } + if !results.isEmpty { + done = true + continuation.resume(returning: true) + browser.cancel() + listener?.cancel() + } + } + + browser.stateUpdateHandler = { state in + guard !done else { return } + if case .waiting(let error) = state, + case .dns(let dnsError) = error, + dnsError == kDNSServiceErr_PolicyDenied { + done = true + continuation.resume(returning: false) + browser.cancel() + listener?.cancel() + } + } + + browser.start(queue: queue) + + queue.asyncAfter(deadline: .now() + 2) { + guard !done else { return } + done = true + continuation.resume(returning: false) + browser.cancel() + listener?.cancel() + } + } + } + func stopServerDiscovery() { pingTask?.cancel() scanTask?.cancel() @@ -268,8 +322,6 @@ class DevLauncherViewModel: ObservableObject { Task { @MainActor [weak self] in guard let self else { return } switch state { - case .ready: - self.markNetworkPermissionGranted() case .waiting(let error): if case .dns(let dnsError) = error, dnsError == kDNSServiceErr_PolicyDenied { self.permissionStatus = .denied @@ -288,6 +340,7 @@ class DevLauncherViewModel: ObservableObject { guard let self = self else { return } Task { @MainActor [weak self, results] in guard let self else { return } + self.markNetworkPermissionGranted() self.pingTask?.cancel() self.pingTask = Task { defer { self.pingTask = nil } diff --git a/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViews.swift b/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViews.swift index 2d677a27d66a0c..f7e8d3cf0fe69f 100644 --- a/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViews.swift +++ b/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViews.swift @@ -24,7 +24,7 @@ public struct DevLauncherRootView: View { public var body: some View { if !hasCompletedPermissionFlow { - LocalNetworkPermissionView { + LocalNetworkPermissionView(viewModel: viewModel) { hasCompletedPermissionFlow = true } } else { diff --git a/packages/expo-dev-launcher/ios/SwiftUI/HomeTabView.swift b/packages/expo-dev-launcher/ios/SwiftUI/HomeTabView.swift index 837f98f8131390..7ed647351ef944 100644 --- a/packages/expo-dev-launcher/ios/SwiftUI/HomeTabView.swift +++ b/packages/expo-dev-launcher/ios/SwiftUI/HomeTabView.swift @@ -17,6 +17,12 @@ struct HomeTabView: View { crashReportBanner } + #if !targetEnvironment(simulator) + if viewModel.permissionStatus == .denied { + NetworkPermissionsBanner() + } + #endif + DevServersView(showingInfoDialog: $showingInfoDialog) if !viewModel.recentlyOpenedApps.isEmpty { @@ -76,6 +82,41 @@ struct HomeTabView: View { } } +struct NetworkPermissionsBanner: View { + var body: some View { + Button { +#if os(iOS) + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } +#endif + } label: { + HStack { + Image(systemName: "wifi.exclamationmark") + .font(.title2) + .foregroundColor(.orange) + VStack(alignment: .leading, spacing: 4) { + Text("Local Network Access Disabled") + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.primary) + Text("Dev servers can't be discovered. Tap to open Settings and enable Local Network access.") + .font(.footnote) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + } + Spacer() + Image(systemName: "gear") + .foregroundColor(.secondary) + } + .padding() + } + .buttonStyle(PlainButtonStyle()) + .background(Color.expoSecondarySystemGroupedBackground) + .cornerRadius(18) + } +} + #Preview { HomeTabView() } diff --git a/packages/expo-dev-launcher/ios/SwiftUI/LocalNetworkPermissionView.swift b/packages/expo-dev-launcher/ios/SwiftUI/LocalNetworkPermissionView.swift index d83f5f515fa12d..c645f6c9807d38 100644 --- a/packages/expo-dev-launcher/ios/SwiftUI/LocalNetworkPermissionView.swift +++ b/packages/expo-dev-launcher/ios/SwiftUI/LocalNetworkPermissionView.swift @@ -3,75 +3,207 @@ import SwiftUI struct LocalNetworkPermissionView: View { + @ObservedObject var viewModel: DevLauncherViewModel let onContinue: () -> Void + @State private var hasRequestedPermission = false + @State private var isCheckingAccess = false + @State private var showNoAccessMessage = false + @State private var showTryAgainFailedAlert = false + @State private var showAlreadyGrantedAlert = false + + private var isDenied: Bool { + viewModel.permissionStatus == .denied + } + var body: some View { VStack(spacing: 0) { Spacer() VStack(spacing: 24) { - Image(systemName: "wifi") - .font(.system(size: 64)) - .foregroundColor(.accentColor) + Image("radar-icon", bundle: getDevLauncherBundle()) + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + Text("Finding Dev Servers") + .font(.title) + .fontWeight(.bold) + + Text("Expo Dev Launcher needs to access your local network to discover development servers running on your computer.") + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + VStack(spacing: 12) { - Text("Find Dev Servers") - .font(.title) - .fontWeight(.bold) - - Text("Expo Dev Launcher needs to access your local network to discover development servers running on your computer.") - .font(.body) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) + if !hasRequestedPermission { + continueButton + } else if isDenied || showNoAccessMessage { + noAccessButtons + } else { + postRequestButtons + } } + } - HStack(alignment: .center, spacing: 8) { - Image(systemName: "info.circle") - Text("You'll see a system prompt asking for local network access.\nTap \"Allow\" to continue.") - .multilineTextAlignment(.center) + Spacer() + + HStack(alignment: .firstTextBaseline) { + Image(systemName: "info.circle") + Text("Dev servers advertise themselves on your local network using Bonjour. This permission allows the development client to discover them automatically.") + .fontWeight(.semibold) + } + .foregroundColor(.secondary) + } + .padding() + .background(Color.expoSystemBackground) + .alert("Permission Not Granted", isPresented: $showTryAgainFailedAlert) { + Button("Open Settings") { + #if os(iOS) + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) } - .frame(maxWidth: .infinity) - .padding() - .background(Color.expoSecondarySystemBackground) - .cornerRadius(12) - .font(.callout) + #endif + } + Button("OK", role: .cancel) {} + } message: { + Text("Local network access is still disabled. Enable it in Settings \u{2192} Privacy & Security \u{2192} Local Network to discover dev servers.") + } + .alert("Permission Already Granted", isPresented: $showAlreadyGrantedAlert) { + Button("OK") { + onContinue() + } + } message: { + Text("Local network access is already enabled. You're all set!") + .multilineTextAlignment(.center) + } + } + + private var continueButton: some View { + Group { + Button { + viewModel.startServerDiscovery() + hasRequestedPermission = true + } label: { + Text("Next") + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + .padding() + } + .background(Color.accentColor) + .foregroundColor(.white) + .cornerRadius(12) + + Text("When the system prompt pops up, tap \"Allow\" to continue.") + .font(.footnote) .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + } - Button { + private var postRequestButtons: some View { + Group { + Button { + onContinue() + } label: { + Text("Done") + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + .padding() + } + .background(Color.accentColor) + .foregroundColor(.white) + .cornerRadius(12) + + checkAccessButton(label: "I was not prompted") { hasAccess in + if hasAccess { + showAlreadyGrantedAlert = true + } else { + showNoAccessMessage = true + showTryAgainFailedAlert = true + } + } + } + } + + private var noAccessButtons: some View { + Group { + Text("Local network permission was not granted. Please enable it in Settings \u{2192} Privacy & Security \u{2192} Local Network.") + .font(.footnote) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + + Button { + #if os(iOS) + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } + #endif + } label: { + Text("Open Settings") + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + .padding() + } + .background(Color.expoSecondarySystemBackground) + .foregroundColor(.primary) + .cornerRadius(12) + + checkAccessButton(label: "Try Again") { hasAccess in + if hasAccess { + viewModel.startServerDiscovery() onContinue() - } label: { - Text("Continue") - .fontWeight(.semibold) - .frame(maxWidth: .infinity) - .padding() + } else { + showTryAgainFailedAlert = true } - .background(Color.accentColor) - .foregroundColor(.white) - .cornerRadius(12) } - Spacer() + Button { + onContinue() + } label: { + Text("Continue Anyway") + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + .padding() + } + .background(Color.expoSecondarySystemBackground) + .foregroundColor(.secondary) + .cornerRadius(12) + } + } - VStack(spacing: 4) { - Text("Why is this needed?") - .font(.footnote) - .fontWeight(.medium) - Text("Dev servers advertise themselves on your local network using Bonjour. This permission allows the app to discover them automatically.") - .font(.caption) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) + private func checkAccessButton(label: String, onResult: @escaping (Bool) -> Void) -> some View { + Button { + isCheckingAccess = true + viewModel.stopServerDiscovery() + Task { + let hasAccess = await viewModel.checkLocalNetworkAccess() + isCheckingAccess = false + onResult(hasAccess) + } + } label: { + if isCheckingAccess { + ProgressView() + .frame(maxWidth: .infinity) + .padding() + } else { + Text(label) + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + .padding() } } - .padding(.horizontal, 24) - .padding(.vertical, 32) - .background(Color.expoSystemBackground) + .disabled(isCheckingAccess) + .background(Color.expoSecondarySystemBackground) + .foregroundColor(.secondary) + .cornerRadius(12) } } #if DEBUG struct LocalNetworkPermissionView_Previews: PreviewProvider { static var previews: some View { - LocalNetworkPermissionView(onContinue: {}) + LocalNetworkPermissionView(viewModel: DevLauncherViewModel(), onContinue: {}) } } #endif diff --git a/packages/expo-dev-launcher/ios/SwiftUI/SettingsTabView.swift b/packages/expo-dev-launcher/ios/SwiftUI/SettingsTabView.swift index ede6a5bc3bf39c..7899349c026e0d 100644 --- a/packages/expo-dev-launcher/ios/SwiftUI/SettingsTabView.swift +++ b/packages/expo-dev-launcher/ios/SwiftUI/SettingsTabView.swift @@ -11,8 +11,6 @@ struct SettingsTabView: View { @State private var showCopiedMessage = false @State private var defaultPageSize: Int = 10 @State private var showCacheClearedMessage = false - @State private var permissionCheckResult: String = "Not checked" - @State private var isCheckingPermission = false private func createBuildInfoJSON() -> String { let buildInfoDict: [String: Any] = [ @@ -41,6 +39,10 @@ struct SettingsTabView: View { .font(.system(size: 13)) .foregroundStyle(.secondary) + #if !targetEnvironment(simulator) + localNetworkDebugSettings + #endif + VStack(alignment: .leading, spacing: 8) { Text("system".uppercased()) .font(.caption) @@ -51,10 +53,6 @@ struct SettingsTabView: View { copyToClipboardButton } - #if DEBUG - localNetworkDebugSettings - #endif - if isAdminUser { debugSettings easUpdateConfig @@ -69,11 +67,12 @@ struct SettingsTabView: View { #if !os(macOS) .navigationBarHidden(true) #endif - #if DEBUG - .onChange(of: viewModel.permissionStatus) { _ in - if isCheckingPermission { - updatePermissionResultFromStatus() - } + #if os(iOS) && !targetEnvironment(simulator) + .task { + viewModel.refreshPermissionStatus() + } + .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in + viewModel.refreshPermissionStatus() } #endif } @@ -238,114 +237,38 @@ struct SettingsTabView: View { """ } - #if DEBUG + #if !targetEnvironment(simulator) private var localNetworkDebugSettings: some View { VStack(alignment: .leading, spacing: 8) { - Text("Local Network Permission".uppercased()) - .font(.caption) - .foregroundColor(.primary.opacity(0.6)) - VStack(spacing: 0) { - HStack { - Text("Permission Status") - Spacer() - Text(permissionCheckResult) - .foregroundColor(.secondary) - } - .padding() - - Divider() + Toggle("Local Network", isOn: .constant(viewModel.permissionStatus == .granted)) + .disabled(true) + .padding() - HStack { - Text("First Launch Check") - Spacer() - Text(viewModel.hasGrantedNetworkPermission ? "Granted" : "Pending") - .foregroundColor(.secondary) - } - .padding() - - Divider() + if viewModel.permissionStatus == .denied { + Divider() - Button { - checkNetworkPermission() - } label: { - HStack { - if isCheckingPermission { - ProgressView() - .scaleEffect(0.8) + #if os(iOS) + Button { + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } + } label: { + HStack { + Text("Open App Settings") + Spacer() + Image(systemName: "gear") + .foregroundColor(.blue) } - Text(isCheckingPermission ? "Checking..." : "Check Permission Now") - Spacer() - Image(systemName: "wifi") - .foregroundColor(.blue) - } - } - .disabled(isCheckingPermission) - .padding() - - Divider() - - Button { - resetPermissionFlow() - } label: { - HStack { - Text("Reset Permission Flow") - Spacer() - Image(systemName: "arrow.counterclockwise") - .foregroundColor(.orange) - } - } - .padding() - - Divider() - - #if os(iOS) - Button { - if let url = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(url) - } - } label: { - HStack { - Text("Open App Settings") - Spacer() - Image(systemName: "gear") - .foregroundColor(.blue) } + .padding() + #endif } - .padding() - #endif } .background(Color.expoSecondarySystemBackground) .cornerRadius(12) - - Text("Use these tools to debug local network permission flow. 'Reset Permission Flow' will show the pre-flight screen again on next launch.") - .font(.system(size: 13)) - .foregroundStyle(.secondary) } } - - private func checkNetworkPermission() { - isCheckingPermission = true - permissionCheckResult = "Checking..." - viewModel.stopServerDiscovery() - viewModel.startServerDiscovery() - } - - private func updatePermissionResultFromStatus() { - isCheckingPermission = false - if viewModel.hasGrantedNetworkPermission { - permissionCheckResult = "✅ Granted" - } else if viewModel.permissionStatus == .denied { - permissionCheckResult = "❌ Denied" - } else { - permissionCheckResult = "⚠️ Unknown" - } - } - - private func resetPermissionFlow() { - viewModel.resetPermissionFlowState() - permissionCheckResult = "Not checked" - } #endif } From a88fab55a1b2a8a7a7c0d66376c698459d66f3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Dr=C3=B3=C5=BCd=C5=BC?= <31368152+behenate@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:20:07 +0100 Subject: [PATCH 02/17] [video] Add `name`, `isDefault` and `autoSelect` fields to audio and subtitle tracks (#43250) # Why While creating our HLS test stream i learned about these fields and they are important for track selection so it's good to expose them to JS # How Extract and add the `NAME`, `DEFAULT`, and `AUTOSELECT` tags from the manifest # Test Plan Tested in BareExpo on Android and iOS --- .../screens/Video/VideoAudioTracksScreen.tsx | 17 ++++---- packages/expo-video/CHANGELOG.md | 1 + .../java/expo/modules/video/records/Tracks.kt | 26 ++++++++++-- .../expo-video/build/VideoPlayer.types.d.ts | 36 ++++++++++++++++ .../build/VideoPlayer.types.d.ts.map | 2 +- .../expo-video/build/VideoPlayer.types.js.map | 2 +- packages/expo-video/ios/Records/Tracks.swift | 38 +++++++++++++++-- .../ios/VideoPlayerAudioTracks.swift | 4 +- .../expo-video/ios/VideoPlayerSubtitles.swift | 4 +- packages/expo-video/src/VideoPlayer.types.ts | 42 +++++++++++++++++++ 10 files changed, 151 insertions(+), 21 deletions(-) diff --git a/apps/native-component-list/src/screens/Video/VideoAudioTracksScreen.tsx b/apps/native-component-list/src/screens/Video/VideoAudioTracksScreen.tsx index 64aa1767566d77..a13e4844f87633 100644 --- a/apps/native-component-list/src/screens/Video/VideoAudioTracksScreen.tsx +++ b/apps/native-component-list/src/screens/Video/VideoAudioTracksScreen.tsx @@ -75,13 +75,16 @@ export default function VideoAudioTracksScreen() { handleAudioTrackChange(value); }}> {availableAudioTracks && - availableAudioTracks.map((source, index) => ( - - ))} + availableAudioTracks.map((source, index) => { + let label = availableAudioTracks[index]?.label ?? 'Off'; + const name = availableAudioTracks[index]?.name; + // Apple uses a weird algorithm to determine whether to add the name tag to the track label + // This way we will get the same results on Android and iOS in the picker + if (name && !label.includes(name)) { + label = `${name} - ${label}`; + } + return ; + })} Current audio track: {audioTrack?.label ?? availableAudioTracks[0]?.label} diff --git a/packages/expo-video/CHANGELOG.md b/packages/expo-video/CHANGELOG.md index 760de9a677a2f6..35324684ccd444 100644 --- a/packages/expo-video/CHANGELOG.md +++ b/packages/expo-video/CHANGELOG.md @@ -7,6 +7,7 @@ ### 🎉 New features - [Android][iOS] Add `url` field to HLS video tracks. ([#41681](https://github.com/expo/expo/pull/41681) by [@behenate](https://github.com/behenate)) +- [Android][iOS] Add `name`, `isDefault` and `autoSelect` fields to `AudioTrack` and `SubtitleTrack`. ([#43250](https://github.com/expo/expo/pull/43250) by [@behenate](https://github.com/behenate)) ### 🐛 Bug fixes diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/records/Tracks.kt b/packages/expo-video/android/src/main/java/expo/modules/video/records/Tracks.kt index f39e7afc2fc022..93d3ef1205e4a2 100644 --- a/packages/expo-video/android/src/main/java/expo/modules/video/records/Tracks.kt +++ b/packages/expo-video/android/src/main/java/expo/modules/video/records/Tracks.kt @@ -12,7 +12,10 @@ import java.util.Locale class SubtitleTrack( @Field val id: String, @Field val language: String?, - @Field val label: String? + @Field val label: String?, + @Field val name: String?, + @Field val isDefault: Boolean, + @Field val autoSelect: Boolean ) : Record, Serializable { companion object { fun fromFormat(format: Format?): SubtitleTrack? { @@ -20,11 +23,17 @@ class SubtitleTrack( val id = format.id ?: return null val language = format.language ?: return null val label = Locale(language).displayLanguage + val name = format.label + val isDefault = (format.selectionFlags and androidx.media3.common.C.SELECTION_FLAG_DEFAULT) != 0 + val autoSelect = (format.selectionFlags and androidx.media3.common.C.SELECTION_FLAG_AUTOSELECT) != 0 return SubtitleTrack( id = id, language = language, - label = label + label = label, + name = name, + isDefault = isDefault, + autoSelect = autoSelect ) } } @@ -33,7 +42,10 @@ class SubtitleTrack( class AudioTrack( @Field val id: String, @Field val language: String?, - @Field val label: String? + @Field val label: String?, + @Field val name: String?, + @Field val isDefault: Boolean, + @Field val autoSelect: Boolean ) : Record, Serializable { companion object { fun fromFormat(format: Format?): AudioTrack? { @@ -41,11 +53,17 @@ class AudioTrack( val id = format.id ?: return null val language = format.language val label = language?.let { Locale(it).displayLanguage } ?: "Unknown" + val name = format.label + val isDefault = (format.selectionFlags and androidx.media3.common.C.SELECTION_FLAG_DEFAULT) != 0 + val autoSelect = (format.selectionFlags and androidx.media3.common.C.SELECTION_FLAG_AUTOSELECT) != 0 return AudioTrack( id = id, language = language, - label = label + label = label, + name = name, + isDefault = isDefault, + autoSelect = autoSelect ) } } diff --git a/packages/expo-video/build/VideoPlayer.types.d.ts b/packages/expo-video/build/VideoPlayer.types.d.ts index 48cfe68e3f71b8..458dd2387915d2 100644 --- a/packages/expo-video/build/VideoPlayer.types.d.ts +++ b/packages/expo-video/build/VideoPlayer.types.d.ts @@ -508,6 +508,24 @@ export type SubtitleTrack = { * Label of the subtitle track in the language of the device. */ label: string; + /** + * Name of the subtitle track as specified in the media source. + * @platform android + * @platform ios + */ + name?: string; + /** + * Indicates whether this is the default subtitle track. + * @platform android + * @platform ios + */ + isDefault?: boolean; + /** + * Indicates whether this track should be auto-selected based on user preferences. + * @platform android + * @platform ios + */ + autoSelect?: boolean; }; /** * Specifies a VideoTrack loaded from a [`VideoSource`](#videosource). @@ -584,6 +602,24 @@ export type AudioTrack = { * Label of the audio track in the language of the device. */ label: string; + /** + * Name of the audio track as specified in the media source. + * @platform android + * @platform ios + */ + name?: string; + /** + * Indicates whether this is the default audio track. + * @platform android + * @platform ios + */ + isDefault?: boolean; + /** + * Indicates whether this track should be auto-selected based on user preferences. + * @platform android + * @platform ios + */ + autoSelect?: boolean; }; /** * Determines the time that the actual position seeked to may precede or exceed the requested seek position. diff --git a/packages/expo-video/build/VideoPlayer.types.d.ts.map b/packages/expo-video/build/VideoPlayer.types.d.ts.map index 47357a586f1108..fdf2d0d0fe520f 100644 --- a/packages/expo-video/build/VideoPlayer.types.d.ts.map +++ b/packages/expo-video/build/VideoPlayer.types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"VideoPlayer.types.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"VideoPlayer.types.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/expo-video/build/VideoPlayer.types.js.map b/packages/expo-video/build/VideoPlayer.types.js.map index 54a1c17d84ecef..4f596535d081ae 100644 --- a/packages/expo-video/build/VideoPlayer.types.js.map +++ b/packages/expo-video/build/VideoPlayer.types.js.map @@ -1 +1 @@ -{"version":3,"file":"VideoPlayer.types.js","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"","sourcesContent":["import { SharedObject } from 'expo';\n\nimport { VideoPlayerEvents } from './VideoPlayerEvents.types';\nimport { VideoThumbnail } from './VideoThumbnail';\n\n/**\n * A class that represents an instance of the video player.\n */\nexport declare class VideoPlayer extends SharedObject {\n /**\n * Boolean value whether the player is currently playing.\n * > Use `play` and `pause` methods to control the playback.\n */\n readonly playing: boolean;\n\n /**\n * Determines whether the player should automatically replay after reaching the end of the video.\n * @default false\n */\n loop: boolean;\n\n /**\n * Determines whether the player should allow external playback.\n * @default true\n * @platform ios\n */\n allowsExternalPlayback: boolean;\n\n /**\n * Determines how the player will interact with other audio playing in the system.\n *\n * @default 'auto'\n * @platform android\n * @platform ios\n */\n audioMixingMode: AudioMixingMode;\n\n /**\n * Boolean value whether the player is currently muted.\n * Setting this property to `true`/`false` will mute/unmute the player.\n * @default false\n */\n muted: boolean;\n\n /**\n * Float value indicating the current playback time in seconds.\n *\n * If the player is not yet playing, this value indicates the time position\n * at which playback will begin once the `play()` method is called.\n *\n * Setting `currentTime` to a new value seeks the player to the given time.\n * Check out the [`seekTolerance`](#seektolerance) property to configure the seeking precision.\n */\n currentTime: number;\n\n /**\n * The exact timestamp when the currently displayed video frame was sent from the server,\n * based on the `EXT-X-PROGRAM-DATE-TIME` tag in the livestream metadata.\n * If this metadata is missing, this property will return `null`.\n * @platform android\n * @platform ios\n */\n readonly currentLiveTimestamp: number | null;\n\n /**\n * Float value indicating the latency of the live stream in seconds.\n * If a livestream doesn't have the required metadata, this will return `null`.\n * @platform android\n * @platform ios\n */\n readonly currentOffsetFromLive: number | null;\n\n /**\n * Float value indicating the time offset from the live in seconds.\n * @platform ios\n */\n targetOffsetFromLive: number;\n\n /**\n * Float value indicating the duration of the current video in seconds.\n */\n readonly duration: number;\n\n /**\n * Float value between `0` and `1.0` representing the current volume.\n * Muting the player doesn't affect the volume. In other words, when the player is muted, the volume is the same as\n * when unmuted. Similarly, setting the volume doesn't unmute the player.\n * @default 1.0\n */\n volume: number;\n\n /**\n * Boolean value indicating if the player should correct audio pitch when the playback speed changes.\n * @default true\n */\n preservesPitch: boolean;\n\n /**\n * Float value indicating the interval in seconds at which the player will emit the [`timeUpdate`](#videoplayerevents) event.\n * When the value is equal to `0`, the event will not be emitted.\n *\n * @default 0\n */\n timeUpdateEventInterval: number;\n\n /**\n * Float value between `0` and `16.0` indicating the current playback speed of the player.\n * @default 1.0\n */\n playbackRate: number;\n\n /**\n * Boolean indicating if the player should keep the screen on while playing.\n *\n * > On Android, this property has an effect only when a [`VideoView`](#videoview) is visible. If you want to keep the screen awake at all times use [`expo-keep-awake`](./keep-awake/).\n *\n * @default true\n * @platform android\n * @platform ios\n */\n keepScreenOnWhilePlaying: boolean;\n\n /**\n * Boolean value indicating whether the player is currently playing a live stream.\n */\n readonly isLive: boolean;\n\n /**\n * Indicates the current status of the player.\n */\n readonly status: VideoPlayerStatus;\n\n /**\n * Boolean value determining whether the player should show the now playing notification.\n *\n * > **Note**: On Android, `supportsBackgroundPlayback` property of the [config plugin](#configuration-in-app-config)\n * > has to be `true` for the now playing notification to work.\n * @default false\n * @platform android\n * @platform ios\n */\n showNowPlayingNotification: boolean;\n\n /**\n * Determines whether the player should continue playing after the app enters the background.\n *\n * > **Note**: The `supportsBackgroundPlayback` property of the [config plugin](#configuration-in-app-config)\n * > has to be `true` for the background playback to work.\n * @default false\n * @platform ios\n * @platform android\n */\n staysActiveInBackground: boolean;\n\n /**\n * Float value indicating how far the player has buffered the video in seconds.\n *\n * This value is 0 when the player has not buffered up to the current playback time.\n * When it's impossible to determine the buffer state (for example, when the player isn't playing any media), this value is -1.\n */\n readonly bufferedPosition: number;\n\n /**\n * Specifies buffer options which will be used by the player when buffering the video.\n *\n * > You should provide a `BufferOptions` object when setting this property. Setting individual buffer properties is not supported.\n * @platform android\n * @platform ios\n */\n bufferOptions: BufferOptions;\n\n /**\n * Specifies the subtitle track which is currently displayed by the player. `null` when no subtitles are displayed.\n *\n * > To ensure a valid subtitle track, always assign one of the subtitle tracks from the [`availableSubtitleTracks`](#availablesubtitletracks) array.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n subtitleTrack: SubtitleTrack | null;\n\n /**\n * Specifies the audio track currently played by the player. `null` when no audio is played.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n audioTrack: AudioTrack | null;\n\n /**\n * An array of audio tracks available for the current video.\n *\n * @platform android\n * @platform ios\n */\n readonly availableAudioTracks: AudioTrack[];\n\n /**\n * An array of subtitle tracks available for the current video.\n *\n * @platform android\n * @platform ios\n */\n readonly availableSubtitleTracks: SubtitleTrack[];\n\n /**\n * Specifies the video track currently played by the player. `null` when no video is displayed.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n readonly videoTrack: VideoTrack | null;\n\n /**\n * An array of video tracks available for the current video.\n *\n * > On iOS, when using a HLS source, make sure that the uri contains `.m3u8` extension or that the [`contentType`](#contenttype) property of the [`VideoSource`](#videosource) has been set to `'hls'`. Otherwise, the video tracks will not be available.\n *\n * @platform android\n * @platform ios\n */\n readonly availableVideoTracks: VideoTrack[];\n\n /**\n * Indicates whether the player is currently playing back the media to an external device via AirPlay.\n *\n * @platform ios\n */\n readonly isExternalPlaybackActive: boolean;\n\n /**\n * Determines the time that the actual position seeked to may precede or exceed the requested seek position.\n *\n * This property affects the precision of setting the [`currentTime`](#currenttime) property and the [`seekBy`](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n *\n * By default, the player seeks to the exact requested time.\n *\n * > If you are trying to optimize for scrubbing (many frequent seeks), also see [`ScrubbingModeOptions`](#scrubbingmodeoptions-1).\n */\n seekTolerance: SeekTolerance;\n\n /**\n * Determines whether the scrubbing mode is enabled and what scrubbing optimizations should be enabled.\n *\n * > See [`SeekTolerance`](#seektolerance) to set the seeking tolerance, which can also affect the scrubbing performance.\n *\n */\n scrubbingModeOptions: ScrubbingModeOptions;\n\n /**\n * Initializes a new video player instance with the given source.\n *\n * @param source The source of the video to be played.\n * @param useSynchronousReplace Optional parameter, when `true` `source` from the first parameter will be loaded on the main thread.\n * @param playerBuilderOptions Options to apply to the player builder before the native constructor is invoked.\n * @hidden\n */\n constructor(\n source: VideoSource,\n useSynchronousReplace?: boolean,\n playerBuilderOptions?: PlayerBuilderOptions\n );\n\n /**\n * Resumes the player.\n */\n play(): void;\n\n /**\n * Pauses the player.\n */\n pause(): void;\n\n /**\n * Replaces the current source with a new one.\n *\n * > On iOS, this method loads the asset data synchronously on the UI thread and can block it for extended periods of time.\n * > Use `replaceAsync` to load the asset asynchronously and avoid UI lags.\n *\n * > This method will be deprecated in the future.\n */\n replace(source: VideoSource, disableWarning?: boolean): void;\n\n /**\n * Replaces the current source with a new one, while offloading loading of the asset to a different thread.\n *\n * > On Android and Web, this method is equivalent to `replace`.\n */\n replaceAsync(source: VideoSource): Promise;\n\n /**\n * Seeks the playback by the given number of seconds. The time to which the player seeks may differ from the specified requested time for efficiency,\n * depending on the encoding and what is currently buffered by the player. Use this function to implement playback controls that seek by specific amount of time,\n * in which case, the actual time usually does not have to be precise. For frame accurate seeking, use the [`currentTime`](#currenttime) property.\n */\n seekBy(seconds: number): void;\n\n /**\n * Seeks the playback to the beginning.\n */\n replay(): void;\n\n /**\n * Generates thumbnails from the currently played asset. The thumbnails are references to native images,\n * thus they can be used as a source of the `Image` component from `expo-image`.\n * @platform android\n * @platform ios\n */\n generateThumbnailsAsync(\n times: number | number[],\n options?: VideoThumbnailOptions\n ): Promise;\n}\n\n/**\n * Additional options for video thumbnails generation.\n */\nexport type VideoThumbnailOptions = {\n /**\n * If provided, the generated thumbnail will not exceed this width in pixels, preserving its aspect ratio.\n * @platform android\n * @platform ios\n */\n maxWidth?: number;\n\n /**\n * If provided, the generated thumbnail will not exceed this height in pixels, preserving its aspect ratio.\n * @platform android\n * @platform ios\n */\n maxHeight?: number;\n};\n\n/**\n * Describes the current status of the player.\n * - `idle`: The player is not playing or loading any videos.\n * - `loading`: The player is loading video data from the provided source\n * - `readyToPlay`: The player has loaded enough data to start playing or to continue playback.\n * - `error`: The player has encountered an error while loading or playing the video.\n */\nexport type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';\n\nexport type VideoSource = string | number | null | VideoSourceObject;\n\nexport type VideoSourceObject = {\n /**\n * The URI of the video.\n *\n * On iOS, `PHAsset` URIs are supported, but can only be loaded using the [`replaceAsync`](#replaceasyncsource) method or the default [`VideoPlayer`](#videoplayer) constructor.\n *\n * This property is exclusive with the `assetId` property. When both are present, the `assetId` will be ignored.\n */\n uri?: string;\n\n /**\n * The asset ID of a local video asset, acquired with the `require` function.\n * This property is exclusive with the `uri` property. When both are present, the `assetId` will be ignored.\n */\n assetId?: number;\n\n /**\n * Specifies the DRM options which will be used by the player while loading the video.\n */\n drm?: DRMOptions;\n\n /**\n * Specifies information which will be displayed in the now playing notification.\n * When undefined the player will display information contained in the video metadata.\n * @platform android\n * @platform ios\n */\n metadata?: VideoMetadata;\n\n /**\n * Specifies headers sent with the video request.\n * > For DRM license headers use the `headers` field of [`DRMOptions`](#drmoptions).\n * @platform android\n * @platform ios\n */\n headers?: Record;\n\n /**\n * Specifies whether the player should use caching for the video.\n * > Due to platform limitations, the cache cannot be used with HLS video sources on iOS. Caching DRM-protected videos is not supported on Android and iOS.\n * @default false\n * @platform android\n * @platform ios\n */\n useCaching?: boolean;\n\n /**\n * Specifies the content type of the video source. When set to `'auto'`, the player will try to automatically determine the content type.\n *\n * You should use this property when playing HLS, SmoothStreaming or DASH videos from an uri, which does not contain a standardized extension for the corresponding media type.\n * @default 'auto'\n * @platform android\n * @platform ios\n */\n contentType?: ContentType;\n};\n\n/**\n * Contains information about any errors that the player encountered during the playback\n */\nexport type PlayerError = {\n message: string;\n};\n\n/**\n * Contains information that will be displayed in the now playing notification when the video is playing.\n * @platform android\n * @platform ios\n */\nexport type VideoMetadata = {\n /**\n * The title of the video.\n * @platform android\n * @platform ios\n */\n title?: string;\n /**\n * Secondary text that will be displayed under the title.\n * @platform android\n * @platform ios\n */\n artist?: string;\n /**\n * The uri of the video artwork.\n * @platform android\n * @platform ios\n */\n artwork?: string;\n};\n\n/**\n * Specifies which type of DRM to use:\n * - Android supports ClearKey, PlayReady and Widevine.\n * - iOS supports FairPlay.\n */\nexport type DRMType = 'clearkey' | 'fairplay' | 'playready' | 'widevine';\n\n/**\n * Specifies DRM options which will be used by the player while loading the video.\n */\nexport type DRMOptions = {\n /**\n * Determines which type of DRM to use.\n */\n type: DRMType;\n\n /**\n * Determines the license server URL.\n */\n licenseServer: string;\n\n /**\n * Determines headers sent to the license server on license requests.\n */\n headers?: Record;\n\n /**\n * Specifies whether the DRM is a multi-key DRM.\n * @platform android\n */\n multiKey?: boolean;\n\n /**\n * Specifies the content ID of the stream.\n * @platform ios\n */\n contentId?: string;\n\n /**\n * Specifies the certificate URL for the FairPlay DRM.\n * @platform ios\n */\n certificateUrl?: string;\n\n /**\n * Specifies the base64 encoded certificate data for the FairPlay DRM.\n * When this property is set, the `certificateUrl` property is ignored.\n * @platform ios\n */\n base64CertificateData?: string;\n};\n\n/**\n * Specifies buffer options which will be used by the player when buffering the video.\n *\n * @platform android\n * @platform ios\n */\nexport type BufferOptions = {\n /**\n * The duration in seconds which determines how much media the player should buffer ahead of the current playback time.\n *\n * On iOS when set to `0` the player will automatically decide appropriate buffer duration.\n *\n * Equivalent to [`AVPlayerItem.preferredForwardBufferDuration`](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration).\n * @default Android: 20, iOS: 0\n * @platform android\n * @platform ios\n */\n readonly preferredForwardBufferDuration?: number;\n\n /**\n * A Boolean value that indicates whether the player should automatically delay playback in order to minimize stalling.\n *\n * Equivalent to [`AVPlayer.automaticallyWaitsToMinimizeStalling`](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal).\n * @default true\n * @platform ios\n */\n readonly waitsToMinimizeStalling?: boolean;\n\n /**\n * Minimum duration of the buffer in seconds required to continue playing after the player has been paused or started buffering.\n *\n * > This property will be ignored if `preferredForwardBufferDuration` is lower.\n * @default 2\n * @platform android\n */\n readonly minBufferForPlayback?: number;\n\n /**\n * The maximum number of bytes that the player can buffer from the network.\n * When 0 the player will automatically decide appropriate buffer size.\n *\n * @default 0\n * @platform android\n */\n readonly maxBufferBytes?: number | null;\n\n /**\n * A Boolean value which determines whether the player should prioritize time over size when buffering media.\n *\n * @default false\n * @platform android\n */\n readonly prioritizeTimeOverSizeThreshold?: boolean;\n};\n\n/**\n * Specifies the content type of the source.\n *\n * - `auto`: The player will automatically determine the content type of the video.\n * - `progressive`: The player will use progressive download content type. This is the default `ContentType` when the uri does not contain an extension.\n * - `hls`: The player will use HLS content type.\n * - `dash`: The player will use DASH content type (Android-only).\n * - `smoothStreaming`: The player will use SmoothStreaming content type (Android-only).\n *\n * @default `auto`\n */\nexport type ContentType = 'auto' | 'progressive' | 'hls' | 'dash' | 'smoothStreaming';\n\n/**\n * Specifies the audio mode that the player should use. Audio mode is set on per-app basis, if there are multiple players playing and\n * have different a `AudioMode` specified, the highest priority mode will be used. Priority order: 'doNotMix' > 'auto' > 'duckOthers' > 'mixWithOthers'.\n *\n * - `mixWithOthers`: The player will mix its audio output with other apps.\n * - `duckOthers`: The player will lower the volume of other apps if any of the active players is outputting audio.\n * - `auto`: The player will allow other apps to keep playing audio only when it is muted. On iOS it will always interrupt other apps when `showNowPlayingNotification` is `true` due to system requirements.\n * - `doNotMix`: The player will pause playback in other apps, even when it's muted.\n *\n * > On iOS, the Now Playing notification is dependent on the audio mode. If the audio mode is different from `doNotMix` or `auto` this feature will not work.\n */\nexport type AudioMixingMode = 'mixWithOthers' | 'duckOthers' | 'auto' | 'doNotMix';\n\nexport type SubtitleTrack = {\n /**\n * A string used by `expo-video` to identify the subtitle track.\n *\n * @platform android\n */\n id?: string;\n\n /**\n * Language of the subtitle track. For example, `en`, `pl`, `de`.\n */\n language: string;\n\n /**\n * Label of the subtitle track in the language of the device.\n */\n label: string;\n};\n\n/**\n * Specifies a VideoTrack loaded from a [`VideoSource`](#videosource).\n */\nexport type VideoTrack = {\n /**\n * The id of the video track.\n *\n * > This field is platform-specific and may return different depending on the operating system.\n */\n id: string;\n\n /**\n * The URL of the `VideoTrack` for HLS video sources. `null` for other source types.\n */\n url: string | null;\n\n /**\n * Size of the video track.\n */\n size: VideoSize;\n\n /**\n * MimeType of the video track or null if unknown.\n */\n mimeType: string | null;\n\n /**\n * Indicates whether the video track format is supported by the device.\n *\n * @platform android\n */\n isSupported: boolean;\n\n /**\n * Specifies the bitrate in bits per second. This is the peak bitrate if known, or else the average bitrate if known, or else null.\n *\n * @deprecated Use `peakBitrate` or `averageBitrate` instead.\n */\n bitrate: number | null;\n\n /**\n * Specifies the average bitrate in bits per second or null if the value is unknown.\n *\n */\n averageBitrate: number | null;\n\n /**\n * Specifies the average bitrate in bits per second or null if the value is unknown.\n */\n peakBitrate: number | null;\n\n /**\n * Specifies the frame rate of the video track in frames per second.\n */\n frameRate: number | null;\n};\n\n/**\n * Specifies the size of a video track.\n */\nexport type VideoSize = {\n /**\n * Width of the video track in pixels.\n */\n width: number;\n /**\n * Height of the video track in pixels.\n */\n height: number;\n};\n\nexport type AudioTrack = {\n /**\n * A string used by expo-video to identify the audio track.\n * @platform android\n */\n id?: string;\n\n /**\n * Language of the audio track. For example, 'en', 'pl', 'de'.\n */\n language: string;\n\n /**\n * Label of the audio track in the language of the device.\n */\n label: string;\n};\n\n/**\n * Determines the time that the actual position seeked to may precede or exceed the requested seek position.\n * Larger tolerance will usually result in faster seeking.\n * This property affects the precision of setting the [`currentTime`](#currenttime) property and the [`seekBy`](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n *\n * > If you are trying to optimize for scrubbing (many frequent seeks), also see [`ScrubbingModeOptions`](#scrubbingmodeoptions-1).\n *\n * @platform android\n * @platform ios\n */\nexport type SeekTolerance = {\n /**\n * The maximum time that the actual position seeked to may precede the requested seek position, in seconds. Must be non-negative.\n * @default 0\n */\n toleranceBefore?: number;\n\n /**\n * The maximum time that the actual position seeked to may exceed the requested seek position, in seconds. Must be non-negative.\n * @default 0\n */\n toleranceAfter?: number;\n};\n\n/**\n * Defines scrubbing mode options used by a [`VideoPlayer`](#videoplayer).\n */\nexport type ScrubbingModeOptions = {\n /**\n * Whether the codec operating rate should be increased in scrubbing mode.\n *\n * You should only enable this when the player is receiving a large number of seeks in a short period of time. For less frequent seeks, fine-tuning the [`SeekTolerance`](#seektolerance-1) may be sufficient.\n *\n * On Android, the player may consume more resources in this mode, so it should only be used for short periods of time in response to user interaction (for example, dragging on a progress bar UI element).\n *\n * On Android, when `scrubbingModeEnabled` is `true`, the playback is suppressed. You should set this property back to `false` when the user interaction ends to allow the playback to resume.\n * For best results, on iOS you should pause the playback when scrubbing.\n *\n * > For best scrubbing performance, consider also increasing the seeking tolerance using the [`SeekTolerance`](#seektolerance-1) property.\n *\n * > Other scrubbing mode options will have no effect when this is `false`.\n * @default false\n * @platform android\n * @platform ios\n */\n scrubbingModeEnabled?: boolean;\n\n /**\n * Whether the codec operating rate should be increased in scrubbing mode.\n *\n * @platform android\n * @default true\n */\n increaseCodecOperatingRate?: boolean;\n\n /**\n * Sets whether ExoPlayer's dynamic scheduling should be enabled in scrubbing mode.\n * This can result in available output buffers being handled more quickly when seeking.\n *\n * @platform android\n * @default true\n */\n enableDynamicScheduling?: boolean;\n /**\n * Sets whether to use `MediaCodec.BUFFER_FLAG_DECODE_ONLY` in scrubbing mode.\n * When playback is using MediaCodec on API 34+, this flag can speed up seeking by signalling that the decoded output of buffers between the previous keyframe and the target frame is not needed by the player.\n *\n * @platform android\n * @default true\n */\n useDecodeOnlyFlag?: boolean;\n\n /**\n * Sets whether to avoid flushing the decoder (where possible) in scrubbing mode.\n * When `true`, avoids flushing the decoder when a new seek starts decoding from a key-frame in compatible content.\n *\n * @platform android\n * @default true\n */\n allowSkippingMediaCodecFlush?: boolean;\n};\n\n/**\n * Options to apply to the player builder before the native constructor is invoked\n * @platform android\n */\nexport type PlayerBuilderOptions = {\n /**\n * Seek backward increment in seconds.\n * Values will be clamped between 0.001 and 999 seconds.\n * @platform android\n */\n seekBackwardIncrement?: number;\n\n /**\n * Seek forward increment in seconds.\n * Values will be clamped between 0.001 and 999 seconds.\n * @platform android\n */\n seekForwardIncrement?: number;\n};\n"]} \ No newline at end of file +{"version":3,"file":"VideoPlayer.types.js","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"","sourcesContent":["import { SharedObject } from 'expo';\n\nimport { VideoPlayerEvents } from './VideoPlayerEvents.types';\nimport { VideoThumbnail } from './VideoThumbnail';\n\n/**\n * A class that represents an instance of the video player.\n */\nexport declare class VideoPlayer extends SharedObject {\n /**\n * Boolean value whether the player is currently playing.\n * > Use `play` and `pause` methods to control the playback.\n */\n readonly playing: boolean;\n\n /**\n * Determines whether the player should automatically replay after reaching the end of the video.\n * @default false\n */\n loop: boolean;\n\n /**\n * Determines whether the player should allow external playback.\n * @default true\n * @platform ios\n */\n allowsExternalPlayback: boolean;\n\n /**\n * Determines how the player will interact with other audio playing in the system.\n *\n * @default 'auto'\n * @platform android\n * @platform ios\n */\n audioMixingMode: AudioMixingMode;\n\n /**\n * Boolean value whether the player is currently muted.\n * Setting this property to `true`/`false` will mute/unmute the player.\n * @default false\n */\n muted: boolean;\n\n /**\n * Float value indicating the current playback time in seconds.\n *\n * If the player is not yet playing, this value indicates the time position\n * at which playback will begin once the `play()` method is called.\n *\n * Setting `currentTime` to a new value seeks the player to the given time.\n * Check out the [`seekTolerance`](#seektolerance) property to configure the seeking precision.\n */\n currentTime: number;\n\n /**\n * The exact timestamp when the currently displayed video frame was sent from the server,\n * based on the `EXT-X-PROGRAM-DATE-TIME` tag in the livestream metadata.\n * If this metadata is missing, this property will return `null`.\n * @platform android\n * @platform ios\n */\n readonly currentLiveTimestamp: number | null;\n\n /**\n * Float value indicating the latency of the live stream in seconds.\n * If a livestream doesn't have the required metadata, this will return `null`.\n * @platform android\n * @platform ios\n */\n readonly currentOffsetFromLive: number | null;\n\n /**\n * Float value indicating the time offset from the live in seconds.\n * @platform ios\n */\n targetOffsetFromLive: number;\n\n /**\n * Float value indicating the duration of the current video in seconds.\n */\n readonly duration: number;\n\n /**\n * Float value between `0` and `1.0` representing the current volume.\n * Muting the player doesn't affect the volume. In other words, when the player is muted, the volume is the same as\n * when unmuted. Similarly, setting the volume doesn't unmute the player.\n * @default 1.0\n */\n volume: number;\n\n /**\n * Boolean value indicating if the player should correct audio pitch when the playback speed changes.\n * @default true\n */\n preservesPitch: boolean;\n\n /**\n * Float value indicating the interval in seconds at which the player will emit the [`timeUpdate`](#videoplayerevents) event.\n * When the value is equal to `0`, the event will not be emitted.\n *\n * @default 0\n */\n timeUpdateEventInterval: number;\n\n /**\n * Float value between `0` and `16.0` indicating the current playback speed of the player.\n * @default 1.0\n */\n playbackRate: number;\n\n /**\n * Boolean indicating if the player should keep the screen on while playing.\n *\n * > On Android, this property has an effect only when a [`VideoView`](#videoview) is visible. If you want to keep the screen awake at all times use [`expo-keep-awake`](./keep-awake/).\n *\n * @default true\n * @platform android\n * @platform ios\n */\n keepScreenOnWhilePlaying: boolean;\n\n /**\n * Boolean value indicating whether the player is currently playing a live stream.\n */\n readonly isLive: boolean;\n\n /**\n * Indicates the current status of the player.\n */\n readonly status: VideoPlayerStatus;\n\n /**\n * Boolean value determining whether the player should show the now playing notification.\n *\n * > **Note**: On Android, `supportsBackgroundPlayback` property of the [config plugin](#configuration-in-app-config)\n * > has to be `true` for the now playing notification to work.\n * @default false\n * @platform android\n * @platform ios\n */\n showNowPlayingNotification: boolean;\n\n /**\n * Determines whether the player should continue playing after the app enters the background.\n *\n * > **Note**: The `supportsBackgroundPlayback` property of the [config plugin](#configuration-in-app-config)\n * > has to be `true` for the background playback to work.\n * @default false\n * @platform ios\n * @platform android\n */\n staysActiveInBackground: boolean;\n\n /**\n * Float value indicating how far the player has buffered the video in seconds.\n *\n * This value is 0 when the player has not buffered up to the current playback time.\n * When it's impossible to determine the buffer state (for example, when the player isn't playing any media), this value is -1.\n */\n readonly bufferedPosition: number;\n\n /**\n * Specifies buffer options which will be used by the player when buffering the video.\n *\n * > You should provide a `BufferOptions` object when setting this property. Setting individual buffer properties is not supported.\n * @platform android\n * @platform ios\n */\n bufferOptions: BufferOptions;\n\n /**\n * Specifies the subtitle track which is currently displayed by the player. `null` when no subtitles are displayed.\n *\n * > To ensure a valid subtitle track, always assign one of the subtitle tracks from the [`availableSubtitleTracks`](#availablesubtitletracks) array.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n subtitleTrack: SubtitleTrack | null;\n\n /**\n * Specifies the audio track currently played by the player. `null` when no audio is played.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n audioTrack: AudioTrack | null;\n\n /**\n * An array of audio tracks available for the current video.\n *\n * @platform android\n * @platform ios\n */\n readonly availableAudioTracks: AudioTrack[];\n\n /**\n * An array of subtitle tracks available for the current video.\n *\n * @platform android\n * @platform ios\n */\n readonly availableSubtitleTracks: SubtitleTrack[];\n\n /**\n * Specifies the video track currently played by the player. `null` when no video is displayed.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n readonly videoTrack: VideoTrack | null;\n\n /**\n * An array of video tracks available for the current video.\n *\n * > On iOS, when using a HLS source, make sure that the uri contains `.m3u8` extension or that the [`contentType`](#contenttype) property of the [`VideoSource`](#videosource) has been set to `'hls'`. Otherwise, the video tracks will not be available.\n *\n * @platform android\n * @platform ios\n */\n readonly availableVideoTracks: VideoTrack[];\n\n /**\n * Indicates whether the player is currently playing back the media to an external device via AirPlay.\n *\n * @platform ios\n */\n readonly isExternalPlaybackActive: boolean;\n\n /**\n * Determines the time that the actual position seeked to may precede or exceed the requested seek position.\n *\n * This property affects the precision of setting the [`currentTime`](#currenttime) property and the [`seekBy`](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n *\n * By default, the player seeks to the exact requested time.\n *\n * > If you are trying to optimize for scrubbing (many frequent seeks), also see [`ScrubbingModeOptions`](#scrubbingmodeoptions-1).\n */\n seekTolerance: SeekTolerance;\n\n /**\n * Determines whether the scrubbing mode is enabled and what scrubbing optimizations should be enabled.\n *\n * > See [`SeekTolerance`](#seektolerance) to set the seeking tolerance, which can also affect the scrubbing performance.\n *\n */\n scrubbingModeOptions: ScrubbingModeOptions;\n\n /**\n * Initializes a new video player instance with the given source.\n *\n * @param source The source of the video to be played.\n * @param useSynchronousReplace Optional parameter, when `true` `source` from the first parameter will be loaded on the main thread.\n * @param playerBuilderOptions Options to apply to the player builder before the native constructor is invoked.\n * @hidden\n */\n constructor(\n source: VideoSource,\n useSynchronousReplace?: boolean,\n playerBuilderOptions?: PlayerBuilderOptions\n );\n\n /**\n * Resumes the player.\n */\n play(): void;\n\n /**\n * Pauses the player.\n */\n pause(): void;\n\n /**\n * Replaces the current source with a new one.\n *\n * > On iOS, this method loads the asset data synchronously on the UI thread and can block it for extended periods of time.\n * > Use `replaceAsync` to load the asset asynchronously and avoid UI lags.\n *\n * > This method will be deprecated in the future.\n */\n replace(source: VideoSource, disableWarning?: boolean): void;\n\n /**\n * Replaces the current source with a new one, while offloading loading of the asset to a different thread.\n *\n * > On Android and Web, this method is equivalent to `replace`.\n */\n replaceAsync(source: VideoSource): Promise;\n\n /**\n * Seeks the playback by the given number of seconds. The time to which the player seeks may differ from the specified requested time for efficiency,\n * depending on the encoding and what is currently buffered by the player. Use this function to implement playback controls that seek by specific amount of time,\n * in which case, the actual time usually does not have to be precise. For frame accurate seeking, use the [`currentTime`](#currenttime) property.\n */\n seekBy(seconds: number): void;\n\n /**\n * Seeks the playback to the beginning.\n */\n replay(): void;\n\n /**\n * Generates thumbnails from the currently played asset. The thumbnails are references to native images,\n * thus they can be used as a source of the `Image` component from `expo-image`.\n * @platform android\n * @platform ios\n */\n generateThumbnailsAsync(\n times: number | number[],\n options?: VideoThumbnailOptions\n ): Promise;\n}\n\n/**\n * Additional options for video thumbnails generation.\n */\nexport type VideoThumbnailOptions = {\n /**\n * If provided, the generated thumbnail will not exceed this width in pixels, preserving its aspect ratio.\n * @platform android\n * @platform ios\n */\n maxWidth?: number;\n\n /**\n * If provided, the generated thumbnail will not exceed this height in pixels, preserving its aspect ratio.\n * @platform android\n * @platform ios\n */\n maxHeight?: number;\n};\n\n/**\n * Describes the current status of the player.\n * - `idle`: The player is not playing or loading any videos.\n * - `loading`: The player is loading video data from the provided source\n * - `readyToPlay`: The player has loaded enough data to start playing or to continue playback.\n * - `error`: The player has encountered an error while loading or playing the video.\n */\nexport type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';\n\nexport type VideoSource = string | number | null | VideoSourceObject;\n\nexport type VideoSourceObject = {\n /**\n * The URI of the video.\n *\n * On iOS, `PHAsset` URIs are supported, but can only be loaded using the [`replaceAsync`](#replaceasyncsource) method or the default [`VideoPlayer`](#videoplayer) constructor.\n *\n * This property is exclusive with the `assetId` property. When both are present, the `assetId` will be ignored.\n */\n uri?: string;\n\n /**\n * The asset ID of a local video asset, acquired with the `require` function.\n * This property is exclusive with the `uri` property. When both are present, the `assetId` will be ignored.\n */\n assetId?: number;\n\n /**\n * Specifies the DRM options which will be used by the player while loading the video.\n */\n drm?: DRMOptions;\n\n /**\n * Specifies information which will be displayed in the now playing notification.\n * When undefined the player will display information contained in the video metadata.\n * @platform android\n * @platform ios\n */\n metadata?: VideoMetadata;\n\n /**\n * Specifies headers sent with the video request.\n * > For DRM license headers use the `headers` field of [`DRMOptions`](#drmoptions).\n * @platform android\n * @platform ios\n */\n headers?: Record;\n\n /**\n * Specifies whether the player should use caching for the video.\n * > Due to platform limitations, the cache cannot be used with HLS video sources on iOS. Caching DRM-protected videos is not supported on Android and iOS.\n * @default false\n * @platform android\n * @platform ios\n */\n useCaching?: boolean;\n\n /**\n * Specifies the content type of the video source. When set to `'auto'`, the player will try to automatically determine the content type.\n *\n * You should use this property when playing HLS, SmoothStreaming or DASH videos from an uri, which does not contain a standardized extension for the corresponding media type.\n * @default 'auto'\n * @platform android\n * @platform ios\n */\n contentType?: ContentType;\n};\n\n/**\n * Contains information about any errors that the player encountered during the playback\n */\nexport type PlayerError = {\n message: string;\n};\n\n/**\n * Contains information that will be displayed in the now playing notification when the video is playing.\n * @platform android\n * @platform ios\n */\nexport type VideoMetadata = {\n /**\n * The title of the video.\n * @platform android\n * @platform ios\n */\n title?: string;\n /**\n * Secondary text that will be displayed under the title.\n * @platform android\n * @platform ios\n */\n artist?: string;\n /**\n * The uri of the video artwork.\n * @platform android\n * @platform ios\n */\n artwork?: string;\n};\n\n/**\n * Specifies which type of DRM to use:\n * - Android supports ClearKey, PlayReady and Widevine.\n * - iOS supports FairPlay.\n */\nexport type DRMType = 'clearkey' | 'fairplay' | 'playready' | 'widevine';\n\n/**\n * Specifies DRM options which will be used by the player while loading the video.\n */\nexport type DRMOptions = {\n /**\n * Determines which type of DRM to use.\n */\n type: DRMType;\n\n /**\n * Determines the license server URL.\n */\n licenseServer: string;\n\n /**\n * Determines headers sent to the license server on license requests.\n */\n headers?: Record;\n\n /**\n * Specifies whether the DRM is a multi-key DRM.\n * @platform android\n */\n multiKey?: boolean;\n\n /**\n * Specifies the content ID of the stream.\n * @platform ios\n */\n contentId?: string;\n\n /**\n * Specifies the certificate URL for the FairPlay DRM.\n * @platform ios\n */\n certificateUrl?: string;\n\n /**\n * Specifies the base64 encoded certificate data for the FairPlay DRM.\n * When this property is set, the `certificateUrl` property is ignored.\n * @platform ios\n */\n base64CertificateData?: string;\n};\n\n/**\n * Specifies buffer options which will be used by the player when buffering the video.\n *\n * @platform android\n * @platform ios\n */\nexport type BufferOptions = {\n /**\n * The duration in seconds which determines how much media the player should buffer ahead of the current playback time.\n *\n * On iOS when set to `0` the player will automatically decide appropriate buffer duration.\n *\n * Equivalent to [`AVPlayerItem.preferredForwardBufferDuration`](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration).\n * @default Android: 20, iOS: 0\n * @platform android\n * @platform ios\n */\n readonly preferredForwardBufferDuration?: number;\n\n /**\n * A Boolean value that indicates whether the player should automatically delay playback in order to minimize stalling.\n *\n * Equivalent to [`AVPlayer.automaticallyWaitsToMinimizeStalling`](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal).\n * @default true\n * @platform ios\n */\n readonly waitsToMinimizeStalling?: boolean;\n\n /**\n * Minimum duration of the buffer in seconds required to continue playing after the player has been paused or started buffering.\n *\n * > This property will be ignored if `preferredForwardBufferDuration` is lower.\n * @default 2\n * @platform android\n */\n readonly minBufferForPlayback?: number;\n\n /**\n * The maximum number of bytes that the player can buffer from the network.\n * When 0 the player will automatically decide appropriate buffer size.\n *\n * @default 0\n * @platform android\n */\n readonly maxBufferBytes?: number | null;\n\n /**\n * A Boolean value which determines whether the player should prioritize time over size when buffering media.\n *\n * @default false\n * @platform android\n */\n readonly prioritizeTimeOverSizeThreshold?: boolean;\n};\n\n/**\n * Specifies the content type of the source.\n *\n * - `auto`: The player will automatically determine the content type of the video.\n * - `progressive`: The player will use progressive download content type. This is the default `ContentType` when the uri does not contain an extension.\n * - `hls`: The player will use HLS content type.\n * - `dash`: The player will use DASH content type (Android-only).\n * - `smoothStreaming`: The player will use SmoothStreaming content type (Android-only).\n *\n * @default `auto`\n */\nexport type ContentType = 'auto' | 'progressive' | 'hls' | 'dash' | 'smoothStreaming';\n\n/**\n * Specifies the audio mode that the player should use. Audio mode is set on per-app basis, if there are multiple players playing and\n * have different a `AudioMode` specified, the highest priority mode will be used. Priority order: 'doNotMix' > 'auto' > 'duckOthers' > 'mixWithOthers'.\n *\n * - `mixWithOthers`: The player will mix its audio output with other apps.\n * - `duckOthers`: The player will lower the volume of other apps if any of the active players is outputting audio.\n * - `auto`: The player will allow other apps to keep playing audio only when it is muted. On iOS it will always interrupt other apps when `showNowPlayingNotification` is `true` due to system requirements.\n * - `doNotMix`: The player will pause playback in other apps, even when it's muted.\n *\n * > On iOS, the Now Playing notification is dependent on the audio mode. If the audio mode is different from `doNotMix` or `auto` this feature will not work.\n */\nexport type AudioMixingMode = 'mixWithOthers' | 'duckOthers' | 'auto' | 'doNotMix';\n\nexport type SubtitleTrack = {\n /**\n * A string used by `expo-video` to identify the subtitle track.\n *\n * @platform android\n */\n id?: string;\n\n /**\n * Language of the subtitle track. For example, `en`, `pl`, `de`.\n */\n language: string;\n\n /**\n * Label of the subtitle track in the language of the device.\n */\n label: string;\n\n /**\n * Name of the subtitle track as specified in the media source.\n * @platform android\n * @platform ios\n */\n name?: string;\n\n /**\n * Indicates whether this is the default subtitle track.\n * @platform android\n * @platform ios\n */\n isDefault?: boolean;\n\n /**\n * Indicates whether this track should be auto-selected based on user preferences.\n * @platform android\n * @platform ios\n */\n autoSelect?: boolean;\n};\n\n/**\n * Specifies a VideoTrack loaded from a [`VideoSource`](#videosource).\n */\nexport type VideoTrack = {\n /**\n * The id of the video track.\n *\n * > This field is platform-specific and may return different depending on the operating system.\n */\n id: string;\n\n /**\n * The URL of the `VideoTrack` for HLS video sources. `null` for other source types.\n */\n url: string | null;\n\n /**\n * Size of the video track.\n */\n size: VideoSize;\n\n /**\n * MimeType of the video track or null if unknown.\n */\n mimeType: string | null;\n\n /**\n * Indicates whether the video track format is supported by the device.\n *\n * @platform android\n */\n isSupported: boolean;\n\n /**\n * Specifies the bitrate in bits per second. This is the peak bitrate if known, or else the average bitrate if known, or else null.\n *\n * @deprecated Use `peakBitrate` or `averageBitrate` instead.\n */\n bitrate: number | null;\n\n /**\n * Specifies the average bitrate in bits per second or null if the value is unknown.\n *\n */\n averageBitrate: number | null;\n\n /**\n * Specifies the average bitrate in bits per second or null if the value is unknown.\n */\n peakBitrate: number | null;\n\n /**\n * Specifies the frame rate of the video track in frames per second.\n */\n frameRate: number | null;\n};\n\n/**\n * Specifies the size of a video track.\n */\nexport type VideoSize = {\n /**\n * Width of the video track in pixels.\n */\n width: number;\n /**\n * Height of the video track in pixels.\n */\n height: number;\n};\n\nexport type AudioTrack = {\n /**\n * A string used by expo-video to identify the audio track.\n * @platform android\n */\n id?: string;\n\n /**\n * Language of the audio track. For example, 'en', 'pl', 'de'.\n */\n language: string;\n\n /**\n * Label of the audio track in the language of the device.\n */\n label: string;\n\n /**\n * Name of the audio track as specified in the media source.\n * @platform android\n * @platform ios\n */\n name?: string;\n\n /**\n * Indicates whether this is the default audio track.\n * @platform android\n * @platform ios\n */\n isDefault?: boolean;\n\n /**\n * Indicates whether this track should be auto-selected based on user preferences.\n * @platform android\n * @platform ios\n */\n autoSelect?: boolean;\n};\n\n/**\n * Determines the time that the actual position seeked to may precede or exceed the requested seek position.\n * Larger tolerance will usually result in faster seeking.\n * This property affects the precision of setting the [`currentTime`](#currenttime) property and the [`seekBy`](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n *\n * > If you are trying to optimize for scrubbing (many frequent seeks), also see [`ScrubbingModeOptions`](#scrubbingmodeoptions-1).\n *\n * @platform android\n * @platform ios\n */\nexport type SeekTolerance = {\n /**\n * The maximum time that the actual position seeked to may precede the requested seek position, in seconds. Must be non-negative.\n * @default 0\n */\n toleranceBefore?: number;\n\n /**\n * The maximum time that the actual position seeked to may exceed the requested seek position, in seconds. Must be non-negative.\n * @default 0\n */\n toleranceAfter?: number;\n};\n\n/**\n * Defines scrubbing mode options used by a [`VideoPlayer`](#videoplayer).\n */\nexport type ScrubbingModeOptions = {\n /**\n * Whether the codec operating rate should be increased in scrubbing mode.\n *\n * You should only enable this when the player is receiving a large number of seeks in a short period of time. For less frequent seeks, fine-tuning the [`SeekTolerance`](#seektolerance-1) may be sufficient.\n *\n * On Android, the player may consume more resources in this mode, so it should only be used for short periods of time in response to user interaction (for example, dragging on a progress bar UI element).\n *\n * On Android, when `scrubbingModeEnabled` is `true`, the playback is suppressed. You should set this property back to `false` when the user interaction ends to allow the playback to resume.\n * For best results, on iOS you should pause the playback when scrubbing.\n *\n * > For best scrubbing performance, consider also increasing the seeking tolerance using the [`SeekTolerance`](#seektolerance-1) property.\n *\n * > Other scrubbing mode options will have no effect when this is `false`.\n * @default false\n * @platform android\n * @platform ios\n */\n scrubbingModeEnabled?: boolean;\n\n /**\n * Whether the codec operating rate should be increased in scrubbing mode.\n *\n * @platform android\n * @default true\n */\n increaseCodecOperatingRate?: boolean;\n\n /**\n * Sets whether ExoPlayer's dynamic scheduling should be enabled in scrubbing mode.\n * This can result in available output buffers being handled more quickly when seeking.\n *\n * @platform android\n * @default true\n */\n enableDynamicScheduling?: boolean;\n /**\n * Sets whether to use `MediaCodec.BUFFER_FLAG_DECODE_ONLY` in scrubbing mode.\n * When playback is using MediaCodec on API 34+, this flag can speed up seeking by signalling that the decoded output of buffers between the previous keyframe and the target frame is not needed by the player.\n *\n * @platform android\n * @default true\n */\n useDecodeOnlyFlag?: boolean;\n\n /**\n * Sets whether to avoid flushing the decoder (where possible) in scrubbing mode.\n * When `true`, avoids flushing the decoder when a new seek starts decoding from a key-frame in compatible content.\n *\n * @platform android\n * @default true\n */\n allowSkippingMediaCodecFlush?: boolean;\n};\n\n/**\n * Options to apply to the player builder before the native constructor is invoked\n * @platform android\n */\nexport type PlayerBuilderOptions = {\n /**\n * Seek backward increment in seconds.\n * Values will be clamped between 0.001 and 999 seconds.\n * @platform android\n */\n seekBackwardIncrement?: number;\n\n /**\n * Seek forward increment in seconds.\n * Values will be clamped between 0.001 and 999 seconds.\n * @platform android\n */\n seekForwardIncrement?: number;\n};\n"]} \ No newline at end of file diff --git a/packages/expo-video/ios/Records/Tracks.swift b/packages/expo-video/ios/Records/Tracks.swift index 13c067f390a407..8488fc277feb76 100644 --- a/packages/expo-video/ios/Records/Tracks.swift +++ b/packages/expo-video/ios/Records/Tracks.swift @@ -11,25 +11,55 @@ internal struct SubtitleTrack: Record { @Field var label: String? = nil - static func from(mediaSelectionOption option: AVMediaSelectionOption) -> SubtitleTrack? { + @Field + var name: String? = nil + + @Field + var isDefault: Bool = false + + @Field + var autoSelect: Bool = false + + static func from(mediaSelectionOption option: AVMediaSelectionOption, in group: AVMediaSelectionGroup? = nil) -> SubtitleTrack? { guard let identifier = option.locale?.identifier else { return nil } - return SubtitleTrack(language: identifier, label: option.displayName) + let isDefault = group?.defaultOption == option + let autoSelect = option.hasMediaCharacteristic(.isAuxiliaryContent) == false + + return SubtitleTrack( + language: identifier, + label: option.displayName, + name: option.commonMetadata.first(where: { $0.commonKey == .commonKeyTitle })?.stringValue, + isDefault: isDefault, + autoSelect: autoSelect + ) } } internal struct AudioTrack: Record { @Field var language: String? = nil @Field var label: String? = nil + @Field var name: String? = nil + @Field var isDefault: Bool = false + @Field var autoSelect: Bool = false - static func from(mediaSelectionOption option: AVMediaSelectionOption) -> AudioTrack? { + static func from(mediaSelectionOption option: AVMediaSelectionOption, in group: AVMediaSelectionGroup? = nil) -> AudioTrack? { guard let identifier = option.locale?.identifier else { return nil } - return AudioTrack(language: identifier, label: option.displayName) + let isDefault = group?.defaultOption == option + let autoSelect = option.hasMediaCharacteristic(.isAuxiliaryContent) == false + + return AudioTrack( + language: identifier, + label: option.displayName, + name: option.commonMetadata.first(where: { $0.commonKey == .commonKeyTitle })?.stringValue, + isDefault: isDefault, + autoSelect: autoSelect + ) } } diff --git a/packages/expo-video/ios/VideoPlayerAudioTracks.swift b/packages/expo-video/ios/VideoPlayerAudioTracks.swift index 0166c8d252fdb4..461e3cb3336a00 100644 --- a/packages/expo-video/ios/VideoPlayerAudioTracks.swift +++ b/packages/expo-video/ios/VideoPlayerAudioTracks.swift @@ -64,7 +64,7 @@ class VideoPlayerAudioTracks { if let group = try await asset.loadMediaSelectionGroup(for: characteristic) { for option in group.options { - guard let audioTrack = AudioTrack.from(mediaSelectionOption: option) else { + guard let audioTrack = AudioTrack.from(mediaSelectionOption: option, in: group) else { continue } @@ -84,6 +84,6 @@ class VideoPlayerAudioTracks { return nil } - return AudioTrack.from(mediaSelectionOption: selectedMediaOption) + return AudioTrack.from(mediaSelectionOption: selectedMediaOption, in: mediaSelectionGroup) } } diff --git a/packages/expo-video/ios/VideoPlayerSubtitles.swift b/packages/expo-video/ios/VideoPlayerSubtitles.swift index 59c84fbbb39d01..ab8464c918cf43 100644 --- a/packages/expo-video/ios/VideoPlayerSubtitles.swift +++ b/packages/expo-video/ios/VideoPlayerSubtitles.swift @@ -63,7 +63,7 @@ class VideoPlayerSubtitles { if let group = try await asset.loadMediaSelectionGroup(for: characteristic) { for option in group.options { - guard let subtitleTrack = SubtitleTrack.from(mediaSelectionOption: option) else { + guard let subtitleTrack = SubtitleTrack.from(mediaSelectionOption: option, in: group) else { continue } @@ -83,6 +83,6 @@ class VideoPlayerSubtitles { return nil } - return SubtitleTrack.from(mediaSelectionOption: selectedMediaOption) + return SubtitleTrack.from(mediaSelectionOption: selectedMediaOption, in: mediaSelectionGroup) } } diff --git a/packages/expo-video/src/VideoPlayer.types.ts b/packages/expo-video/src/VideoPlayer.types.ts index d0240354f3be5f..385de4467d2b04 100644 --- a/packages/expo-video/src/VideoPlayer.types.ts +++ b/packages/expo-video/src/VideoPlayer.types.ts @@ -585,6 +585,27 @@ export type SubtitleTrack = { * Label of the subtitle track in the language of the device. */ label: string; + + /** + * Name of the subtitle track as specified in the media source. + * @platform android + * @platform ios + */ + name?: string; + + /** + * Indicates whether this is the default subtitle track. + * @platform android + * @platform ios + */ + isDefault?: boolean; + + /** + * Indicates whether this track should be auto-selected based on user preferences. + * @platform android + * @platform ios + */ + autoSelect?: boolean; }; /** @@ -674,6 +695,27 @@ export type AudioTrack = { * Label of the audio track in the language of the device. */ label: string; + + /** + * Name of the audio track as specified in the media source. + * @platform android + * @platform ios + */ + name?: string; + + /** + * Indicates whether this is the default audio track. + * @platform android + * @platform ios + */ + isDefault?: boolean; + + /** + * Indicates whether this track should be auto-selected based on user preferences. + * @platform android + * @platform ios + */ + autoSelect?: boolean; }; /** From c558433ed913955d2e163809a89560293cbe6e7e Mon Sep 17 00:00:00 2001 From: Alan Hughes <30924086+alanjhughes@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:15:25 +0000 Subject: [PATCH 03/17] [android][image] Add `CookieJar` to image request client (#43257) --- packages/expo-image/CHANGELOG.md | 2 ++ .../ExpoImageOkHttpClientGlideModule.kt | 29 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/expo-image/CHANGELOG.md b/packages/expo-image/CHANGELOG.md index e7a7d3a788f953..2bb85a4fae829e 100644 --- a/packages/expo-image/CHANGELOG.md +++ b/packages/expo-image/CHANGELOG.md @@ -8,6 +8,8 @@ ### 🐛 Bug fixes +- [Android] Uses shared cookie jar for image requests. ([#43257](https://github.com/expo/expo/pull/43257) by [@alanjhughes](https://github.com/alanjhughes)) + ### 💡 Others ## 55.0.3 — 2026-01-27 diff --git a/packages/expo-image/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt b/packages/expo-image/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt index 071907c97adcd8..74c6a3da9f7de2 100644 --- a/packages/expo-image/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt +++ b/packages/expo-image/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt @@ -1,6 +1,7 @@ package expo.modules.image.okhttp import android.content.Context +import android.webkit.CookieManager import com.bumptech.glide.Glide import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule @@ -9,6 +10,9 @@ import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.Headers import com.bumptech.glide.module.LibraryGlideModule import expo.modules.image.events.OkHttpProgressListener +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl import okhttp3.OkHttpClient import java.io.InputStream @@ -68,10 +72,33 @@ data class GlideUrlWrapper(val glideUrl: GlideUrl) { } } +private object SharedCookieJar : CookieJar { + override fun saveFromResponse(url: HttpUrl, cookies: List) { + val cookieManager = getCookieManager() ?: return + val urlString = url.toString() + for (cookie in cookies) { + cookieManager.setCookie(urlString, cookie.toString()) + } + cookieManager.flush() + } + + override fun loadForRequest(url: HttpUrl): List { + val cookieManager = getCookieManager() ?: return emptyList() + val cookieString = cookieManager.getCookie(url.toString()) ?: return emptyList() + return cookieString.split(";").mapNotNull { Cookie.parse(url, it.trim()) } + } + + private fun getCookieManager(): CookieManager? = runCatching { + CookieManager.getInstance() + }.getOrNull() +} + @GlideModule class ExpoImageOkHttpClientGlideModule : LibraryGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { - val client = OkHttpClient() + val client = OkHttpClient.Builder() + .cookieJar(SharedCookieJar) + .build() // We don't use the `GlideUrl` directly but we want to replace the default okhttp loader anyway // to make sure that the app will use only one client. registry.replace(GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(client)) From 4f6b84124e496f64d331dc848d54b3785342f33b Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 19 Feb 2026 11:42:04 +0000 Subject: [PATCH 04/17] fix(fingerprint): Add independent dependency for `@expo/env` (#42764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Why The environment is currently being instantiated by resolving `@expo/env` from `expo`. Failing to load this results in an error. However, the actual dependency chain is `expo -> @expo/cli -> @expo/env` (and some others), which means that this has a chance of failing, which would in turn fail `ExpoConfigLoader`. We have three options here: - expose the exact config loading procedure that `@expo/cli` uses via `expo/internal/*` (requires a large refactor that requires us to unify the `@expo/env` loading and `@expo/config` loading — we should do this but this will be likely a breaking change) - fix the resolution chain by turning the double-resolution into a triple-resolution - add `@expo/env` as a dependency to `@expo/fingerprint` and rely on it either being the same or not mattering This PR goes for the third option for simplicity for now. This isn't quite the same and may impact the module loader cache, so the `@expo/env` loading has been moved before the module loader cache capturing. **This makes this a change that'll affect the project fingerprint.** # How - Add `@expo/env` as a dependency to `@expo/fingerprint` and load it directly - Make sure we load the env files **after** setting `NODE_ENV=development` - This is the default, however, being explicit would be better, since it can technically be overridden otherwise. This would lead to us loading the config with `NODE_ENV=development` while sourcing the env files from another environment - **Unrelated:** Add type declaration for `module._cache` # Test Plan - Existing tests # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- packages/@expo/fingerprint/CHANGELOG.md | 2 ++ .../@expo/fingerprint/build/ExpoConfigLoader.js | 10 ++-------- .../fingerprint/build/ExpoConfigLoader.js.map | 2 +- .../@expo/fingerprint/build/ExpoResolver.d.ts | 4 ---- packages/@expo/fingerprint/build/ExpoResolver.js | 12 ------------ .../@expo/fingerprint/build/ExpoResolver.js.map | 2 +- packages/@expo/fingerprint/package.json | 1 + .../@expo/fingerprint/src/ExpoConfigLoader.ts | 12 ++++-------- packages/@expo/fingerprint/src/ExpoResolver.ts | 15 --------------- packages/@expo/fingerprint/src/types/module.d.ts | 7 +++++++ 10 files changed, 18 insertions(+), 49 deletions(-) create mode 100644 packages/@expo/fingerprint/src/types/module.d.ts diff --git a/packages/@expo/fingerprint/CHANGELOG.md b/packages/@expo/fingerprint/CHANGELOG.md index 51feebc04a1d9b..f3c7b66725b418 100644 --- a/packages/@expo/fingerprint/CHANGELOG.md +++ b/packages/@expo/fingerprint/CHANGELOG.md @@ -8,6 +8,8 @@ ### 🐛 Bug fixes +- Fix resolution to `expo -> @expo/cli -> @expo/env` being unstable ([#42764](https://github.com/expo/expo/pull/42764) by [@kitten](https://github.com/kitten)) + ### 💡 Others ## 0.16.3 — 2026-02-03 diff --git a/packages/@expo/fingerprint/build/ExpoConfigLoader.js b/packages/@expo/fingerprint/build/ExpoConfigLoader.js index 247200aad38501..35a71dcdfba9bc 100644 --- a/packages/@expo/fingerprint/build/ExpoConfigLoader.js +++ b/packages/@expo/fingerprint/build/ExpoConfigLoader.js @@ -9,11 +9,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.getExpoConfigLoaderPath = getExpoConfigLoaderPath; const promises_1 = __importDefault(require("fs/promises")); const module_1 = __importDefault(require("module")); -const node_assert_1 = __importDefault(require("node:assert")); const node_process_1 = __importDefault(require("node:process")); const path_1 = __importDefault(require("path")); const resolve_from_1 = __importDefault(require("resolve-from")); -const ExpoResolver_1 = require("./ExpoResolver"); const Options_1 = require("./Options"); const Path_1 = require("./utils/Path"); async function runAsync(programName, args = []) { @@ -23,15 +21,11 @@ async function runAsync(programName, args = []) { } const projectRoot = path_1.default.resolve(args[0]); const ignoredFile = args[1] ? path_1.default.resolve(args[1]) : null; - // @ts-expect-error: module internal _cache - const loadedModulesBefore = new Set(Object.keys(module_1.default._cache)); - const expoEnvPath = (0, ExpoResolver_1.resolveExpoEnvPath)(projectRoot); - (0, node_assert_1.default)(expoEnvPath, `Could not find '@expo/env' package for the project from ${projectRoot}.`); - require(expoEnvPath).load(projectRoot); setNodeEnv('development'); + require('@expo/env').load(projectRoot); + const loadedModulesBefore = new Set(Object.keys(module_1.default._cache)); const { getConfig } = require((0, resolve_from_1.default)(path_1.default.resolve(projectRoot), 'expo/config')); const config = await getConfig(projectRoot, { skipSDKVersionRequirement: true }); - // @ts-expect-error: module internal _cache const loadedModules = Object.keys(module_1.default._cache) .filter((modulePath) => !loadedModulesBefore.has(modulePath)) .map((modulePath) => path_1.default.relative(projectRoot, modulePath)); diff --git a/packages/@expo/fingerprint/build/ExpoConfigLoader.js.map b/packages/@expo/fingerprint/build/ExpoConfigLoader.js.map index d2133276b4774a..5e8d77f7e894fb 100644 --- a/packages/@expo/fingerprint/build/ExpoConfigLoader.js.map +++ b/packages/@expo/fingerprint/build/ExpoConfigLoader.js.map @@ -1 +1 @@ -{"version":3,"file":"ExpoConfigLoader.js","sourceRoot":"","sources":["../src/ExpoConfigLoader.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ExpoConfigLoader.js","sourceRoot":"","sources":["../src/ExpoConfigLoader.ts"],"names":[],"mappings":";AAAA;;GAEG;;;;;AAsFH,0DAEC;AAtFD,2DAA6B;AAC7B,oDAA4B;AAC5B,gEAAmC;AACnC,gDAAwB;AACxB,gEAAuC;AAEvC,uCAAiD;AACjD,uCAA6C;AAE7C,KAAK,UAAU,QAAQ,CAAC,WAAmB,EAAE,OAAiB,EAAE;IAC9D,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,OAAO,CAAC,GAAG,CAAC,UAAU,WAAW,8BAA8B,CAAC,CAAC;QACjE,OAAO;IACT,CAAC;IAED,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAE3D,UAAU,CAAC,aAAa,CAAC,CAAC;IAC1B,OAAO,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAEvC,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAEhE,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,IAAA,sBAAW,EAAC,cAAI,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC;IACrF,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,WAAW,EAAE,EAAE,yBAAyB,EAAE,IAAI,EAAE,CAAC,CAAC;IAEjF,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAM,CAAC,MAAM,CAAC;SAC7C,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,mBAAmB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;SAC5D,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,cAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC;IAE/D,MAAM,YAAY,GAAG;QACnB,GAAG,mCAAmC;QACtC,GAAG,CAAC,MAAM,qBAAqB,CAAC,WAAW,CAAC,CAAC;KAC9C,CAAC;IACF,MAAM,qBAAqB,GAAG,aAAa,CAAC,MAAM,CAChD,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,IAAA,oBAAa,EAAC,UAAU,EAAE,YAAY,CAAC,CACzD,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,qBAAqB,EAAE,CAAC,CAAC;IAChF,IAAI,sBAAO,CAAC,IAAI,EAAE,CAAC;QACjB,sBAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED,mCAAmC;AACnC,IAAI,OAAO,CAAC,IAAI,EAAE,QAAQ,KAAK,UAAU,EAAE,CAAC;IAC1C,CAAC,KAAK,IAAI,EAAE;QACV,MAAM,YAAY,GAAG,sBAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC,CAAC;QACzE,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,sBAAO,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,sBAAO,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC;QACnF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC;YACnC,sBAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;AACP,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,qBAAqB,CAAC,WAA0B;IAC7D,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,8BAAoB,CAAC;IAC9B,CAAC;IAED,MAAM,WAAW,GAAG,EAAE,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,iBAAiB,GAAG,MAAM,kBAAE,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QACjE,MAAM,sBAAsB,GAAG,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7D,KAAK,MAAM,IAAI,IAAI,sBAAsB,EAAE,CAAC;YAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAChC,IAAI,WAAW,EAAE,CAAC;gBAChB,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAChC,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IAEV,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;GAEG;AACH,SAAgB,uBAAuB;IACrC,OAAO,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,qBAAqB,CAAC,CAAC;AACrD,CAAC;AAED;;;GAGG;AACH,SAAS,UAAU,CAAC,IAAkC;IACpD,sBAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,sBAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,IAAI,CAAC;IACpD,sBAAO,CAAC,GAAG,CAAC,SAAS,GAAG,sBAAO,CAAC,GAAG,CAAC,SAAS,IAAI,sBAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;IAEtE,+FAA+F;IAC/F,UAAU,CAAC,OAAO,GAAG,sBAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC;AAC7D,CAAC;AAED,6DAA6D;AAC7D,MAAM,mCAAmC,GAAG;IAC1C,2FAA2F;IAC3F,cAAc;IAEd,6BAA6B;IAC7B,4BAA4B;IAC5B,kCAAkC;IAClC,gCAAgC;IAChC,wCAAwC;IACxC,oBAAoB;QAClB,KAAK;QACL,aAAa;QACb,cAAc;QACd,aAAa;QACb,OAAO;QACP,OAAO;QACP,QAAQ;QACR,eAAe;QACf,sBAAsB;QACtB,QAAQ;QACR,aAAa;QACb,iBAAiB;QACjB,UAAU;QACV,UAAU;QACV,aAAa;QACb,WAAW;QACX,OAAO;QACP,sBAAsB;QACtB,IAAI;QACJ,YAAY;QACZ,mBAAmB;QACnB,qBAAqB;QACrB,cAAc;QACd,cAAc;QACd,aAAa;QACb,SAAS;QACT,gBAAgB;QAChB,sBAAsB;QACtB,mBAAmB;KACpB,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ;CACpB,CAAC"} \ No newline at end of file diff --git a/packages/@expo/fingerprint/build/ExpoResolver.d.ts b/packages/@expo/fingerprint/build/ExpoResolver.d.ts index 6a353bfb560191..812d67e8113d24 100644 --- a/packages/@expo/fingerprint/build/ExpoResolver.d.ts +++ b/packages/@expo/fingerprint/build/ExpoResolver.d.ts @@ -2,10 +2,6 @@ * Resolve the version of `expo` package in the project. */ export declare function resolveExpoVersion(projectRoot: string): string | null; -/** - * Resolve the path to the `@expo/env` package in the project. - */ -export declare function resolveExpoEnvPath(projectRoot: string): string | null; /** * Resolve the package root of `expo-modules-autolinking` package in the project. */ diff --git a/packages/@expo/fingerprint/build/ExpoResolver.js b/packages/@expo/fingerprint/build/ExpoResolver.js index 99f809af700ec9..12eebac5ef50cb 100644 --- a/packages/@expo/fingerprint/build/ExpoResolver.js +++ b/packages/@expo/fingerprint/build/ExpoResolver.js @@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.resolveExpoVersion = resolveExpoVersion; -exports.resolveExpoEnvPath = resolveExpoEnvPath; exports.resolveExpoAutolinkingPackageRoot = resolveExpoAutolinkingPackageRoot; exports.resolveExpoAutolinkingCliPath = resolveExpoAutolinkingCliPath; exports.resolveExpoAutolinkingVersion = resolveExpoAutolinkingVersion; @@ -25,17 +24,6 @@ function resolveExpoVersion(projectRoot) { } return null; } -/** - * Resolve the path to the `@expo/env` package in the project. - */ -function resolveExpoEnvPath(projectRoot) { - const expoPackageRoot = resolve_from_1.default.silent(projectRoot, 'expo/package.json'); - const expoEnvPackageJsonPath = resolve_from_1.default.silent(expoPackageRoot ?? projectRoot, '@expo/env/package.json'); - if (expoEnvPackageJsonPath) { - return path_1.default.dirname(expoEnvPackageJsonPath); - } - return null; -} /** * Resolve the package root of `expo-modules-autolinking` package in the project. */ diff --git a/packages/@expo/fingerprint/build/ExpoResolver.js.map b/packages/@expo/fingerprint/build/ExpoResolver.js.map index fb2cc2eb66b525..3c6db050ecdabd 100644 --- a/packages/@expo/fingerprint/build/ExpoResolver.js.map +++ b/packages/@expo/fingerprint/build/ExpoResolver.js.map @@ -1 +1 @@ -{"version":3,"file":"ExpoResolver.js","sourceRoot":"","sources":["../src/ExpoResolver.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ExpoResolver.js","sourceRoot":"","sources":["../src/ExpoResolver.ts"],"names":[],"mappings":";;;;;AASA,gDAOC;AAKD,8EAkBC;AAMD,sEAMC;AAKD,sEAOC;AAKD,kFAEC;AAMD,gDAMC;AAlFD,gDAAwB;AACxB,gEAAuC;AACvC,oDAA4B;AAE5B,IAAI,gCAAgC,GAA4B,IAAI,CAAC;AAErE;;GAEG;AACH,SAAgB,kBAAkB,CAAC,WAAmB;IACpD,MAAM,mBAAmB,GAAG,sBAAW,CAAC,MAAM,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAC;IACjF,IAAI,mBAAmB,EAAE,CAAC;QACxB,MAAM,eAAe,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACrD,OAAO,eAAe,CAAC,OAAO,CAAC;IACjC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,SAAgB,iCAAiC,CAAC,WAAmB;IACnE,IAAI,gCAAgC,EAAE,CAAC;QACrC,MAAM,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,GAAG,gCAAgC,CAAC;QAChF,IAAI,iBAAiB,KAAK,WAAW,EAAE,CAAC;YACtC,OAAO,iBAAiB,CAAC;QAC3B,CAAC;IACH,CAAC;IACD,MAAM,eAAe,GAAG,sBAAW,CAAC,MAAM,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAC;IAC7E,MAAM,0BAA0B,GAAG,sBAAW,CAAC,MAAM,CACnD,eAAe,IAAI,WAAW,EAC9B,uCAAuC,CACxC,CAAC;IACF,IAAI,0BAA0B,EAAE,CAAC;QAC/B,MAAM,sBAAsB,GAAG,cAAI,CAAC,OAAO,CAAC,0BAA0B,CAAC,CAAC;QACxE,gCAAgC,GAAG,CAAC,WAAW,EAAE,sBAAsB,CAAC,CAAC;QACzE,OAAO,sBAAsB,CAAC;IAChC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,SAAgB,6BAA6B,CAAC,WAAmB;IAC/D,MAAM,sBAAsB,GAAG,iCAAiC,CAAC,WAAW,CAAC,CAAC;IAC9E,IAAI,sBAAsB,IAAI,IAAI,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,cAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,KAAK,EAAE,6BAA6B,CAAC,CAAC;AACjF,CAAC;AAED;;GAEG;AACH,SAAgB,6BAA6B,CAAC,WAAmB;IAC/D,MAAM,sBAAsB,GAAG,iCAAiC,CAAC,WAAW,CAAC,CAAC;IAC9E,IAAI,sBAAsB,EAAE,CAAC;QAC3B,MAAM,sBAAsB,GAAG,OAAO,CAAC,cAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,CAAC,CAAC;QAC1F,OAAO,sBAAsB,CAAC,OAAO,CAAC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,SAAgB,mCAAmC,CAAC,WAAmB;IACrE,OAAO,sBAAW,CAAC,MAAM,CAAC,WAAW,EAAE,qBAAqB,CAAC,IAAI,IAAI,CAAC;AACxE,CAAC;AAED;;;GAGG;AACH,SAAgB,kBAAkB,CAAC,WAAmB,EAAE,KAAa;IACnE,MAAM,WAAW,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;IACpD,IAAI,WAAW,EAAE,CAAC;QAChB,OAAO,gBAAM,CAAC,SAAS,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"} \ No newline at end of file diff --git a/packages/@expo/fingerprint/package.json b/packages/@expo/fingerprint/package.json index 239b931cfe9912..2c36dab3d87cae 100644 --- a/packages/@expo/fingerprint/package.json +++ b/packages/@expo/fingerprint/package.json @@ -43,6 +43,7 @@ "license": "MIT", "homepage": "https://github.com/expo/expo/tree/main/packages/@expo/fingerprint#readme", "dependencies": { + "@expo/env": "^2.0.11", "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", diff --git a/packages/@expo/fingerprint/src/ExpoConfigLoader.ts b/packages/@expo/fingerprint/src/ExpoConfigLoader.ts index ca00f244201cc0..cf92105d0ac387 100644 --- a/packages/@expo/fingerprint/src/ExpoConfigLoader.ts +++ b/packages/@expo/fingerprint/src/ExpoConfigLoader.ts @@ -4,12 +4,10 @@ import fs from 'fs/promises'; import module from 'module'; -import assert from 'node:assert'; import process from 'node:process'; import path from 'path'; import resolveFrom from 'resolve-from'; -import { resolveExpoEnvPath } from './ExpoResolver'; import { DEFAULT_IGNORE_PATHS } from './Options'; import { isIgnoredPath } from './utils/Path'; @@ -22,16 +20,14 @@ async function runAsync(programName: string, args: string[] = []) { const projectRoot = path.resolve(args[0]); const ignoredFile = args[1] ? path.resolve(args[1]) : null; - // @ts-expect-error: module internal _cache + setNodeEnv('development'); + require('@expo/env').load(projectRoot); + const loadedModulesBefore = new Set(Object.keys(module._cache)); - const expoEnvPath = resolveExpoEnvPath(projectRoot); - assert(expoEnvPath, `Could not find '@expo/env' package for the project from ${projectRoot}.`); - require(expoEnvPath).load(projectRoot); - setNodeEnv('development'); const { getConfig } = require(resolveFrom(path.resolve(projectRoot), 'expo/config')); const config = await getConfig(projectRoot, { skipSDKVersionRequirement: true }); - // @ts-expect-error: module internal _cache + const loadedModules = Object.keys(module._cache) .filter((modulePath) => !loadedModulesBefore.has(modulePath)) .map((modulePath) => path.relative(projectRoot, modulePath)); diff --git a/packages/@expo/fingerprint/src/ExpoResolver.ts b/packages/@expo/fingerprint/src/ExpoResolver.ts index 2954c0c74484b9..7426a67dccdfb9 100644 --- a/packages/@expo/fingerprint/src/ExpoResolver.ts +++ b/packages/@expo/fingerprint/src/ExpoResolver.ts @@ -16,21 +16,6 @@ export function resolveExpoVersion(projectRoot: string): string | null { return null; } -/** - * Resolve the path to the `@expo/env` package in the project. - */ -export function resolveExpoEnvPath(projectRoot: string): string | null { - const expoPackageRoot = resolveFrom.silent(projectRoot, 'expo/package.json'); - const expoEnvPackageJsonPath = resolveFrom.silent( - expoPackageRoot ?? projectRoot, - '@expo/env/package.json' - ); - if (expoEnvPackageJsonPath) { - return path.dirname(expoEnvPackageJsonPath); - } - return null; -} - /** * Resolve the package root of `expo-modules-autolinking` package in the project. */ diff --git a/packages/@expo/fingerprint/src/types/module.d.ts b/packages/@expo/fingerprint/src/types/module.d.ts new file mode 100644 index 00000000000000..b78b434c33d1e3 --- /dev/null +++ b/packages/@expo/fingerprint/src/types/module.d.ts @@ -0,0 +1,7 @@ +import * as __module from 'module'; + +declare module 'module' { + namespace Module { + export const _cache: Record; + } +} From ca793d7a79e69c9a3080ba00848755d736cff57d Mon Sep 17 00:00:00 2001 From: Alan Hughes <30924086+alanjhughes@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:54:42 +0000 Subject: [PATCH 05/17] [ios][audio] Support native preloading (#43061) # Why closes #42900 Adds support for native preloading # How Adds a `preload` function and cache that allows users to create a player instance and start loading the buffer ahead of when `useAudioPlayer` is used in the component. When a url is used that exist's in the cache, we return the preloaded `AVPlayer` instance. # Test Plan Bare expo --- .../src/screens/Audio/AudioPreloadScreen.tsx | 234 ++++++++++++++++++ .../src/screens/Audio/AudioScreen.tsx | 15 +- packages/expo-audio/build/Audio.types.d.ts | 33 +++ .../expo-audio/build/Audio.types.d.ts.map | 2 +- packages/expo-audio/build/Audio.types.js.map | 2 +- .../expo-audio/build/AudioModule.types.d.ts | 6 +- .../build/AudioModule.types.d.ts.map | 2 +- .../expo-audio/build/AudioModule.types.js.map | 2 +- packages/expo-audio/build/ExpoAudio.d.ts | 50 +++- packages/expo-audio/build/ExpoAudio.d.ts.map | 2 +- packages/expo-audio/build/ExpoAudio.js | 76 +++++- packages/expo-audio/build/ExpoAudio.js.map | 2 +- .../ios/AudioComponentRegistry.swift | 33 +++ packages/expo-audio/ios/AudioModule.swift | 50 +++- packages/expo-audio/ios/AudioPlayer.swift | 23 +- packages/expo-audio/ios/AudioUtils.swift | 17 +- packages/expo-audio/ios/MediaController.swift | 18 +- packages/expo-audio/src/Audio.types.ts | 34 +++ packages/expo-audio/src/AudioModule.types.ts | 11 +- packages/expo-audio/src/ExpoAudio.ts | 102 +++++++- 20 files changed, 666 insertions(+), 48 deletions(-) create mode 100644 apps/native-component-list/src/screens/Audio/AudioPreloadScreen.tsx diff --git a/apps/native-component-list/src/screens/Audio/AudioPreloadScreen.tsx b/apps/native-component-list/src/screens/Audio/AudioPreloadScreen.tsx new file mode 100644 index 00000000000000..623311d01c0023 --- /dev/null +++ b/apps/native-component-list/src/screens/Audio/AudioPreloadScreen.tsx @@ -0,0 +1,234 @@ +import { + preload, + getPreloadedSources, + clearAllPreloadedSources, + useAudioPlayer, + useAudioPlayerStatus, + AudioPlayer, + AudioSource, + AudioStatus, +} from 'expo-audio'; +import React from 'react'; +import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; + +import HeadingText from '../../components/HeadingText'; +import ListButton from '../../components/ListButton'; +import Colors from '../../constants/Colors'; + +export const sfx1: AudioSource = { + uri: 'https://cdn.freesound.org/previews/370/370182_6430986-hq.mp3', +}; + +export const sfx2: AudioSource = { + uri: 'https://cdn.freesound.org/previews/401/401542_2331641-hq.mp3', +}; + +export default function AudioPreloadScreen(props: any) { + React.useLayoutEffect(() => { + props.navigation.setOptions({ + title: 'Audio Preloading', + }); + }); + + return ( + + Sound Effects + + Both sounds were preloaded in module scope. Tap the buttons to play — they should start + near-instantly. + + + + Preload Cache + + + Preloaded Player + + This player uses a preloaded source. Try replacing to swap between the two preloaded tracks. + + + + ); +} + +function SoundEffectButtons() { + const player1 = useAudioPlayer(sfx1, { keepAudioSessionActive: true }); + const player2 = useAudioPlayer(sfx2, { keepAudioSessionActive: true }); + const status1 = useAudioPlayerStatus(player1); + const status2 = useAudioPlayerStatus(player2); + + const playSfx = (player: AudioPlayer, status: AudioStatus) => { + if (status.playing) { + player.seekTo(0); + } else { + if (status.didJustFinish || status.currentTime > 0) { + player.seekTo(0); + } + player.play(); + } + }; + + return ( + + playSfx(player1, status1)} + /> + playSfx(player2, status2)} + /> + + ); +} + +function SfxButton({ + label, + isLoaded, + onPress, +}: { + label: string; + isLoaded: boolean; + onPress: () => void; +}) { + return ( + [styles.sfxButton, pressed && styles.sfxButtonPressed]} + onPress={onPress}> + {label} + {isLoaded ? 'Ready' : 'Loading...'} + + ); +} + +function PreloadCacheInfo() { + const [sources, setSources] = React.useState([]); + + const refresh = async () => setSources(await getPreloadedSources()); + + return ( + + + Preloaded sources are consumed when a player is created with a matching URL. Use the buttons + below to preload, inspect, and clear the cache. + + { + await preload(sfx1); + await preload(sfx2); + refresh(); + }} + /> + + + {sources.length === 0 ? 'Cache is empty.' : `${sources.length} source(s) in cache:`} + + {sources.map((uri) => ( + + {uri} + + ))} + { + await clearAllPreloadedSources(); + refresh(); + }} + /> + + ); +} + +function PreloadedPlayer() { + const player = useAudioPlayer(sfx1); + const status = useAudioPlayerStatus(player); + const [currentSource, setCurrentSource] = React.useState<1 | 2>(1); + + const handleReplace = () => { + if (currentSource === 1) { + player.replace(sfx2); + setCurrentSource(2); + } else { + player.replace(sfx1); + setCurrentSource(1); + } + }; + + return ( + + Current: Sound {currentSource} + + {status.isLoaded ? `Loaded — ${Math.round(status.duration)}s` : 'Loading...'} + {status.playing ? ' — Playing' : ''} + + + { + if (status.playing) { + player.pause(); + } else { + if (status.didJustFinish) player.seekTo(0); + player.play(); + } + }} + /> + + + + ); +} + +const styles = StyleSheet.create({ + contentContainer: { + padding: 10, + }, + hint: { + marginVertical: 4, + fontSize: 13, + color: '#666', + }, + sfxRow: { + flexDirection: 'row', + gap: 12, + marginVertical: 12, + }, + sfxButton: { + flex: 1, + backgroundColor: Colors.tintColor, + borderRadius: 12, + paddingVertical: 20, + alignItems: 'center', + justifyContent: 'center', + }, + sfxButtonPressed: { + opacity: 0.7, + }, + sfxButtonText: { + color: '#fff', + fontSize: 16, + fontWeight: '700', + }, + sfxButtonStatus: { + color: 'rgba(255,255,255,0.7)', + fontSize: 12, + marginTop: 4, + }, + cacheUri: { + fontSize: 11, + color: '#999', + fontFamily: 'monospace', + marginLeft: 8, + marginVertical: 2, + }, + buttonRow: { + flexDirection: 'row', + gap: 8, + }, +}); diff --git a/apps/native-component-list/src/screens/Audio/AudioScreen.tsx b/apps/native-component-list/src/screens/Audio/AudioScreen.tsx index fcba58c5858a75..3f000133858615 100644 --- a/apps/native-component-list/src/screens/Audio/AudioScreen.tsx +++ b/apps/native-component-list/src/screens/Audio/AudioScreen.tsx @@ -1,6 +1,12 @@ +import { preload } from 'expo-audio'; + +import { sfx1, sfx2 } from './AudioPreloadScreen'; import { optionalRequire } from '../../navigation/routeBuilder'; import ComponentListScreen, { apiScreensToListElements } from '../ComponentListScreen'; +preload(sfx1); +preload(sfx2); + export const AudioScreens = [ { name: 'Expo Audio Player', @@ -34,7 +40,6 @@ export const AudioScreens = [ return optionalRequire(() => require('./CreateAudioPlayerScreen')); }, }, - { name: 'Expo Audio Lock Screen Controls', route: 'audio/expo-audio-controls', @@ -51,6 +56,14 @@ export const AudioScreens = [ return optionalRequire(() => require('./AudioEventsScreen')); }, }, + { + name: 'Expo Audio Preloading', + route: 'audio/expo-audio-preload', + options: {}, + getComponent() { + return optionalRequire(() => require('./AudioPreloadScreen')); + }, + }, ]; export default function AudioScreen() { diff --git a/packages/expo-audio/build/Audio.types.d.ts b/packages/expo-audio/build/Audio.types.d.ts index 2f88e3f2ba1ce3..4aba6ad4505f98 100644 --- a/packages/expo-audio/build/Audio.types.d.ts +++ b/packages/expo-audio/build/Audio.types.d.ts @@ -116,6 +116,39 @@ export type AudioPlayerOptions = { * @default false */ keepAudioSessionActive?: boolean; + /** + * The duration in seconds the player should buffer ahead of the current playback position. + * A higher value improves playback stability at the cost of more memory/network usage. + * + * - **iOS**: Maps to `AVPlayerItem.preferredForwardBufferDuration`. A value of `0` lets the system decide. + * - **Android**: Configures ExoPlayer's `DefaultLoadControl` max buffer duration. + * - **Web**: Not applicable (browser manages buffering). + * + * @default 0 (system default) + * + * @platform ios + * @platform android + */ + preferredForwardBufferDuration?: number; +}; +/** + * Options for configuring audio preloading behavior. + */ +export type PreloadOptions = { + /** + * The duration in seconds the player should buffer ahead of the current playback position. + * A higher value improves playback stability at the cost of more memory/network usage. + * + * - **iOS**: Maps to `AVPlayerItem.preferredForwardBufferDuration`. A value of `0` lets the system decide. + * - **Android**: Configures ExoPlayer's buffer duration. + * - **Web**: Not applicable (browser manages buffering). + * + * @default 10 + * + * @platform ios + * @platform android + */ + preferredForwardBufferDuration?: number; }; /** * @deprecated Use `AudioPlayerOptions` instead. diff --git a/packages/expo-audio/build/Audio.types.d.ts.map b/packages/expo-audio/build/Audio.types.d.ts.map index 41b111a5169100..d34f43d8e9755e 100644 --- a/packages/expo-audio/build/Audio.types.d.ts.map +++ b/packages/expo-audio/build/Audio.types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"Audio.types.d.ts","sourceRoot":"","sources":["../src/Audio.types.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"Audio.types.d.ts","sourceRoot":"","sources":["../src/Audio.types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/expo-audio/build/Audio.types.js.map b/packages/expo-audio/build/Audio.types.js.map index 12f73c729cfacc..3bb272393a0502 100644 --- a/packages/expo-audio/build/Audio.types.js.map +++ b/packages/expo-audio/build/Audio.types.js.map @@ -1 +1 @@ -{"version":3,"file":"Audio.types.js","sourceRoot":"","sources":["../src/Audio.types.ts"],"names":[],"mappings":"","sourcesContent":["import { AudioQuality, IOSOutputFormat } from './RecordingConstants';\n\n/**\n * Represents audio source information returned from native.\n * This is the object returned when reading sources from a queue.\n */\nexport type AudioSourceInfo = {\n /**\n * A string representing the resource identifier for the audio.\n */\n uri?: string;\n /**\n * An optional display name for the audio source.\n */\n name?: string;\n};\n\n// @docsMissing\nexport type AudioSource =\n | string\n | number\n | null\n | {\n /**\n * A string representing the resource identifier for the audio,\n * which could be an HTTPS address, a local file path, or the name of a static audio file resource.\n */\n uri?: string;\n /**\n * The asset ID of a local audio asset, acquired with the `require` function.\n * This property is exclusive with the `uri` property. When both are present, the `assetId` will be ignored.\n */\n assetId?: number;\n /**\n * An object representing the HTTP headers to send along with the request for a remote audio source.\n * On web requires the `Access-Control-Allow-Origin` header returned by the server to include the current domain.\n */\n headers?: Record;\n /**\n * An optional display name for the audio source.\n * Useful for showing track names in a queue or playlist UI.\n */\n name?: string;\n };\n\n/**\n * Options for configuring audio player behavior.\n */\nexport type AudioPlayerOptions = {\n /**\n * How often (in milliseconds) to emit playback status updates. Defaults to 500ms.\n *\n * @example\n * ```tsx\n * import { useAudioPlayer } from 'expo-audio';\n *\n * export default function App() {\n * const player = useAudioPlayer(source);\n *\n * // High-frequency updates for smooth progress bars\n * const player = useAudioPlayer(source, { updateInterval: 100 });\n *\n * // Standard updates (default behavior)\n * const player = useAudioPlayer(source, { updateInterval: 500 });\n *\n * // Low-frequency updates for better performance\n * const player = useAudioPlayer(source, { updateInterval: 1000 });\n * }\n * ```\n *\n * @default 500ms\n *\n * @platform ios\n * @platform android\n * @platform web\n */\n updateInterval?: number;\n /**\n * If set to `true`, the system will attempt to download the resource to the device before loading.\n * This value defaults to `false`.\n *\n * Works with:\n * - Local assets from `require('path/to/file')`\n * - Remote HTTP/HTTPS URLs\n * - Asset objects\n *\n * When enabled, this ensures the audio file is fully downloaded before playback begins.\n * This can improve playback performance and reduce buffering, especially for users\n * managing multiple audio players simultaneously.\n *\n * On Android and iOS, this will download the audio file to the device's tmp directory before playback begins.\n * The system will purge the file at its discretion.\n *\n * On web, this will download the audio file to the user's device memory and make it available for the user to play.\n * The system will usually purge the file from memory after a reload or on memory pressure.\n * On web, CORS restrictions apply to the blob url, so you need to make sure the server returns the `Access-Control-Allow-Origin` header.\n *\n * @platform ios\n * @platform web\n * @platform android\n */\n downloadFirst?: boolean;\n /**\n * Determines the [cross origin policy](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin) used by the underlying native view on web.\n * If `undefined` (default), does not use CORS at all. If set to `'anonymous'`, the audio will be loaded with CORS enabled.\n * Note that some audio may not play if CORS is enabled, depending on the CDN settings.\n * If you encounter issues, consider adjusting the `crossOrigin` property.\n *\n *\n * @platform web\n * @default undefined\n */\n crossOrigin?: 'anonymous' | 'use-credentials';\n /**\n * If set to `true`, the audio session will not be deactivated when this player pauses or finishes playback.\n * This prevents interrupting other audio sources (like videos) when the audio ends.\n *\n * Useful for sound effects that should not interfere with ongoing video playback or other audio.\n * The audio session for this player will not be deactivated automatically when the player finishes playback.\n *\n * > **Note:** If needed, you can manually deactivate the audio session using `setIsAudioActiveAsync(false)`.\n *\n * @platform ios\n * @default false\n */\n keepAudioSessionActive?: boolean;\n};\n\n/**\n * @deprecated Use `AudioPlayerOptions` instead.\n * Options for audio loading behavior.\n */\nexport type AudioLoadOptions = AudioPlayerOptions;\n\n/**\n * Represents an available audio input device for recording.\n *\n * This type describes audio input sources like built-in microphones, external microphones,\n * or other audio input devices that can be used for recording. Each input has an identifying\n * information that can be used to select the preferred recording source.\n */\nexport type RecordingInput = {\n /** Human-readable name of the audio input device. */\n name: string;\n /** Type or category of the input device (for example, 'Built-in Microphone', 'External Microphone'). */\n type: string;\n /** Unique identifier for the input device, used to select the input ('Built-in Microphone', 'External Microphone') for recording. */\n uid: string;\n};\n\n/**\n * Pitch correction quality settings for audio playback rate changes.\n *\n * When changing playback rate, pitch correction can be applied to maintain the original pitch.\n * Different quality levels offer trade-offs between processing power and audio quality.\n *\n * @platform ios\n */\nexport type PitchCorrectionQuality = 'low' | 'medium' | 'high';\n\n/**\n * Comprehensive status information for an `AudioPlayer`.\n *\n * This object contains all the current state information about audio playback,\n * including playback position, duration, loading state, and playback settings.\n * Used by `useAudioPlayerStatus()` to provide real-time status updates.\n */\nexport type AudioStatus = {\n /** Unique identifier for the player instance. */\n id: string;\n /** Current playback position in seconds. */\n currentTime: number;\n /** String representation of the player's internal playback state. */\n playbackState: string;\n /** String representation of the player's time control status (playing/paused/waiting). */\n timeControlStatus: string;\n /** Reason why the player is waiting to play (if applicable). */\n reasonForWaitingToPlay: string;\n /** Whether the player is currently muted. */\n mute: boolean;\n /** Total duration of the audio in seconds, or 0 if not yet determined. */\n duration: number;\n /** Whether the audio is currently playing. */\n playing: boolean;\n /** Whether the audio is set to loop when it reaches the end. */\n loop: boolean;\n /** Whether the audio just finished playing. */\n didJustFinish: boolean;\n /** Whether the player is currently buffering data. */\n isBuffering: boolean;\n /** Whether the audio has finished loading and is ready to play. */\n isLoaded: boolean;\n /** Current playback rate (1.0 = normal speed). */\n playbackRate: number;\n /**\n * Whether pitch correction is enabled for rate changes.\n * @default true\n */\n shouldCorrectPitch: boolean;\n /**\n * Whether the media services were reset by the system.\n * When `true`, the player was interrupted because the system's media daemon crashed.\n * The player will automatically attempt to recover by reloading the source and resuming playback.\n * @platform ios\n */\n mediaServicesDidReset?: boolean;\n};\n\n/**\n * Status information for recording operations from the event system.\n *\n * This type represents the status data emitted by `recordingStatusUpdate` events.\n * It contains high-level information about the recording session and any errors.\n * Used internally by the event system. Most users should use `useAudioRecorderState()` instead.\n */\nexport type RecordingStatus = {\n /** Unique identifier for the recording session. */\n id: string;\n /** Whether the recording has finished (stopped). */\n isFinished: boolean;\n /** Whether an error occurred during recording. */\n hasError: boolean;\n /** Error message if an error occurred, `null` otherwise. */\n error: string | null;\n /** File URL of the completed recording, if available. */\n url: string | null;\n /**\n * Whether the media services were reset by the system.\n * When `true`, the recording was interrupted because the system's media daemon crashed.\n * The recorder is now invalid and must be re-prepared by calling `prepareToRecordAsync()`.\n * @platform ios\n */\n mediaServicesDidReset?: boolean;\n};\n\n/**\n * Current state information for an `AudioRecorder`.\n *\n * This object contains detailed information about the recorder's current state,\n * including recording status, duration, and technical details. This is what you get\n * when calling `recorder.getStatus()` or using `useAudioRecorderState()`.\n */\nexport type RecorderState = {\n /** Whether the recorder is ready and able to record. */\n canRecord: boolean;\n /** Whether recording is currently in progress. */\n isRecording: boolean;\n /** Duration of the current recording in milliseconds. */\n durationMillis: number;\n /** Whether the media services have been reset (typically indicates a system interruption). */\n mediaServicesDidReset: boolean;\n /** Current audio level/volume being recorded (if metering is enabled). */\n metering?: number;\n /** File URL where the recording will be saved, if available. */\n url: string | null;\n};\n\n/**\n * Audio output format options for Android recording.\n *\n * Specifies the container format for recorded audio files on Android.\n * Different formats have different compatibility and compression characteristics.\n *\n * @platform android\n */\nexport type AndroidOutputFormat =\n | 'default'\n | '3gp'\n | 'mpeg4'\n | 'amrnb'\n | 'amrwb'\n | 'aac_adts'\n | 'mpeg2ts'\n | 'webm';\n\n/**\n * Audio encoder options for Android recording.\n *\n * Specifies the audio codec used to encode recorded audio on Android.\n * Different encoders offer different quality, compression, and compatibility trade-offs.\n *\n * @platform android\n */\nexport type AndroidAudioEncoder = 'default' | 'amr_nb' | 'amr_wb' | 'aac' | 'he_aac' | 'aac_eld';\n\n/**\n * Bit rate strategies for audio encoding.\n *\n * Determines how the encoder manages bit rate during recording, affecting\n * file size consistency and quality characteristics.\n */\nexport type BitRateStrategy = 'constant' | 'longTermAverage' | 'variableConstrained' | 'variable';\n\n/**\n * Options for controlling how audio recording is started.\n */\nexport type RecordingStartOptions = {\n /**\n * The duration in seconds after which recording should automatically stop.\n * If not provided, recording continues until manually stopped.\n *\n * @platform ios\n * @platform android\n * @platform web\n */\n forDuration?: number;\n /**\n * The time in seconds to wait before starting the recording.\n * If not provided, recording starts immediately.\n *\n * **Platform behavior:**\n * - Android: Ignored, recording starts immediately\n * - iOS: Uses native AVAudioRecorder.record(atTime:) for precise timing.\n * - Web: Ignored, recording starts immediately\n *\n * > **warning** On iOS, the recording process starts immediately (you'll see status updates),\n * but actual audio capture begins after the specified delay. This is not a countdown, since\n * the recorder is active but silent during the delay period.\n *\n * @platform ios\n */\n atTime?: number;\n};\n\nexport type RecordingOptions = {\n /**\n * A boolean that determines whether audio level information will be part of the status object under the \"metering\" key.\n */\n isMeteringEnabled?: boolean;\n /**\n * The desired file extension.\n *\n * @example .caf\n */\n extension: string;\n /**\n * The desired sample rate.\n *\n * @example 44100\n */\n sampleRate: number;\n /**\n * The desired number of channels.\n *\n * @example 2\n */\n numberOfChannels: number;\n /**\n * The desired bit rate.\n *\n * @example 128000\n */\n bitRate: number;\n /**\n * Recording options for the Android platform.\n * @platform android\n */\n android: RecordingOptionsAndroid;\n /**\n * Recording options for the iOS platform.\n * @platform ios\n */\n ios: RecordingOptionsIos;\n /**\n * Recording options for the Web platform.\n * @platform web\n */\n web: RecordingOptionsWeb;\n};\n\n/**\n * Recording options for the web.\n *\n * Web recording uses the `MediaRecorder` API, which has different capabilities\n * compared to native platforms. These options map directly to `MediaRecorder` settings.\n *\n * @platform web\n */\nexport type RecordingOptionsWeb = {\n /** MIME type for the recording (for example, 'audio/webm', 'audio/mp4'). */\n mimeType?: string;\n /** Target bits per second for the recording. */\n bitsPerSecond?: number;\n};\n\n/**\n * Recording configuration options specific to iOS.\n *\n * iOS recording uses `AVAudioRecorder` with extensive format and quality options.\n * These settings provide fine-grained control over the recording characteristics.\n *\n * @platform ios\n */\nexport type RecordingOptionsIos = {\n /**\n * The desired file extension.\n *\n * @example .caf\n */\n extension?: string;\n /**\n * The desired sample rate.\n *\n * @example 44100\n */\n sampleRate?: number;\n /**\n * The desired file format. See the [`IOSOutputFormat`](#iosoutputformat) enum for all valid values.\n */\n outputFormat?: string | IOSOutputFormat | number;\n /**\n * The desired audio quality. See the [`AudioQuality`](#audioquality) enum for all valid values.\n */\n audioQuality: AudioQuality | number;\n /**\n * The desired bit rate strategy. See the next section for an enumeration of all valid values of `bitRateStrategy`.\n */\n bitRateStrategy?: number;\n /**\n * The desired bit depth hint.\n *\n * @example 16\n */\n bitDepthHint?: number;\n /**\n * The desired PCM bit depth.\n *\n * @example 16\n */\n linearPCMBitDepth?: number;\n /**\n * A boolean describing if the PCM data should be formatted in big endian.\n */\n linearPCMIsBigEndian?: boolean;\n /**\n * A boolean describing if the PCM data should be encoded in floating point or integral values.\n */\n linearPCMIsFloat?: boolean;\n};\n\n/**\n * Recording configuration options specific to Android.\n *\n * Android recording uses `MediaRecorder` with options for format, encoder, and file constraints.\n * These settings control the output format and quality characteristics.\n *\n * @platform android\n */\nexport type RecordingOptionsAndroid = {\n /**\n * The desired file extension.\n *\n * @example .caf\n */\n extension?: string;\n /**\n * The desired sample rate.\n *\n * @example 44100\n */\n sampleRate?: number;\n /**\n * The desired file format. See the [`AndroidOutputFormat`](#androidoutputformat) type for all valid values.\n */\n outputFormat: AndroidOutputFormat;\n /**\n * The desired audio encoder. See the [`AndroidAudioEncoder`](#androidaudioencoder) type for all valid values.\n */\n audioEncoder: AndroidAudioEncoder;\n /**\n * The desired maximum file size in bytes, after which the recording will stop (but `stopAndUnloadAsync()` must still\n * be called after this point).\n *\n * @example\n * `65536`\n */\n maxFileSize?: number;\n /**\n * The desired audio Source. See the [`RecordingSource`](#recordingsource) type for all valid values.\n */\n audioSource?: RecordingSource;\n};\n\nexport type AudioMode = {\n /**\n * Determines if audio playback is allowed when the device is in silent mode.\n *\n * @platform ios\n */\n playsInSilentMode: boolean;\n /**\n * Determines how the audio session interacts with other audio sessions.\n *\n * - `'doNotMix'`: Requests exclusive audio focus. Other apps will pause their audio.\n * - `'duckOthers'`: Requests audio focus with ducking. Other apps lower their volume but continue playing.\n * - `'mixWithOthers'`: Audio plays alongside other apps without interrupting them.\n * On Android, this means no audio focus is requested. Best suited for sound effects,\n * UI feedback, or short audio clips.\n *\n * @default 'mixWithOthers'\n */\n interruptionMode: InterruptionMode;\n /**\n * Determines how the audio session interacts with other sessions on Android.\n *\n * @platform android\n * @deprecated Use `interruptionMode` instead, which now works on both platforms.\n */\n interruptionModeAndroid?: InterruptionModeAndroid;\n /**\n * Whether the audio session allows recording.\n *\n * @default false\n * @platform ios\n */\n allowsRecording: boolean;\n /**\n * Whether the audio session stays active when the app moves to the background.\n *\n * > **Note**: On Android, you have to enable the lockscreen controls with [`setActiveForLockScreen`](#setactiveforlockscreenactive-metadata-options) for sustained background playback. Otherwise, the audio will stop after approximately 3 minutes of background playback (OS limitation). Make sure to also appropriately [configure the config-plugin](#configuration-in-app-config).\n * @default false\n */\n shouldPlayInBackground: boolean;\n /**\n * Whether the audio should route through the earpiece.\n * On iOS, this only has an effect when `allowsRecording` is `true` (i.e., the audio session\n * category is `.playAndRecord`). When `false` (the default), audio is routed through the speaker.\n * @default false\n */\n shouldRouteThroughEarpiece: boolean;\n /**\n * Whether audio recording should continue when the app moves to the background.\n *\n * @default false\n * @platform ios\n * @platform android\n */\n allowsBackgroundRecording?: boolean;\n};\n\n/**\n * Audio interruption behavior modes.\n *\n * Controls how your app's audio interacts with other apps' audio.\n *\n * - `'doNotMix'`: Requests exclusive audio focus. Other apps will pause their audio.\n * - `'duckOthers'`: Requests audio focus with ducking. Other apps lower their volume but continue playing.\n * - `'mixWithOthers'`: Audio plays alongside other apps without interrupting them.\n *\n * On Android, this means no audio focus is requested. Best suited for sound effects,\n * UI feedback, or short audio clips. Note that on Android your app won't receive\n * audio focus loss callbacks (for example, during phone calls) when using this mode.\n *\n * > **Note:** When using `setActiveForLockScreen`, this must be set to `doNotMix`.\n *\n * @default 'mixWithOthers'\n */\nexport type InterruptionMode = 'mixWithOthers' | 'doNotMix' | 'duckOthers';\n\n/**\n * @deprecated Use `InterruptionMode` instead, which now works on both platforms.\n */\nexport type InterruptionModeAndroid = InterruptionMode;\n\n/**\n * Recording source for android.\n *\n * An audio source defines both a default physical source of audio signal, and a recording configuration.\n *\n * - `camcorder`: Microphone audio source tuned for video recording, with the same orientation as the camera if available.\n * - `default`: The default audio source.\n * - `mic`: Microphone audio source.\n * - `unprocessed`: Microphone audio source tuned for unprocessed (raw) sound if available, behaves like `default` otherwise.\n * - `voice_communication`: Microphone audio source tuned for voice communications such as VoIP. It will for instance take advantage of echo cancellation or automatic gain control if available.\n * - `voice_performance`: Source for capturing audio meant to be processed in real time and played back for live performance (e.g karaoke). The capture path will minimize latency and coupling with playback path.\n * - `voice_recognition`: Microphone audio source tuned for voice recognition.\n *\n * @see https://developer.android.com/reference/android/media/MediaRecorder.AudioSource\n * @platform android\n */\nexport type RecordingSource =\n | 'camcorder'\n | 'default'\n | 'mic'\n | 'remote_submix'\n | 'unprocessed'\n | 'voice_communication'\n | 'voice_performance'\n | 'voice_recognition';\n\n// @docsMissing\nexport type AudioMetadata = {\n title?: string;\n artist?: string;\n albumTitle?: string;\n artworkUrl?: string;\n};\n\n/**\n * Loop mode for audio playlist playback.\n *\n * - `'none'`: No looping. Playback stops after the last track.\n * - `'single'`: Loops the current track indefinitely.\n * - `'all'`: Loops the entire playlist, returning to the first track after the last.\n */\nexport type AudioPlaylistLoopMode = 'none' | 'single' | 'all';\n\n/**\n * Options for configuring an audio playlist.\n */\nexport type AudioPlaylistOptions = {\n /**\n * Initial sources to add to the playlist. Each source can be a local asset, remote URL, or null.\n * @default []\n */\n sources?: AudioSource[];\n\n /**\n * How often (in milliseconds) to emit playback status updates. Defaults to 500ms.\n * @default 500\n */\n updateInterval?: number;\n\n /**\n * Loop mode for the playlist.\n * - `'none'`: No looping (default)\n * - `'single'`: Loop the current track\n * - `'all'`: Loop the entire playlist\n * @default 'none'\n */\n loop?: AudioPlaylistLoopMode;\n\n /**\n * Sets the `crossOrigin` attribute on the `