mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
Merge pull request #3207 from team-spotube/ios-mobilevlckit-cocoapods
fix(ios): use MobileVLCKit from CocoaPods instead of a git Swift package
This commit is contained in:
commit
3a856d6d03
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.
|
||||
*/
|
||||
@ -130,15 +130,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
|
||||
}
|
||||
@ -166,77 +159,32 @@ 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 }
|
||||
guard !self.released else { return }
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Internals
|
||||
|
||||
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,11 +192,12 @@ 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 {
|
||||
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) {
|
||||
DispatchQueue.main.async(execute: work)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user