mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
fix(ios): use MobileVLCKit from CocoaPods instead of a git Swift package
Android Studio couldn't build the iOS app: resolving the videolan/vlckit Swift package fails inside it (Xcode's source control service reports "Communication with the service was interrupted"), ending in "Missing package product 'VLCKit'". VLCKit's README only documents CocoaPods and Carthage anyway. - Podfile with MobileVLCKit 3.7.5 (iOS-only build, 233 MB instead of the 922 MB cross-platform VLCKit 4 binary); build iosApp.xcworkspace. - VlcPlayerEngine ported to the MobileVLCKit 3 API: it has a real "ended" state, so the stop bookkeeping and the post-resume seek workaround needed for VLCKit 4 are gone. Buffering/ES-added notifications keep arriving while playing, so "playing" is reported whenever VLC is playing. - NativePlayerListener.onBuffered removed: MobileVLCKit has no buffering progress callback. - Pods/ is ignored; AGENTS.md notes the pod install + workspace setup.
This commit is contained in:
parent
f9c040015e
commit
e02c1b73b4
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,6 +16,7 @@ captures
|
||||
!*.xcodeproj/project.xcworkspace/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||
iosApp/Pods/
|
||||
node_modules/
|
||||
composeApp/release/
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
- Desktop/JVM: `composeApp/src/jvmMain/kotlin/org/spotube/spotube/main.kt` (`mainClass = org.spotube.spotube.MainKt`).
|
||||
- Android: `composeApp/src/androidMain/kotlin/org/spotube/spotube/MainActivity.kt`.
|
||||
- iOS bridge: `composeApp/src/iosMain/kotlin/org/spotube/spotube/MainViewController.kt` and `iosApp/iosApp/ContentView.swift`.
|
||||
- iOS audio: MobileVLCKit from CocoaPods (`iosApp/Podfile`), driven by `iosApp/iosApp/VlcPlayerEngine.swift` through `NativePlayerEngine` (iosMain). Run `pod install` in `iosApp/` first, then build `iosApp/iosApp.xcworkspace` (not the `.xcodeproj`, which can't build the Pods). Android Studio's iOS run config must point at the workspace too.
|
||||
|
||||
## High-value commands
|
||||
- Use Gradle wrapper (`./gradlew` or `./gradlew.bat`) only.
|
||||
|
||||
@ -34,7 +34,7 @@ import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* iOS player. Playback is done by VLCKit through [NativePlayerEngine]; AVPlayer misreports the
|
||||
* iOS player. Playback is done by MobileVLCKit through [NativePlayerEngine]; AVPlayer misreports the
|
||||
* duration of some fragmented MP4 streams (doubling it, which leaves the second half silent).
|
||||
* The engine plays a single URL, so the playlist, loop, shuffle and auto-advance live here.
|
||||
*/
|
||||
@ -110,15 +110,6 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
_duration.tryEmit(durationMs.milliseconds)
|
||||
}
|
||||
|
||||
override fun onBuffered(percent: Float) {
|
||||
if (disposed) return
|
||||
val durationMs = _duration.value.inWholeMilliseconds
|
||||
_bufferingPosition.tryEmit(
|
||||
if (durationMs > 0) (durationMs * (percent.coerceIn(0f, 100f) / 100f)).toLong().milliseconds
|
||||
else Duration.ZERO
|
||||
)
|
||||
}
|
||||
|
||||
override fun onError(message: String) {
|
||||
if (disposed) return
|
||||
_playerState.tryEmit(PlayerState.IDLE)
|
||||
|
||||
@ -18,14 +18,15 @@
|
||||
package org.spotube.spotube.core.audioplayer
|
||||
|
||||
/**
|
||||
* Single-media playback engine implemented in Swift on top of VLCKit (`iosApp/iosApp/VlcPlayerEngine.swift`).
|
||||
* Single-media playback engine implemented in Swift on top of MobileVLCKit
|
||||
* (`iosApp/iosApp/VlcPlayerEngine.swift`).
|
||||
*
|
||||
* VLCKit ships as a SwiftPM binary target, so it can't be bound with cinterop from Gradle. Swift
|
||||
* implements this contract instead and registers it in [NativePlayerRegistry] before the Compose
|
||||
* view controller is created. The playlist, loop and shuffle logic stays in Kotlin
|
||||
* MobileVLCKit comes from CocoaPods into the Xcode project, so Gradle can't bind it with cinterop.
|
||||
* Swift implements this contract instead and registers it in [NativePlayerRegistry] before the
|
||||
* Compose view controller is created. The playlist, loop and shuffle logic stays in Kotlin
|
||||
* ([AudioPlayer]); the engine only ever plays one URL at a time.
|
||||
*
|
||||
* Implementations may be called from any thread and must hop to the thread VLCKit needs
|
||||
* Implementations may be called from any thread and must hop to the thread VLC needs
|
||||
* themselves. [NativePlayerListener] callbacks may arrive on any thread.
|
||||
*/
|
||||
interface NativePlayerEngine {
|
||||
@ -53,9 +54,6 @@ interface NativePlayerListener {
|
||||
fun onState(state: NativePlayerState)
|
||||
fun onTime(positionMs: Long)
|
||||
fun onDuration(durationMs: Long)
|
||||
|
||||
/** Network cache fill of the current media, 0..100. */
|
||||
fun onBuffered(percent: Float)
|
||||
fun onError(message: String)
|
||||
}
|
||||
|
||||
|
||||
9
iosApp/Podfile
Normal file
9
iosApp/Podfile
Normal file
@ -0,0 +1,9 @@
|
||||
# MobileVLCKit plays audio on iOS (iosApp/iosApp/VlcPlayerEngine.swift).
|
||||
# After `pod install`, open and build iosApp.xcworkspace, not iosApp.xcodeproj.
|
||||
platform :ios, '18.2'
|
||||
|
||||
target 'iosApp' do
|
||||
use_frameworks!
|
||||
|
||||
pod 'MobileVLCKit', '3.7.5'
|
||||
end
|
||||
16
iosApp/Podfile.lock
Normal file
16
iosApp/Podfile.lock
Normal file
@ -0,0 +1,16 @@
|
||||
PODS:
|
||||
- MobileVLCKit (3.7.5)
|
||||
|
||||
DEPENDENCIES:
|
||||
- MobileVLCKit (= 3.7.5)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- MobileVLCKit
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
MobileVLCKit: 9fb4475cd820b2d271ac394d5d7ba18c525332ab
|
||||
|
||||
PODFILE CHECKSUM: 6dfb07dbc8a64d0c6fe7231a81687c56f08e8250
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
@ -7,11 +7,14 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
9A1B2C3D4E5F60718293A4B1 /* VLCKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9A1B2C3D4E5F60718293A4B3 /* VLCKit */; };
|
||||
EC1521163DBA0344F6C5A520 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F2995B965BD790B4CF9EE4E /* Pods_iosApp.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
11DF02E4B44E5BCF61736473 /* Spotube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Spotube.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
34377C7B259B7315C03BE940 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
3F2995B965BD790B4CF9EE4E /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BA9738EB07A613714DD0F50B /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
@ -35,6 +38,8 @@
|
||||
};
|
||||
E265D0F6D10F52F5C1374EEB /* Configuration */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = Configuration;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -45,22 +50,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9A1B2C3D4E5F60718293A4B1 /* VLCKit in Frameworks */,
|
||||
EC1521163DBA0344F6C5A520 /* Pods_iosApp.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
EFA169D322B90F32841D602F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E265D0F6D10F52F5C1374EEB /* Configuration */,
|
||||
5BB730BF90365122B74925FF /* iosApp */,
|
||||
835615636B5FE86AA850B7DB /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
835615636B5FE86AA850B7DB /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -69,6 +65,35 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
85BADDD666F59900213B22FE /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3F2995B965BD790B4CF9EE4E /* Pods_iosApp.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
92A495F3BC0A87A606BB8FD4 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
34377C7B259B7315C03BE940 /* Pods-iosApp.debug.xcconfig */,
|
||||
BA9738EB07A613714DD0F50B /* Pods-iosApp.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EFA169D322B90F32841D602F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E265D0F6D10F52F5C1374EEB /* Configuration */,
|
||||
5BB730BF90365122B74925FF /* iosApp */,
|
||||
835615636B5FE86AA850B7DB /* Products */,
|
||||
92A495F3BC0A87A606BB8FD4 /* Pods */,
|
||||
85BADDD666F59900213B22FE /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -76,10 +101,12 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 6F8AC65FA015674062B91E20 /* Build configuration list for PBXNativeTarget "iosApp" */;
|
||||
buildPhases = (
|
||||
FD8DD8DB9F90C61C8EB92C92 /* [CP] Check Pods Manifest.lock */,
|
||||
6413B0F29E004BC8B0C4D22A /* Compile Kotlin Framework */,
|
||||
314349EF3C4149F4A4A72378 /* Sources */,
|
||||
EFCF243BC7D8246E008EF23D /* Frameworks */,
|
||||
ED9557BFD7438D37A5E9513D /* Resources */,
|
||||
19DB94093A1327B00D11FC9C /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -89,9 +116,6 @@
|
||||
5BB730BF90365122B74925FF /* iosApp */,
|
||||
);
|
||||
name = iosApp;
|
||||
packageProductDependencies = (
|
||||
9A1B2C3D4E5F60718293A4B3 /* VLCKit */,
|
||||
);
|
||||
productName = iosApp;
|
||||
productReference = 11DF02E4B44E5BCF61736473 /* Spotube.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
@ -120,9 +144,6 @@
|
||||
);
|
||||
mainGroup = EFA169D322B90F32841D602F;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
9A1B2C3D4E5F60718293A4B2 /* XCRemoteSwiftPackageReference "vlckit" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 835615636B5FE86AA850B7DB /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -144,6 +165,23 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
19DB94093A1327B00D11FC9C /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
6413B0F29E004BC8B0C4D22A /* Compile Kotlin Framework */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@ -163,6 +201,28 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
|
||||
};
|
||||
FD8DD8DB9F90C61C8EB92C92 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@ -176,6 +236,122 @@
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
1C42200356BD14712FAE1CFA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = E265D0F6D10F52F5C1374EEB /* Configuration */;
|
||||
baseConfigurationReferenceRelativePath = Config.xcconfig;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
4FB276594B17F1A0D71D2462 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BA9738EB07A613714DD0F50B /* Pods-iosApp.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ARCHS = arm64;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "${TEAM_ID}";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = iosApp/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
AC996E9458BD3DAE9610EA49 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 34377C7B259B7315C03BE940 /* Pods-iosApp.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ARCHS = arm64;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "${TEAM_ID}";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = iosApp/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
AF5828948B37FEEF5EF7F345 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = E265D0F6D10F52F5C1374EEB /* Configuration */;
|
||||
@ -241,132 +417,9 @@
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
1C42200356BD14712FAE1CFA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = E265D0F6D10F52F5C1374EEB /* Configuration */;
|
||||
baseConfigurationReferenceRelativePath = Config.xcconfig;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
AC996E9458BD3DAE9610EA49 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = arm64;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "${TEAM_ID}";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = iosApp/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
4FB276594B17F1A0D71D2462 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = arm64;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "${TEAM_ID}";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = iosApp/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
79A44D8DEA428263AC7DC2E1 /* Build configuration list for PBXProject "iosApp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
AF5828948B37FEEF5EF7F345 /* Debug */,
|
||||
1C42200356BD14712FAE1CFA /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
6F8AC65FA015674062B91E20 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
@ -376,26 +429,16 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
79A44D8DEA428263AC7DC2E1 /* Build configuration list for PBXProject "iosApp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
AF5828948B37FEEF5EF7F345 /* Debug */,
|
||||
1C42200356BD14712FAE1CFA /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
9A1B2C3D4E5F60718293A4B2 /* XCRemoteSwiftPackageReference "vlckit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/videolan/vlckit";
|
||||
requirement = {
|
||||
kind = revision;
|
||||
revision = 6b87735a95fd3aa96f0fdb6d1b465a2d92badd8f;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
9A1B2C3D4E5F60718293A4B3 /* VLCKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9A1B2C3D4E5F60718293A4B2 /* XCRemoteSwiftPackageReference "vlckit" */;
|
||||
productName = VLCKit;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = B056E79C138AE849968965F1 /* Project object */;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"originHash" : "1299735cf5953086f977e465ed8fbd8efae67129992b979462f68d933745ea6b",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "vlckit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/videolan/vlckit",
|
||||
"state" : {
|
||||
"revision" : "6b87735a95fd3aa96f0fdb6d1b465a2d92badd8f"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
10
iosApp/iosApp.xcworkspace/contents.xcworkspacedata
generated
Normal file
10
iosApp/iosApp.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:iosApp.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -16,9 +16,9 @@
|
||||
import AVFoundation
|
||||
import ComposeApp
|
||||
import Foundation
|
||||
import VLCKit
|
||||
import MobileVLCKit
|
||||
|
||||
/// Registers the VLCKit engine with the shared Kotlin `AudioPlayer`. Call before the first
|
||||
/// Registers the MobileVLCKit engine with the shared Kotlin `AudioPlayer`. Call before the first
|
||||
/// `ComposeView` is created.
|
||||
func registerNativePlayerEngine() {
|
||||
NativePlayerRegistry.shared.factory = VlcPlayerEngineFactory()
|
||||
@ -30,30 +30,23 @@ final class VlcPlayerEngineFactory: NSObject, NativePlayerEngineFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Plays one URL at a time with VLCKit. The Kotlin `AudioPlayer` owns the playlist; see
|
||||
/// Plays one URL at a time with MobileVLCKit. The Kotlin `AudioPlayer` owns the playlist; see
|
||||
/// `NativePlayerEngine.kt` for the contract.
|
||||
///
|
||||
/// Every VLCKit call is made on the main queue, in the order the Kotlin side issued it.
|
||||
/// Every VLC call is made on the main queue, in the order the Kotlin side issued it.
|
||||
final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegate {
|
||||
private let player = VLCMediaPlayer()
|
||||
private var listener: NativePlayerListener?
|
||||
|
||||
// VLCKit 4 has no "ended" state: reaching the end of a media goes Stopping -> Stopped, the
|
||||
// same as stop(). `stopRequested` marks the stops this class asked for, so any other stop of
|
||||
// a media that had started playing is a natural end.
|
||||
private var stopRequested = false
|
||||
private var hasStarted = false
|
||||
private var finished = false
|
||||
private var released = false
|
||||
|
||||
private var rate: Float = 1
|
||||
private var volumePercent: Int32 = 100
|
||||
private var lastReportedDurationMs: Int64 = 0
|
||||
private var lastReportedState: NativePlayerState?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
player.delegate = self
|
||||
player.timeChangeUpdateInterval = 0.25
|
||||
player.minimalTimePeriod = 250_000
|
||||
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
|
||||
}
|
||||
|
||||
@ -66,19 +59,18 @@ final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegat
|
||||
func load(url: String, autoPlay: Bool, startPositionMs: Int64) {
|
||||
onMain {
|
||||
guard !self.released else { return }
|
||||
guard let mediaUrl = URL(string: url), let media = VLCMedia(url: mediaUrl) else {
|
||||
guard let mediaUrl = URL(string: url) else {
|
||||
self.listener?.onError(message: "Invalid URL: \(url)")
|
||||
return
|
||||
}
|
||||
|
||||
self.stopCurrentMedia()
|
||||
self.hasStarted = false
|
||||
self.finished = false
|
||||
let media = VLCMedia(url: mediaUrl)
|
||||
if startPositionMs > 0 {
|
||||
media.addOption(":start-time=\(Double(startPositionMs) / 1000.0)")
|
||||
}
|
||||
self.lastReportedDurationMs = 0
|
||||
self.player.media = media
|
||||
self.listener?.onState(state: .opening)
|
||||
self.report(.opening)
|
||||
if autoPlay { self.startPlayback() }
|
||||
}
|
||||
}
|
||||
@ -100,7 +92,7 @@ final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegat
|
||||
func stop() {
|
||||
onMain {
|
||||
guard !self.released else { return }
|
||||
self.stopCurrentMedia()
|
||||
self.player.stop()
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,25 +131,26 @@ final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegat
|
||||
|
||||
// MARK: VLCMediaPlayerDelegate
|
||||
|
||||
func mediaPlayerStateChanged(_ newState: VLCMediaPlayerState) {
|
||||
func mediaPlayerStateChanged(_ aNotification: Notification) {
|
||||
onMain {
|
||||
guard !self.released else { return }
|
||||
switch newState {
|
||||
switch self.player.state {
|
||||
case .opening:
|
||||
self.listener?.onState(state: .opening)
|
||||
self.report(.opening)
|
||||
case .buffering, .esAdded:
|
||||
// MobileVLCKit keeps sending these while it plays; only a stall counts as buffering.
|
||||
self.report(self.player.isPlaying ? .playing : .buffering)
|
||||
case .playing:
|
||||
self.hasStarted = true
|
||||
self.applyPlaybackSettings()
|
||||
self.reportDuration()
|
||||
self.listener?.onState(state: .playing)
|
||||
self.report(.playing)
|
||||
case .paused:
|
||||
self.listener?.onState(state: .paused)
|
||||
case .stopping, .stopped:
|
||||
self.handleStop(isFinalStop: newState == .stopped)
|
||||
self.report(.paused)
|
||||
case .ended:
|
||||
self.report(.ended)
|
||||
case .stopped:
|
||||
self.report(.idle)
|
||||
case .error:
|
||||
self.finished = true
|
||||
self.listener?.onError(message: "VLC could not play this media")
|
||||
self.listener?.onState(state: .idle)
|
||||
self.report(.idle)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -165,29 +158,13 @@ final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegat
|
||||
}
|
||||
|
||||
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
||||
onMain {
|
||||
guard !self.released, let ms = self.player.time.value?.int64Value, ms >= 0 else { return }
|
||||
self.listener?.onTime(positionMs: ms)
|
||||
}
|
||||
}
|
||||
|
||||
func mediaPlayerLengthChanged(_ length: Int64) {
|
||||
onMain {
|
||||
guard !self.released else { return }
|
||||
self.listener?.onDuration(durationMs: length)
|
||||
}
|
||||
}
|
||||
|
||||
func mediaPlayerBufferingChanged(_ progress: Float) {
|
||||
onMain {
|
||||
guard !self.released else { return }
|
||||
self.listener?.onBuffered(percent: progress * 100)
|
||||
// VLC always reports 0 and 1 before the first frame; only mid-playback stalls matter.
|
||||
guard self.hasStarted else { return }
|
||||
if progress < 1 {
|
||||
self.listener?.onState(state: .buffering)
|
||||
} else if self.player.isPlaying {
|
||||
self.listener?.onState(state: .playing)
|
||||
// Time only advances while playing, whatever the last state notification said.
|
||||
if self.player.isPlaying { self.report(.playing) }
|
||||
self.reportDuration()
|
||||
if let ms = self.player.time.value?.int64Value, ms >= 0 {
|
||||
self.listener?.onTime(positionMs: ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -196,47 +173,18 @@ final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegat
|
||||
|
||||
private func startPlayback() {
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
let wasPaused = player.state == .paused
|
||||
player.play()
|
||||
if wasPaused { kickTimeReporting() }
|
||||
}
|
||||
|
||||
/// After a pause, VLCKit 4 withholds time updates until libvlc reports a fresh time point, and
|
||||
/// on iOS that report doesn't come after a resume: audio plays but the position stays frozen.
|
||||
/// A seek goes through a different callback that does reset VLCKit's time tracking, so seek
|
||||
/// to where we already are.
|
||||
private func kickTimeReporting() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
||||
guard let self, !self.released, self.player.isPlaying, self.player.isSeekable else { return }
|
||||
self.player.time = self.player.time
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops the current media and marks the resulting stop events as ours.
|
||||
private func stopCurrentMedia() {
|
||||
switch player.state {
|
||||
case .opening, .playing, .paused, .stopping:
|
||||
stopRequested = true
|
||||
player.stop()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func handleStop(isFinalStop: Bool) {
|
||||
if stopRequested {
|
||||
if isFinalStop {
|
||||
stopRequested = false
|
||||
listener?.onState(state: .idle)
|
||||
}
|
||||
return
|
||||
}
|
||||
if hasStarted && !finished {
|
||||
finished = true
|
||||
listener?.onState(state: .ended)
|
||||
} else if isFinalStop && !finished {
|
||||
listener?.onState(state: .idle)
|
||||
/// Sends [state] to Kotlin unless it was the last one sent.
|
||||
private func report(_ state: NativePlayerState) {
|
||||
guard state != lastReportedState else { return }
|
||||
lastReportedState = state
|
||||
if state == .playing {
|
||||
applyPlaybackSettings()
|
||||
reportDuration()
|
||||
}
|
||||
listener?.onState(state: state)
|
||||
}
|
||||
|
||||
private func applyPlaybackSettings() {
|
||||
@ -244,10 +192,11 @@ final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegat
|
||||
if player.rate != rate { player.rate = rate }
|
||||
}
|
||||
|
||||
/// MobileVLCKit has no length callback, so the length is read from the media as it plays.
|
||||
private func reportDuration() {
|
||||
if let ms = player.media?.length.value?.int64Value, ms > 0 {
|
||||
listener?.onDuration(durationMs: ms)
|
||||
}
|
||||
guard let ms = player.media?.length.value?.int64Value, ms > 0, ms != lastReportedDurationMs else { return }
|
||||
lastReportedDurationMs = ms
|
||||
listener?.onDuration(durationMs: ms)
|
||||
}
|
||||
|
||||
private func onMain(_ work: @escaping () -> Void) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user