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/
|
!*.xcodeproj/project.xcworkspace/
|
||||||
!*.xcworkspace/contents.xcworkspacedata
|
!*.xcworkspace/contents.xcworkspacedata
|
||||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||||
|
iosApp/Pods/
|
||||||
node_modules/
|
node_modules/
|
||||||
composeApp/release/
|
composeApp/release/
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
- Desktop/JVM: `composeApp/src/jvmMain/kotlin/org/spotube/spotube/main.kt` (`mainClass = org.spotube.spotube.MainKt`).
|
- 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`.
|
- 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 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
|
## High-value commands
|
||||||
- Use Gradle wrapper (`./gradlew` or `./gradlew.bat`) only.
|
- Use Gradle wrapper (`./gradlew` or `./gradlew.bat`) only.
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import kotlin.time.Duration
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
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).
|
* 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.
|
* 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)
|
_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) {
|
override fun onError(message: String) {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
_playerState.tryEmit(PlayerState.IDLE)
|
_playerState.tryEmit(PlayerState.IDLE)
|
||||||
|
|||||||
@ -18,14 +18,15 @@
|
|||||||
package org.spotube.spotube.core.audioplayer
|
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
|
* MobileVLCKit comes from CocoaPods into the Xcode project, so Gradle can't bind it with cinterop.
|
||||||
* implements this contract instead and registers it in [NativePlayerRegistry] before the Compose
|
* Swift implements this contract instead and registers it in [NativePlayerRegistry] before the
|
||||||
* view controller is created. The playlist, loop and shuffle logic stays in Kotlin
|
* 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.
|
* ([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.
|
* themselves. [NativePlayerListener] callbacks may arrive on any thread.
|
||||||
*/
|
*/
|
||||||
interface NativePlayerEngine {
|
interface NativePlayerEngine {
|
||||||
@ -53,9 +54,6 @@ interface NativePlayerListener {
|
|||||||
fun onState(state: NativePlayerState)
|
fun onState(state: NativePlayerState)
|
||||||
fun onTime(positionMs: Long)
|
fun onTime(positionMs: Long)
|
||||||
fun onDuration(durationMs: Long)
|
fun onDuration(durationMs: Long)
|
||||||
|
|
||||||
/** Network cache fill of the current media, 0..100. */
|
|
||||||
fun onBuffered(percent: Float)
|
|
||||||
fun onError(message: String)
|
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 = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
11DF02E4B44E5BCF61736473 /* Spotube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Spotube.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@ -35,6 +38,8 @@
|
|||||||
};
|
};
|
||||||
E265D0F6D10F52F5C1374EEB /* Configuration */ = {
|
E265D0F6D10F52F5C1374EEB /* Configuration */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
);
|
||||||
path = Configuration;
|
path = Configuration;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -45,22 +50,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
9A1B2C3D4E5F60718293A4B1 /* VLCKit in Frameworks */,
|
EC1521163DBA0344F6C5A520 /* Pods_iosApp.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
EFA169D322B90F32841D602F = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
E265D0F6D10F52F5C1374EEB /* Configuration */,
|
|
||||||
5BB730BF90365122B74925FF /* iosApp */,
|
|
||||||
835615636B5FE86AA850B7DB /* Products */,
|
|
||||||
);
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
835615636B5FE86AA850B7DB /* Products */ = {
|
835615636B5FE86AA850B7DB /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -69,6 +65,35 @@
|
|||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
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 */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@ -76,10 +101,12 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 6F8AC65FA015674062B91E20 /* Build configuration list for PBXNativeTarget "iosApp" */;
|
buildConfigurationList = 6F8AC65FA015674062B91E20 /* Build configuration list for PBXNativeTarget "iosApp" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
FD8DD8DB9F90C61C8EB92C92 /* [CP] Check Pods Manifest.lock */,
|
||||||
6413B0F29E004BC8B0C4D22A /* Compile Kotlin Framework */,
|
6413B0F29E004BC8B0C4D22A /* Compile Kotlin Framework */,
|
||||||
314349EF3C4149F4A4A72378 /* Sources */,
|
314349EF3C4149F4A4A72378 /* Sources */,
|
||||||
EFCF243BC7D8246E008EF23D /* Frameworks */,
|
EFCF243BC7D8246E008EF23D /* Frameworks */,
|
||||||
ED9557BFD7438D37A5E9513D /* Resources */,
|
ED9557BFD7438D37A5E9513D /* Resources */,
|
||||||
|
19DB94093A1327B00D11FC9C /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -89,9 +116,6 @@
|
|||||||
5BB730BF90365122B74925FF /* iosApp */,
|
5BB730BF90365122B74925FF /* iosApp */,
|
||||||
);
|
);
|
||||||
name = iosApp;
|
name = iosApp;
|
||||||
packageProductDependencies = (
|
|
||||||
9A1B2C3D4E5F60718293A4B3 /* VLCKit */,
|
|
||||||
);
|
|
||||||
productName = iosApp;
|
productName = iosApp;
|
||||||
productReference = 11DF02E4B44E5BCF61736473 /* Spotube.app */;
|
productReference = 11DF02E4B44E5BCF61736473 /* Spotube.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@ -120,9 +144,6 @@
|
|||||||
);
|
);
|
||||||
mainGroup = EFA169D322B90F32841D602F;
|
mainGroup = EFA169D322B90F32841D602F;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
|
||||||
9A1B2C3D4E5F60718293A4B2 /* XCRemoteSwiftPackageReference "vlckit" */,
|
|
||||||
);
|
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 835615636B5FE86AA850B7DB /* Products */;
|
productRefGroup = 835615636B5FE86AA850B7DB /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@ -144,6 +165,23 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase 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 */ = {
|
6413B0F29E004BC8B0C4D22A /* Compile Kotlin Framework */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@ -163,6 +201,28 @@
|
|||||||
shellPath = /bin/sh;
|
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";
|
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 */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@ -176,6 +236,122 @@
|
|||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration 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 */ = {
|
AF5828948B37FEEF5EF7F345 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReferenceAnchor = E265D0F6D10F52F5C1374EEB /* Configuration */;
|
baseConfigurationReferenceAnchor = E265D0F6D10F52F5C1374EEB /* Configuration */;
|
||||||
@ -241,132 +417,9 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
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 */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList 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" */ = {
|
6F8AC65FA015674062B91E20 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
@ -376,26 +429,16 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
79A44D8DEA428263AC7DC2E1 /* Build configuration list for PBXProject "iosApp" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
AF5828948B37FEEF5EF7F345 /* Debug */,
|
||||||
|
1C42200356BD14712FAE1CFA /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* 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 */;
|
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 AVFoundation
|
||||||
import ComposeApp
|
import ComposeApp
|
||||||
import Foundation
|
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.
|
/// `ComposeView` is created.
|
||||||
func registerNativePlayerEngine() {
|
func registerNativePlayerEngine() {
|
||||||
NativePlayerRegistry.shared.factory = VlcPlayerEngineFactory()
|
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.
|
/// `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 {
|
final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegate {
|
||||||
private let player = VLCMediaPlayer()
|
private let player = VLCMediaPlayer()
|
||||||
private var listener: NativePlayerListener?
|
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 released = false
|
||||||
|
|
||||||
private var rate: Float = 1
|
private var rate: Float = 1
|
||||||
private var volumePercent: Int32 = 100
|
private var volumePercent: Int32 = 100
|
||||||
|
private var lastReportedDurationMs: Int64 = 0
|
||||||
|
private var lastReportedState: NativePlayerState?
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
player.delegate = self
|
player.delegate = self
|
||||||
player.timeChangeUpdateInterval = 0.25
|
|
||||||
player.minimalTimePeriod = 250_000
|
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
|
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) {
|
func load(url: String, autoPlay: Bool, startPositionMs: Int64) {
|
||||||
onMain {
|
onMain {
|
||||||
guard !self.released else { return }
|
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)")
|
self.listener?.onError(message: "Invalid URL: \(url)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.stopCurrentMedia()
|
let media = VLCMedia(url: mediaUrl)
|
||||||
self.hasStarted = false
|
|
||||||
self.finished = false
|
|
||||||
if startPositionMs > 0 {
|
if startPositionMs > 0 {
|
||||||
media.addOption(":start-time=\(Double(startPositionMs) / 1000.0)")
|
media.addOption(":start-time=\(Double(startPositionMs) / 1000.0)")
|
||||||
}
|
}
|
||||||
|
self.lastReportedDurationMs = 0
|
||||||
self.player.media = media
|
self.player.media = media
|
||||||
self.listener?.onState(state: .opening)
|
self.report(.opening)
|
||||||
if autoPlay { self.startPlayback() }
|
if autoPlay { self.startPlayback() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -100,7 +92,7 @@ final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegat
|
|||||||
func stop() {
|
func stop() {
|
||||||
onMain {
|
onMain {
|
||||||
guard !self.released else { return }
|
guard !self.released else { return }
|
||||||
self.stopCurrentMedia()
|
self.player.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,25 +131,26 @@ final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegat
|
|||||||
|
|
||||||
// MARK: VLCMediaPlayerDelegate
|
// MARK: VLCMediaPlayerDelegate
|
||||||
|
|
||||||
func mediaPlayerStateChanged(_ newState: VLCMediaPlayerState) {
|
func mediaPlayerStateChanged(_ aNotification: Notification) {
|
||||||
onMain {
|
onMain {
|
||||||
guard !self.released else { return }
|
guard !self.released else { return }
|
||||||
switch newState {
|
switch self.player.state {
|
||||||
case .opening:
|
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:
|
case .playing:
|
||||||
self.hasStarted = true
|
self.report(.playing)
|
||||||
self.applyPlaybackSettings()
|
|
||||||
self.reportDuration()
|
|
||||||
self.listener?.onState(state: .playing)
|
|
||||||
case .paused:
|
case .paused:
|
||||||
self.listener?.onState(state: .paused)
|
self.report(.paused)
|
||||||
case .stopping, .stopped:
|
case .ended:
|
||||||
self.handleStop(isFinalStop: newState == .stopped)
|
self.report(.ended)
|
||||||
|
case .stopped:
|
||||||
|
self.report(.idle)
|
||||||
case .error:
|
case .error:
|
||||||
self.finished = true
|
|
||||||
self.listener?.onError(message: "VLC could not play this media")
|
self.listener?.onError(message: "VLC could not play this media")
|
||||||
self.listener?.onState(state: .idle)
|
self.report(.idle)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -166,77 +159,32 @@ final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegat
|
|||||||
|
|
||||||
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
||||||
onMain {
|
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)
|
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
|
// MARK: Internals
|
||||||
|
|
||||||
private func startPlayback() {
|
private func startPlayback() {
|
||||||
try? AVAudioSession.sharedInstance().setActive(true)
|
try? AVAudioSession.sharedInstance().setActive(true)
|
||||||
let wasPaused = player.state == .paused
|
|
||||||
player.play()
|
player.play()
|
||||||
if wasPaused { kickTimeReporting() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// After a pause, VLCKit 4 withholds time updates until libvlc reports a fresh time point, and
|
/// Sends [state] to Kotlin unless it was the last one sent.
|
||||||
/// on iOS that report doesn't come after a resume: audio plays but the position stays frozen.
|
private func report(_ state: NativePlayerState) {
|
||||||
/// A seek goes through a different callback that does reset VLCKit's time tracking, so seek
|
guard state != lastReportedState else { return }
|
||||||
/// to where we already are.
|
lastReportedState = state
|
||||||
private func kickTimeReporting() {
|
if state == .playing {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
applyPlaybackSettings()
|
||||||
guard let self, !self.released, self.player.isPlaying, self.player.isSeekable else { return }
|
reportDuration()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
listener?.onState(state: state)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyPlaybackSettings() {
|
private func applyPlaybackSettings() {
|
||||||
@ -244,11 +192,12 @@ final class VlcPlayerEngine: NSObject, NativePlayerEngine, VLCMediaPlayerDelegat
|
|||||||
if player.rate != rate { player.rate = rate }
|
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() {
|
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)
|
listener?.onDuration(durationMs: ms)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func onMain(_ work: @escaping () -> Void) {
|
private func onMain(_ work: @escaping () -> Void) {
|
||||||
DispatchQueue.main.async(execute: work)
|
DispatchQueue.main.async(execute: work)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user