@@ -24,8 +24,8 @@ class AbsAudioPlayer : Plugin() {
|
||||
|
||||
lateinit var mainActivity: MainActivity
|
||||
lateinit var apiHandler:ApiHandler
|
||||
lateinit var playerNotificationService: PlayerNotificationService
|
||||
|
||||
lateinit var playerNotificationService: PlayerNotificationService
|
||||
override fun load() {
|
||||
mainActivity = (activity as MainActivity)
|
||||
apiHandler = ApiHandler(mainActivity)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"integrations": {
|
||||
"capacitor": {}
|
||||
},
|
||||
"type": "custom"
|
||||
}
|
||||
@@ -9,10 +9,20 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */; };
|
||||
3A200C1727D64E0D00CBF02E /* Audiobook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A200C1627D64E0D00CBF02E /* Audiobook.swift */; };
|
||||
3A90295F280968E700E1D427 /* PlaybackReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90295E280968E700E1D427 /* PlaybackReport.swift */; };
|
||||
3AB34053280829BF0039308B /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB34052280829BF0039308B /* Extensions.swift */; };
|
||||
3AB34055280832720039308B /* PlayerEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB34054280832720039308B /* PlayerEvents.swift */; };
|
||||
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */; };
|
||||
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF618E2804325C0070250E /* PlayerHandler.swift */; };
|
||||
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE428043E50006DB301 /* AbsDatabase.swift */; };
|
||||
3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE628043E72006DB301 /* AbsDatabase.m */; };
|
||||
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */; };
|
||||
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEA280443DD006DB301 /* Database.swift */; };
|
||||
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEC28044E6C006DB301 /* Store.swift */; };
|
||||
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970B2806E2590096F747 /* ApiClient.swift */; };
|
||||
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */; };
|
||||
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */; };
|
||||
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */; };
|
||||
4D8D410C26E17C3A00BA5F0D /* MyNativeAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8D410B26E17C3A00BA5F0D /* MyNativeAudio.swift */; };
|
||||
4D8D412E26E187E500BA5F0D /* MyNativeAudio.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D8D412D26E187E500BA5F0D /* MyNativeAudio.m */; };
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||
@@ -25,11 +35,21 @@
|
||||
/* Begin PBXFileReference section */
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
|
||||
3A200C1627D64E0D00CBF02E /* Audiobook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Audiobook.swift; sourceTree = "<group>"; };
|
||||
3A90295E280968E700E1D427 /* PlaybackReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackReport.swift; sourceTree = "<group>"; };
|
||||
3AB34052280829BF0039308B /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
3AB34054280832720039308B /* PlayerEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerEvents.swift; sourceTree = "<group>"; };
|
||||
3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSession.swift; sourceTree = "<group>"; };
|
||||
3ABF618E2804325C0070250E /* PlayerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerHandler.swift; sourceTree = "<group>"; };
|
||||
3AD4FCE428043E50006DB301 /* AbsDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDatabase.swift; sourceTree = "<group>"; };
|
||||
3AD4FCE628043E72006DB301 /* AbsDatabase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsDatabase.m; sourceTree = "<group>"; };
|
||||
3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnectionConfig.swift; sourceTree = "<group>"; };
|
||||
3AD4FCEA280443DD006DB301 /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
|
||||
3AD4FCEC28044E6C006DB301 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
|
||||
3AF1970B2806E2590096F747 /* ApiClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiClient.swift; sourceTree = "<group>"; };
|
||||
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsAudioPlayer.swift; sourceTree = "<group>"; };
|
||||
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsAudioPlayer.m; sourceTree = "<group>"; };
|
||||
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfo.swift; sourceTree = "<group>"; };
|
||||
4D8D410B26E17C3A00BA5F0D /* MyNativeAudio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyNativeAudio.swift; sourceTree = "<group>"; };
|
||||
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
4D8D412D26E187E500BA5F0D /* MyNativeAudio.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MyNativeAudio.m; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -63,20 +83,42 @@
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3AFCB5E427EA231D00ECCC05 /* audio */ = {
|
||||
3ABF6190280432610070250E /* player */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */,
|
||||
4D8D410B26E17C3A00BA5F0D /* MyNativeAudio.swift */,
|
||||
4D8D412D26E187E500BA5F0D /* MyNativeAudio.m */,
|
||||
3ABF618E2804325C0070250E /* PlayerHandler.swift */,
|
||||
);
|
||||
path = audio;
|
||||
path = player;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3AC8248B27F2316900529205 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3AFCB5E527EA232A00ECCC05 /* models */,
|
||||
3AFCB5E627EA23F700ECCC05 /* util */,
|
||||
3ABF6190280432610070250E /* player */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3AFCB5E427EA231D00ECCC05 /* plugins */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3AD4FCE428043E50006DB301 /* AbsDatabase.swift */,
|
||||
3AD4FCE628043E72006DB301 /* AbsDatabase.m */,
|
||||
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */,
|
||||
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */,
|
||||
);
|
||||
path = plugins;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3AFCB5E527EA232A00ECCC05 /* models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3A200C1627D64E0D00CBF02E /* Audiobook.swift */,
|
||||
3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */,
|
||||
3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */,
|
||||
3A90295E280968E700E1D427 /* PlaybackReport.swift */,
|
||||
);
|
||||
path = models;
|
||||
sourceTree = "<group>";
|
||||
@@ -85,6 +127,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */,
|
||||
3AD4FCEA280443DD006DB301 /* Database.swift */,
|
||||
3AD4FCEC28044E6C006DB301 /* Store.swift */,
|
||||
3AF1970B2806E2590096F747 /* ApiClient.swift */,
|
||||
3AB34052280829BF0039308B /* Extensions.swift */,
|
||||
3AB34054280832720039308B /* PlayerEvents.swift */,
|
||||
);
|
||||
path = util;
|
||||
sourceTree = "<group>";
|
||||
@@ -92,6 +139,7 @@
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3AC8248B27F2316900529205 /* Shared */,
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
@@ -110,9 +158,7 @@
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3AFCB5E627EA23F700ECCC05 /* util */,
|
||||
3AFCB5E527EA232A00ECCC05 /* models */,
|
||||
3AFCB5E427EA231D00ECCC05 /* audio */,
|
||||
3AFCB5E427EA231D00ECCC05 /* plugins */,
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
@@ -163,8 +209,8 @@
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 0920;
|
||||
LastSwiftUpdateCheck = 920;
|
||||
LastUpgradeCheck = 1330;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
@@ -248,12 +294,22 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4D8D412E26E187E500BA5F0D /* MyNativeAudio.m in Sources */,
|
||||
3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */,
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
4D8D410C26E17C3A00BA5F0D /* MyNativeAudio.swift in Sources */,
|
||||
3A200C1727D64E0D00CBF02E /* Audiobook.swift in Sources */,
|
||||
3A90295F280968E700E1D427 /* PlaybackReport.swift in Sources */,
|
||||
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */,
|
||||
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */,
|
||||
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */,
|
||||
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */,
|
||||
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */,
|
||||
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */,
|
||||
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
|
||||
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
|
||||
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
|
||||
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
||||
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */,
|
||||
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
|
||||
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -293,6 +349,7 @@
|
||||
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;
|
||||
@@ -300,8 +357,10 @@
|
||||
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;
|
||||
@@ -350,6 +409,7 @@
|
||||
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;
|
||||
@@ -357,8 +417,10 @@
|
||||
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;
|
||||
@@ -390,11 +452,11 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||
DEVELOPMENT_TEAM = N8AA4S3S96;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
@@ -414,11 +476,11 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||
DEVELOPMENT_TEAM = N8AA4S3S96;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "App.app"
|
||||
BlueprintName = "App"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "App.app"
|
||||
BlueprintName = "App"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "App.app"
|
||||
BlueprintName = "App"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "car",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-180.png",
|
||||
"idiom" : "car",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-48.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "notificationCenter",
|
||||
"scale" : "2x",
|
||||
"size" : "24x24",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-55.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "notificationCenter",
|
||||
"scale" : "2x",
|
||||
"size" : "27.5x27.5",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-87.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "notificationCenter",
|
||||
"scale" : "2x",
|
||||
"size" : "33x33",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-88.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "44x44",
|
||||
"subtype" : "40mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "46x46",
|
||||
"subtype" : "41mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-100.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "50x50",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "51x51",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-172.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "86x86",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-196.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "98x98",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-216.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "108x108",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "117x117",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-1024.png",
|
||||
"idiom" : "watch-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-1024.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 605 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 743 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 927 B |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>audiobookshelf-app</string>
|
||||
<string>Audiobookshelf</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -27,6 +27,10 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INPlayMediaIntent</string>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Capacitor/Capacitor.h>
|
||||
|
||||
CAP_PLUGIN(MyNativeAudio, "MyNativeAudio",
|
||||
CAP_PLUGIN_METHOD(initPlayer, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(seekPlayer, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(terminateStream, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(getStreamSyncData, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
|
||||
|
||||
CAP_PLUGIN_METHOD(setSleepTimer, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(increaseSleepTime, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(decreaseSleepTime, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(cancelSleepTimer, CAPPluginReturnPromise);
|
||||
)
|
||||
@@ -1,280 +0,0 @@
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import MediaPlayer
|
||||
import AVKit
|
||||
|
||||
func parseSleepTime(millis: String?) -> Double {
|
||||
(Double(millis ?? "0") ?? 0) / 1000
|
||||
}
|
||||
|
||||
@objc(MyNativeAudio)
|
||||
public class MyNativeAudio: CAPPlugin {
|
||||
var currentPlayer: AudioPlayer?
|
||||
|
||||
var playerContext = 0
|
||||
|
||||
var currentSleepTimer: Timer? = nil
|
||||
var remainingSleepDuration: Double = 0
|
||||
|
||||
@objc func initPlayer(_ call: CAPPluginCall) {
|
||||
NSLog("Init Player")
|
||||
let audiobook = Audiobook(
|
||||
streamId: call.getString("id")!,
|
||||
audiobookId: call.getString("audiobookId")!,
|
||||
playlistUrl: call.getString("playlistUrl")!,
|
||||
|
||||
startTime: (Double(call.getString("startTime") ?? "0") ?? 0.0) / 1000,
|
||||
duration: call.getDouble("duration") ?? 0,
|
||||
|
||||
title: call.getString("title") ?? "No Title",
|
||||
series: call.getString("series"),
|
||||
author: call.getString("author"),
|
||||
artworkUrl: call.getString("cover"),
|
||||
|
||||
token: call.getString("token") ?? ""
|
||||
)
|
||||
let playWhenReady = call.getBool("playWhenReady", false)
|
||||
|
||||
if currentPlayer != nil && currentPlayer?.audiobook.streamId == audiobook.streamId {
|
||||
if playWhenReady {
|
||||
self.currentPlayer?.play()
|
||||
}
|
||||
|
||||
call.resolve(["success": true])
|
||||
return
|
||||
} else if currentPlayer != nil && currentPlayer?.audiobook.streamId != audiobook.streamId {
|
||||
stop()
|
||||
}
|
||||
|
||||
currentPlayer = AudioPlayer(audiobook: audiobook, playWhenReady: playWhenReady)
|
||||
currentPlayer!.addObserver(self, forKeyPath: #keyPath(AudioPlayer.status), options: .new, context: &playerContext)
|
||||
|
||||
call.resolve(["success": true])
|
||||
}
|
||||
|
||||
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard context == &playerContext else {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
return
|
||||
}
|
||||
|
||||
if keyPath == #keyPath(AudioPlayer.status) {
|
||||
sendMetadata()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func seekForward(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
||||
let destinationTime = self.currentPlayer!.getCurrentTime() + amount
|
||||
|
||||
self.currentPlayer!.seek(destinationTime)
|
||||
call.resolve()
|
||||
}
|
||||
@objc func seekBackward(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
||||
let destinationTime = self.currentPlayer!.getCurrentTime() - amount
|
||||
|
||||
self.currentPlayer!.seek(destinationTime)
|
||||
call.resolve()
|
||||
}
|
||||
@objc func seekPlayer(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let seekTime = (Double(call.getString("timeMs", "0")) ?? 0) / 1000
|
||||
NSLog("Seek Player \(seekTime)")
|
||||
|
||||
self.currentPlayer!.seek(seekTime)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func pausePlayer(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
self.currentPlayer!.pause()
|
||||
|
||||
sendPlaybackStatusUpdate(false)
|
||||
call.resolve()
|
||||
}
|
||||
@objc func playPlayer(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
self.currentPlayer!.play(allowSeekBack: true)
|
||||
|
||||
sendPlaybackStatusUpdate(true)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func terminateStream(_ call: CAPPluginCall) {
|
||||
stop()
|
||||
call.resolve()
|
||||
}
|
||||
@objc func stop(_ call: CAPPluginCall? = nil) {
|
||||
if self.currentPlayer != nil {
|
||||
self.currentPlayer!.destroy()
|
||||
}
|
||||
self.currentPlayer = nil
|
||||
|
||||
if call != nil {
|
||||
call!.resolve([ "result": true ])
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getCurrentTime(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = self.currentPlayer?.getCurrentTime() ?? 0
|
||||
call.resolve([ "value": currentTime * 1000, "bufferedTime": currentTime * 1000 ])
|
||||
}
|
||||
@objc func getStreamSyncData(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
call.resolve([ "isPlaying": false, "lastPauseTime": 0, "id": nil ])
|
||||
return
|
||||
}
|
||||
|
||||
call.resolve([ "isPlaying": self.currentPlayer!.rate > 0.0, "lastPauseTime": 0, "id": self.currentPlayer?.audiobook.streamId as Any ])
|
||||
}
|
||||
|
||||
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let speed = call.getFloat("speed") ?? 0
|
||||
self.currentPlayer!.setPlaybackRate(speed)
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func setSleepTimer(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let time = parseSleepTime(millis: call.getString("time"))
|
||||
setSleepTimer(seconds: time)
|
||||
|
||||
call.resolve([ "success": true ])
|
||||
}
|
||||
@objc func increaseSleepTime(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
var time = self.remainingSleepDuration + parseSleepTime(millis: call.getString("time"))
|
||||
if time > self.currentPlayer!.getDuration() {
|
||||
time = self.currentPlayer!.getDuration()
|
||||
}
|
||||
|
||||
setSleepTimer(seconds: time)
|
||||
call.resolve([ "success": true ])
|
||||
}
|
||||
@objc func decreaseSleepTime(_ call: CAPPluginCall) {
|
||||
if self.currentSleepTimer == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
var time = parseSleepTime(millis: call.getString("time"))
|
||||
if time < 0 {
|
||||
time = 0
|
||||
}
|
||||
|
||||
setSleepTimer(seconds: time)
|
||||
call.resolve([
|
||||
"success": true,
|
||||
])
|
||||
}
|
||||
@objc func cancelSleepTimer(_ call: CAPPluginCall) {
|
||||
setSleepTimer(seconds: 0)
|
||||
call.resolve([
|
||||
"success": true,
|
||||
])
|
||||
}
|
||||
|
||||
func setSleepTimer(seconds: Double) {
|
||||
if currentPlayer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
remainingSleepDuration = seconds
|
||||
currentSleepTimer?.invalidate()
|
||||
|
||||
self.notifyListeners("onSleepTimerSet", data: [
|
||||
"value": self.remainingSleepDuration,
|
||||
])
|
||||
|
||||
if seconds == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.currentSleepTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
|
||||
self.updateSleepTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
func updateSleepTime() {
|
||||
if currentPlayer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if self.remainingSleepDuration <= 0 {
|
||||
if currentSleepTimer != nil {
|
||||
currentSleepTimer!.invalidate()
|
||||
}
|
||||
self.notifyListeners("onSleepTimerEnded", data: [
|
||||
"value": currentPlayer!.getCurrentTime(),
|
||||
])
|
||||
|
||||
currentPlayer!.pause()
|
||||
return
|
||||
}
|
||||
|
||||
remainingSleepDuration -= 1
|
||||
self.notifyListeners("onSleepTimerSet", data: [
|
||||
"value": self.remainingSleepDuration,
|
||||
])
|
||||
}
|
||||
|
||||
func sendMetadata() {
|
||||
if self.currentPlayer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.notifyListeners("onMetadata", data: [
|
||||
"duration": self.currentPlayer!.getDuration() * 1000,
|
||||
"currentTime": self.currentPlayer!.getCurrentTime() * 1000,
|
||||
"stateName": "unknown"
|
||||
])
|
||||
}
|
||||
func sendPlaybackStatusUpdate(_ playing: Bool) {
|
||||
self.notifyListeners("onPlayingUpdate", data: [
|
||||
"value": playing
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// AudioBook.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 07.03.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Audiobook {
|
||||
var streamId: String
|
||||
var audiobookId: String
|
||||
var playlistUrl: String
|
||||
|
||||
var startTime: Double = 0.0
|
||||
var duration: Double
|
||||
|
||||
var title: String
|
||||
var series: String?
|
||||
var author: String?
|
||||
var artworkUrl: String?
|
||||
|
||||
var token: String
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// AbsAudioPlayer.m
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 13.04.22.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Capacitor/Capacitor.h>
|
||||
|
||||
CAP_PLUGIN(AbsAudioPlayer, "AbsAudioPlayer",
|
||||
CAP_PLUGIN_METHOD(prepareLibraryItem, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(closePlayback, CAPPluginReturnPromise);
|
||||
|
||||
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
|
||||
|
||||
CAP_PLUGIN_METHOD(playPause, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
|
||||
|
||||
CAP_PLUGIN_METHOD(seek, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise);
|
||||
|
||||
CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise);
|
||||
)
|
||||
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// AbsAudioPlayer.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 13.04.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Capacitor
|
||||
|
||||
@objc(AbsAudioPlayer)
|
||||
public class AbsAudioPlayer: CAPPlugin {
|
||||
override public func load() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(sendPlaybackClosedEvent), name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
}
|
||||
|
||||
@objc func prepareLibraryItem(_ call: CAPPluginCall) {
|
||||
let libraryItemId = call.getString("libraryItemId")
|
||||
let episodeId = call.getString("episodeId")
|
||||
let playWhenReady = call.getBool("playWhenReady", true)
|
||||
|
||||
if libraryItemId == nil {
|
||||
NSLog("provide library item id")
|
||||
return call.resolve()
|
||||
}
|
||||
if libraryItemId!.starts(with: "local") {
|
||||
NSLog("local items are not implemnted")
|
||||
return call.resolve()
|
||||
}
|
||||
|
||||
sendPrepareMetadataEvent(itemId: libraryItemId!, playWhenReady: playWhenReady)
|
||||
ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId) { session in
|
||||
PlayerHandler.startPlayback(session: session, playWhenReady: playWhenReady)
|
||||
|
||||
do {
|
||||
self.sendPlaybackSession(session: try session.asDictionary())
|
||||
call.resolve(try session.asDictionary())
|
||||
} catch(let exception) {
|
||||
NSLog("failed to convert session to json")
|
||||
debugPrint(exception)
|
||||
|
||||
call.resolve([:])
|
||||
}
|
||||
|
||||
self.sendMetadata()
|
||||
}
|
||||
}
|
||||
@objc func closePlayback(_ call: CAPPluginCall) {
|
||||
PlayerHandler.stopPlayback()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func getCurrentTime(_ call: CAPPluginCall) {
|
||||
call.resolve([
|
||||
"value": PlayerHandler.getCurrentTime() ?? 0,
|
||||
"bufferedTime": PlayerHandler.getCurrentTime() ?? 0,
|
||||
])
|
||||
}
|
||||
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
|
||||
PlayerHandler.setPlaybackSpeed(speed: call.getFloat("value", 1.0))
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func playPause(_ call: CAPPluginCall) {
|
||||
PlayerHandler.playPause()
|
||||
call.resolve([ "playing": !PlayerHandler.paused() ])
|
||||
}
|
||||
@objc func playPlayer(_ call: CAPPluginCall) {
|
||||
PlayerHandler.play()
|
||||
call.resolve()
|
||||
}
|
||||
@objc func pausePlayer(_ call: CAPPluginCall) {
|
||||
PlayerHandler.pause()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func seek(_ call: CAPPluginCall) {
|
||||
PlayerHandler.seek(amount: call.getDouble("value", 0.0))
|
||||
call.resolve()
|
||||
}
|
||||
@objc func seekForward(_ call: CAPPluginCall) {
|
||||
PlayerHandler.seekForward(amount: call.getDouble("value", 0.0))
|
||||
call.resolve()
|
||||
}
|
||||
@objc func seekBackward(_ call: CAPPluginCall) {
|
||||
PlayerHandler.seekBackward(amount: call.getDouble("value", 0.0))
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func sendMetadata() {
|
||||
self.notifyListeners("onPlayingUpdate", data: [ "value": !PlayerHandler.paused() ])
|
||||
self.notifyListeners("onMetadata", data: PlayerHandler.getMetdata())
|
||||
}
|
||||
@objc func sendPlaybackClosedEvent() {
|
||||
self.notifyListeners("onPlaybackClosed", data: [ "value": true ])
|
||||
}
|
||||
|
||||
@objc func sendPrepareMetadataEvent(itemId: String, playWhenReady: Bool) {
|
||||
self.notifyListeners("onPrepareMedia", data: [
|
||||
"audiobookId": itemId,
|
||||
"playWhenReady": playWhenReady,
|
||||
])
|
||||
}
|
||||
@objc func sendPlaybackSession(session: [String: Any]) {
|
||||
self.notifyListeners("onPlaybackSession", data: session)
|
||||
}
|
||||
|
||||
/*
|
||||
IMPLEMENTED:
|
||||
|
||||
cancelSleepTimer
|
||||
decreaseSleepTime
|
||||
increaseSleepTime
|
||||
getSleepTimerTime
|
||||
setSleepTimer
|
||||
* closePlayback
|
||||
* setPlaybackSpeed
|
||||
* seekBackward
|
||||
* seekForward
|
||||
* seek
|
||||
* playPause
|
||||
* playPlayer
|
||||
* pausePlayer
|
||||
* getCurrentTime
|
||||
|
||||
* onPlaybackSession
|
||||
* onPrepareMedia
|
||||
|
||||
* onPlaybackClosed
|
||||
* onPlayingUpdate
|
||||
* onMetadata
|
||||
onSleepTimerEnded
|
||||
onSleepTimerSet
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// AbsDatabase.m
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 11.04.22.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Capacitor/Capacitor.h>
|
||||
|
||||
CAP_PLUGIN(AbsDatabase, "AbsDatabase",
|
||||
CAP_PLUGIN_METHOD(setCurrentServerConnectionConfig, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(removeServerConnectionConfig, CAPPluginReturnPromise);
|
||||
|
||||
CAP_PLUGIN_METHOD(logout, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(getDeviceData, CAPPluginReturnPromise);
|
||||
|
||||
CAP_PLUGIN_METHOD(getLocalLibraryItems, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(getLocalLibraryItemByLLId, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
|
||||
)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// AbsDatabase.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 11.04.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import RealmSwift
|
||||
|
||||
extension String {
|
||||
|
||||
func fromBase64() -> String? {
|
||||
guard let data = Data(base64Encoded: self) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
func toBase64() -> String {
|
||||
return Data(self.utf8).base64EncodedString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@objc(AbsDatabase)
|
||||
public class AbsDatabase: CAPPlugin {
|
||||
@objc func setCurrentServerConnectionConfig(_ call: CAPPluginCall) {
|
||||
var id = call.getString("id")
|
||||
let address = call.getString("address", "")
|
||||
let userId = call.getString("userId", "")
|
||||
let username = call.getString("username", "")
|
||||
let token = call.getString("token", "")
|
||||
|
||||
let name = "\(address) (\(username))"
|
||||
let config = ServerConnectionConfig()
|
||||
|
||||
if id == nil {
|
||||
id = "\(address)@\(username)".toBase64()
|
||||
}
|
||||
|
||||
config.id = id!
|
||||
config.name = name
|
||||
config.address = address
|
||||
config.userId = userId
|
||||
config.username = username
|
||||
config.token = token
|
||||
|
||||
Store.serverConfig = config
|
||||
call.resolve(convertServerConnectionConfigToJSON(config: config))
|
||||
}
|
||||
@objc func removeServerConnectionConfig(_ call: CAPPluginCall) {
|
||||
let id = call.getString("serverConnectionConfigId", "")
|
||||
Database.deleteServerConnectionConfig(id: id)
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
@objc func logout(_ call: CAPPluginCall) {
|
||||
Store.serverConfig = nil
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func getDeviceData(_ call: CAPPluginCall) {
|
||||
let configs = Database.getServerConnectionConfigs()
|
||||
let index = Database.getLastActiveConfigIndex()
|
||||
|
||||
call.resolve([
|
||||
"serverConnectionConfigs": configs.map { config in convertServerConnectionConfigToJSON(config: config) },
|
||||
"lastServerConnectionConfigId": configs.first { config in config.index == index }?.id,
|
||||
// Luckily this isn't implemented yet
|
||||
// "currentLocalPlaybackSession": nil,
|
||||
])
|
||||
}
|
||||
|
||||
@objc func getLocalLibraryItems(_ call: CAPPluginCall) {
|
||||
call.resolve([ "value": [] ])
|
||||
}
|
||||
@objc func getLocalLibraryItem(_ call: CAPPluginCall) {
|
||||
call.resolve()
|
||||
}
|
||||
@objc func getLocalLibraryItemByLLId(_ call: CAPPluginCall) {
|
||||
call.resolve()
|
||||
}
|
||||
@objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) {
|
||||
call.resolve([ "value": [] ])
|
||||
}
|
||||
}
|
||||
@@ -9,16 +9,19 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
|
||||
pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog'
|
||||
pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics'
|
||||
pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network'
|
||||
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
|
||||
pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage'
|
||||
pod 'RobingenzCapacitorAppUpdate', :path => '..\..\node_modules\@robingenz\capacitor-app-update'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
||||
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
|
||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
|
||||
pod 'RobingenzCapacitorAppUpdate', :path => '../../node_modules/@robingenz/capacitor-app-update'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
|
||||
pod 'RealmSwift', '~>10'
|
||||
pod 'Alamofire', '~> 5.5'
|
||||
end
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// PlaybackReport.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 15.04.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct PlaybackReport: Decodable, Encodable {
|
||||
var currentTime: Double
|
||||
var duration: Double
|
||||
var timeListened: Double
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// PlaybackSession.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 12.04.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct PlaybackSession: Decodable, Encodable {
|
||||
var id: String
|
||||
var userId: String?
|
||||
var libraryItemId: String?
|
||||
var episodeId: String?
|
||||
var mediaType: String
|
||||
// var mediaMetadata: MediaTypeMetadata - It is not implemented in android?
|
||||
var chapters: [Chapter]
|
||||
var displayTitle: String?
|
||||
var displayAuthor: String?
|
||||
var coverPath: String?
|
||||
var duration: Double
|
||||
var playMethod: Int
|
||||
var startedAt: Double?
|
||||
var updatedAt: Double?
|
||||
var timeListening: Double
|
||||
var audioTracks: [AudioTrack]
|
||||
var currentTime: Double
|
||||
// var libraryItem: LibraryItem?
|
||||
// var localLibraryItem: LocalLibraryItem?
|
||||
var serverConnectionConfigId: String?
|
||||
var serverAddress: String?
|
||||
}
|
||||
struct Chapter: Decodable, Encodable {
|
||||
var id: Int
|
||||
var start: Double
|
||||
var end: Double
|
||||
var title: String?
|
||||
}
|
||||
struct AudioTrack: Decodable, Encodable {
|
||||
var index: Int?
|
||||
var startOffset: Double
|
||||
var duration: Double
|
||||
var title: String
|
||||
var contentUrl: String
|
||||
var mimeType: String
|
||||
var metadata: FileMetadata?
|
||||
// var isLocal: Bool
|
||||
// var localFileId: String?
|
||||
// var audioProbeResult: AudioProbeResult? Needed for local playback
|
||||
var serverIndex: Int?
|
||||
}
|
||||
struct FileMetadata: Decodable, Encodable {
|
||||
var filename: String
|
||||
var ext: String
|
||||
var path: String
|
||||
var relPath: String
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// ServerConnectionConfig.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 11.04.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RealmSwift
|
||||
|
||||
class ServerConnectionConfig: Object {
|
||||
@Persisted(primaryKey: true) var id: String
|
||||
@Persisted(indexed: true) var index: Int
|
||||
@Persisted var name: String
|
||||
@Persisted var address: String
|
||||
@Persisted var userId: String
|
||||
@Persisted var username: String
|
||||
@Persisted var token: String
|
||||
}
|
||||
class ServerConnectionConfigActiveIndex: Object {
|
||||
// This could overflow, but you really would have to try
|
||||
@Persisted var index: Int?
|
||||
|
||||
}
|
||||
|
||||
func convertServerConnectionConfigToJSON(config: ServerConnectionConfig) -> Dictionary<String, Any> {
|
||||
return Database.realmQueue.sync {
|
||||
return [
|
||||
"id": config.id,
|
||||
"name": config.name,
|
||||
"index": config.index,
|
||||
"address": config.address,
|
||||
"userId": config.userId,
|
||||
"username": config.username,
|
||||
"token": config.token,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -24,20 +24,30 @@ class AudioPlayer: NSObject {
|
||||
private var playWhenReady: Bool
|
||||
|
||||
private var audioPlayer: AVPlayer
|
||||
public var audiobook: Audiobook
|
||||
private var playbackSession: PlaybackSession
|
||||
private var activeAudioTrack: AudioTrack
|
||||
|
||||
init(audiobook: Audiobook, playWhenReady: Bool = false) {
|
||||
// MARK: - Constructor
|
||||
init(playbackSession: PlaybackSession, playWhenReady: Bool = false) {
|
||||
self.playWhenReady = playWhenReady
|
||||
self.audiobook = audiobook
|
||||
self.audioPlayer = AVPlayer()
|
||||
self.playbackSession = playbackSession
|
||||
self.status = -1
|
||||
self.rate = 0.0
|
||||
|
||||
if playbackSession.audioTracks.count != 1 || playbackSession.audioTracks[0].mimeType != "application/vnd.apple.mpegurl" {
|
||||
NSLog("The player only support HLS streams right now")
|
||||
self.activeAudioTrack = AudioTrack(index: 0, startOffset: -1, duration: -1, title: "", contentUrl: "", mimeType: "")
|
||||
|
||||
super.init()
|
||||
return
|
||||
}
|
||||
self.activeAudioTrack = playbackSession.audioTracks[0]
|
||||
|
||||
super.init()
|
||||
|
||||
initAudioSession()
|
||||
setupRemoteTransportControls()
|
||||
NowPlayingInfo.setAudiobook(audiobook: audiobook)
|
||||
|
||||
// Listen to player events
|
||||
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext)
|
||||
@@ -47,14 +57,13 @@ class AudioPlayer: NSObject {
|
||||
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: .new, context: &playerItemContext)
|
||||
|
||||
self.audioPlayer.replaceCurrentItem(with: playerItem)
|
||||
seek(self.audiobook.startTime)
|
||||
|
||||
NSLog("Audioplayer ready")
|
||||
}
|
||||
deinit {
|
||||
destroy()
|
||||
}
|
||||
func destroy() {
|
||||
public func destroy() {
|
||||
pause()
|
||||
audioPlayer.replaceCurrentItem(with: nil)
|
||||
|
||||
@@ -65,9 +74,11 @@ class AudioPlayer: NSObject {
|
||||
print(error)
|
||||
}
|
||||
|
||||
DispatchQueue.main.sync {
|
||||
// DispatchQueue.main.sync {
|
||||
UIApplication.shared.endReceivingRemoteControlEvents()
|
||||
}
|
||||
// }
|
||||
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
@@ -152,10 +163,11 @@ class AudioPlayer: NSObject {
|
||||
// MARK: - Private
|
||||
private func createAsset() -> AVAsset {
|
||||
let headers: [String: String] = [
|
||||
"Authorization": "Bearer \(audiobook.token)"
|
||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||
]
|
||||
|
||||
return AVURLAsset(url: URL(string: audiobook.playlistUrl)!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
|
||||
debugPrint(activeAudioTrack)
|
||||
return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(activeAudioTrack.contentUrl)")!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
|
||||
}
|
||||
private func initAudioSession() {
|
||||
do {
|
||||
@@ -166,13 +178,12 @@ class AudioPlayer: NSObject {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Now playing
|
||||
func setupRemoteTransportControls() {
|
||||
DispatchQueue.main.sync {
|
||||
private func setupRemoteTransportControls() {
|
||||
// DispatchQueue.main.sync {
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
}
|
||||
// }
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
|
||||
commandCenter.playCommand.isEnabled = true
|
||||
@@ -228,8 +239,8 @@ class AudioPlayer: NSObject {
|
||||
return .success
|
||||
}
|
||||
}
|
||||
|
||||
func updateNowPlaying() {
|
||||
private func updateNowPlaying() {
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
||||
NowPlayingInfo.update(duration: getDuration(), currentTime: getCurrentTime(), rate: rate)
|
||||
}
|
||||
|
||||
@@ -240,18 +251,20 @@ class AudioPlayer: NSObject {
|
||||
guard let playerStatus = AVPlayerItem.Status(rawValue: (change?[.newKey] as? Int ?? -1)) else { return }
|
||||
|
||||
if playerStatus == .readyToPlay {
|
||||
updateNowPlaying()
|
||||
self.updateNowPlaying()
|
||||
|
||||
self.status = 0
|
||||
if self.playWhenReady {
|
||||
self.playWhenReady = false
|
||||
self.play()
|
||||
}
|
||||
|
||||
seek(playbackSession.currentTime)
|
||||
}
|
||||
}
|
||||
} else if context == &playerContext {
|
||||
if keyPath == #keyPath(AVPlayer.rate) {
|
||||
setPlaybackRate(change?[.newKey] as? Float ?? 1.0, observed: true)
|
||||
self.setPlaybackRate(change?[.newKey] as? Float ?? 1.0, observed: true)
|
||||
} else if keyPath == #keyPath(AVPlayer.currentItem) {
|
||||
NSLog("WARNING: Item ended")
|
||||
}
|
||||
@@ -260,4 +273,6 @@ class AudioPlayer: NSObject {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
public static var instance: AudioPlayer?
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// PlayerHandler.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 11.04.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class PlayerHandler {
|
||||
private static var player: AudioPlayer?
|
||||
private static var session: PlaybackSession?
|
||||
private static var timer: Timer?
|
||||
|
||||
private static var listeningTimePassedSinceLastSync = 0.0
|
||||
|
||||
public static func startPlayback(session: PlaybackSession, playWhenReady: Bool) {
|
||||
if player != nil {
|
||||
player?.destroy()
|
||||
player = nil
|
||||
}
|
||||
|
||||
NowPlayingInfo.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil))
|
||||
|
||||
self.session = session
|
||||
player = AudioPlayer(playbackSession: session, playWhenReady: playWhenReady)
|
||||
|
||||
// DispatchQueue.main.sync {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
self.tick()
|
||||
}
|
||||
// }
|
||||
}
|
||||
public static func stopPlayback() {
|
||||
player?.destroy()
|
||||
player = nil
|
||||
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
|
||||
NowPlayingInfo.reset()
|
||||
}
|
||||
|
||||
public static func getCurrentTime() -> Double? {
|
||||
self.player?.getCurrentTime()
|
||||
}
|
||||
public static func setPlaybackSpeed(speed: Float) {
|
||||
self.player?.setPlaybackRate(speed)
|
||||
}
|
||||
|
||||
public static func play() {
|
||||
self.player?.play()
|
||||
}
|
||||
public static func pause() {
|
||||
self.player?.play()
|
||||
}
|
||||
public static func playPause() {
|
||||
if paused() {
|
||||
self.player?.play()
|
||||
} else {
|
||||
self.player?.pause()
|
||||
}
|
||||
}
|
||||
|
||||
public static func seekForward(amount: Double) {
|
||||
if player == nil {
|
||||
return
|
||||
}
|
||||
|
||||
let destinationTime = player!.getCurrentTime() + amount
|
||||
player!.seek(destinationTime)
|
||||
}
|
||||
public static func seekBackward(amount: Double) {
|
||||
if player == nil {
|
||||
return
|
||||
}
|
||||
|
||||
let destinationTime = player!.getCurrentTime() - amount
|
||||
player!.seek(destinationTime)
|
||||
}
|
||||
public static func seek(amount: Double) {
|
||||
player?.seek(amount)
|
||||
}
|
||||
|
||||
public static func paused() -> Bool {
|
||||
player?.rate == 0.0
|
||||
}
|
||||
|
||||
public static func getMetdata() -> [String: Any] {
|
||||
DispatchQueue.main.async {
|
||||
syncProgress()
|
||||
}
|
||||
|
||||
return [
|
||||
"duration": player?.getDuration() ?? 0,
|
||||
"currentTime": player?.getCurrentTime() ?? 0,
|
||||
"playerState": !paused(),
|
||||
"currentRate": player?.rate ?? 0,
|
||||
]
|
||||
}
|
||||
|
||||
private static func tick() {
|
||||
if !paused() {
|
||||
listeningTimePassedSinceLastSync += 1
|
||||
}
|
||||
|
||||
if listeningTimePassedSinceLastSync > 3 {
|
||||
syncProgress()
|
||||
}
|
||||
}
|
||||
public static func syncProgress() {
|
||||
if player == nil || session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
let report = PlaybackReport(currentTime: player!.getCurrentTime(), duration: player!.getDuration(), timeListened: listeningTimePassedSinceLastSync)
|
||||
|
||||
session!.currentTime = player!.getCurrentTime()
|
||||
listeningTimePassedSinceLastSync = 0
|
||||
|
||||
// TODO: check if online
|
||||
NSLog("sending playback report")
|
||||
ApiClient.reportPlaybackProgress(report: report, sessionId: session!.id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// ApiClient.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 13.04.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
|
||||
class ApiClient {
|
||||
public static func postResource<T: Decodable>(endpoint: String, parameters: [String: String], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) {
|
||||
let headers: HTTPHeaders = [
|
||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||
]
|
||||
|
||||
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).responseDecodable(of: decodable) { response in
|
||||
switch response.result {
|
||||
case .success(let obj):
|
||||
callback?(obj)
|
||||
case .failure(let error):
|
||||
NSLog("api request to \(endpoint) failed")
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
public static func postResource(endpoint: String, parameters: [String: String], callback: ((_ success: Bool) -> Void)?) {
|
||||
let headers: HTTPHeaders = [
|
||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||
]
|
||||
|
||||
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).response { response in
|
||||
switch response.result {
|
||||
case .success(let _):
|
||||
callback?(true)
|
||||
case .failure(let error):
|
||||
NSLog("api request to \(endpoint) failed")
|
||||
print(error)
|
||||
|
||||
callback?(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func startPlaybackSession(libraryItemId: String, episodeId: String?, callback: @escaping (_ param: PlaybackSession) -> Void) {
|
||||
var endpoint = "api/items/\(libraryItemId)/play"
|
||||
if episodeId != nil {
|
||||
endpoint += "/\(episodeId!)"
|
||||
}
|
||||
|
||||
ApiClient.postResource(endpoint: endpoint, parameters: [
|
||||
"forceTranscode": "true", // TODO: direct play
|
||||
"mediaPlayer": "AVPlayer",
|
||||
], decodable: PlaybackSession.self) { obj in
|
||||
var session = obj
|
||||
|
||||
session.serverConnectionConfigId = Store.serverConfig!.id
|
||||
session.serverAddress = Store.serverConfig!.address
|
||||
|
||||
callback(session)
|
||||
}
|
||||
}
|
||||
public static func reportPlaybackProgress(report: PlaybackReport, sessionId: String) {
|
||||
try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// Database.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 11.04.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RealmSwift
|
||||
|
||||
class Database {
|
||||
// All DB releated actions must be executed on "realm-queue"
|
||||
public static let realmQueue = DispatchQueue(label: "realm-queue")
|
||||
private static var instance: Realm = try! Realm(queue: realmQueue)
|
||||
|
||||
public static func setServerConnectionConfig(config: ServerConnectionConfig) {
|
||||
var refrence: ThreadSafeReference<ServerConnectionConfig>?
|
||||
if config.realm != nil {
|
||||
refrence = ThreadSafeReference(to: config)
|
||||
}
|
||||
|
||||
realmQueue.sync {
|
||||
let existing: ServerConnectionConfig? = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: config.id)
|
||||
|
||||
if config.index == 0 {
|
||||
let lastConfig: ServerConnectionConfig? = instance.objects(ServerConnectionConfig.self).last
|
||||
|
||||
if lastConfig != nil {
|
||||
config.index = lastConfig!.index + 1
|
||||
} else {
|
||||
config.index = 1
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try instance.write {
|
||||
if existing != nil {
|
||||
instance.delete(existing!)
|
||||
}
|
||||
if refrence == nil {
|
||||
instance.add(config)
|
||||
} else {
|
||||
guard let resolved = instance.resolve(refrence!) else {
|
||||
throw "unable to resolve refrence"
|
||||
}
|
||||
|
||||
instance.add(resolved);
|
||||
}
|
||||
}
|
||||
} catch(let exception) {
|
||||
NSLog("failed to save server config")
|
||||
debugPrint(exception)
|
||||
}
|
||||
|
||||
setLastActiveConfigIndex(index: config.index)
|
||||
}
|
||||
}
|
||||
public static func deleteServerConnectionConfig(id: String) {
|
||||
realmQueue.sync {
|
||||
let config = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: id)
|
||||
|
||||
do {
|
||||
try instance.write {
|
||||
if config != nil {
|
||||
instance.delete(config!)
|
||||
}
|
||||
}
|
||||
} catch(let exception) {
|
||||
NSLog("failed to delete server config")
|
||||
debugPrint(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
public static func getServerConnectionConfigs() -> [ServerConnectionConfig] {
|
||||
var refrences: [ThreadSafeReference<ServerConnectionConfig>] = []
|
||||
|
||||
realmQueue.sync {
|
||||
let configs = instance.objects(ServerConnectionConfig.self)
|
||||
refrences = configs.map { config in
|
||||
return ThreadSafeReference(to: config)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let realm = try Realm()
|
||||
|
||||
return refrences.map { refrence in
|
||||
return realm.resolve(refrence)!
|
||||
}
|
||||
} catch(let exception) {
|
||||
NSLog("error while readling configs")
|
||||
debugPrint(exception)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public static func setLastActiveConfigIndexToNil() {
|
||||
realmQueue.sync {
|
||||
setLastActiveConfigIndex(index: nil)
|
||||
}
|
||||
}
|
||||
public static func setLastActiveConfigIndex(index: Int?) {
|
||||
let existing = instance.objects(ServerConnectionConfigActiveIndex.self)
|
||||
let obj = ServerConnectionConfigActiveIndex()
|
||||
obj.index = index
|
||||
|
||||
do {
|
||||
try instance.write {
|
||||
instance.delete(existing)
|
||||
instance.add(obj)
|
||||
}
|
||||
} catch(let exception) {
|
||||
NSLog("failed to save server config active index")
|
||||
debugPrint(exception)
|
||||
}
|
||||
}
|
||||
public static func getLastActiveConfigIndex() -> Int? {
|
||||
return realmQueue.sync {
|
||||
return instance.objects(ServerConnectionConfigActiveIndex.self).first?.index ?? nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Extensions.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 14.04.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String: Error {}
|
||||
|
||||
extension Encodable {
|
||||
func asDictionary() throws -> [String: Any] {
|
||||
let data = try JSONEncoder().encode(self)
|
||||
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
|
||||
throw NSError()
|
||||
}
|
||||
return dictionary
|
||||
}
|
||||
}
|
||||
@@ -16,19 +16,31 @@ func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
|
||||
}).resume()
|
||||
}
|
||||
|
||||
struct NowPlayingMetadata {
|
||||
var id: String
|
||||
var itemId: String
|
||||
var artworkUrl: String?
|
||||
var title: String
|
||||
var author: String?
|
||||
var series: String?
|
||||
}
|
||||
|
||||
class NowPlayingInfo {
|
||||
private static var nowPlayingInfo: [String: Any] = [:]
|
||||
private static var audiobook: Audiobook?
|
||||
|
||||
public static func setAudiobook(audiobook: Audiobook) {
|
||||
self.audiobook = audiobook
|
||||
setMetadata(nil)
|
||||
public static func setSessionMetadata(metadata: NowPlayingMetadata) {
|
||||
setMetadata(artwork: nil, metadata: metadata)
|
||||
|
||||
if !shouldFetchCover() || audiobook.artworkUrl == nil {
|
||||
/*
|
||||
if !shouldFetchCover(id: metadata.id) || metadata.artworkUrl == nil {
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(metadata.itemId)/cover?token=\(Store.serverConfig!.token)") else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = URL(string: audiobook.artworkUrl!) else { return }
|
||||
getData(from: url) { [self] image in
|
||||
guard let downloadedImage = image else {
|
||||
return
|
||||
@@ -37,7 +49,7 @@ class NowPlayingInfo {
|
||||
return downloadedImage
|
||||
})
|
||||
|
||||
self.setMetadata(artwork)
|
||||
self.setMetadata(artwork: artwork, metadata: metadata)
|
||||
}
|
||||
}
|
||||
public static func update(duration: Double, currentTime: Double, rate: Float) {
|
||||
@@ -49,31 +61,29 @@ class NowPlayingInfo {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||
}
|
||||
public static func reset() {
|
||||
audiobook = nil
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
||||
}
|
||||
|
||||
private static func setMetadata(_ artwork: MPMediaItemArtwork?) {
|
||||
if self.audiobook == nil {
|
||||
private static func setMetadata(artwork: MPMediaItemArtwork?, metadata: NowPlayingMetadata?) {
|
||||
if metadata == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if artwork != nil {
|
||||
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
|
||||
} else if shouldFetchCover() {
|
||||
} else if shouldFetchCover(id: metadata!.id) {
|
||||
nowPlayingInfo[MPMediaItemPropertyArtwork] = nil
|
||||
}
|
||||
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] = audiobook!.streamId
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = URL(string: audiobook!.playlistUrl)
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] = metadata!.id
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = "hls"
|
||||
|
||||
nowPlayingInfo[MPMediaItemPropertyTitle] = audiobook!.title
|
||||
nowPlayingInfo[MPMediaItemPropertyArtist] = audiobook!.author ?? "unknown"
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = audiobook!.series
|
||||
nowPlayingInfo[MPMediaItemPropertyTitle] = metadata!.title
|
||||
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
|
||||
}
|
||||
private static func shouldFetchCover() -> Bool {
|
||||
audiobook != nil && (nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != audiobook!.streamId || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil)
|
||||
private static func shouldFetchCover(id: String) -> Bool {
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != id || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// PlayerEvents.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 14.04.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum PlayerEvents: String {
|
||||
case update = "com.audiobookshelf.app.player.update"
|
||||
case closed = "com.audiobookshelf.app.player.closed"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// Store.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 11.04.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RealmSwift
|
||||
|
||||
class Store {
|
||||
@ThreadSafe private static var _serverConfig: ServerConnectionConfig?
|
||||
public static var serverConfig: ServerConnectionConfig? {
|
||||
get {
|
||||
return _serverConfig
|
||||
}
|
||||
set(updated) {
|
||||
if updated != nil {
|
||||
Database.setServerConnectionConfig(config: updated!)
|
||||
} else {
|
||||
Database.setLastActiveConfigIndexToNil()
|
||||
}
|
||||
|
||||
Database.realmQueue.sync {
|
||||
_serverConfig = updated
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate",
|
||||
"icons-android": "cordova-res android --skip-config --copy"
|
||||
"icons-android": "cordova-res android --skip-config --copy",
|
||||
"ionic:build": "npm run build",
|
||||
"ionic:serve": "npm run start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^3.2.2",
|
||||
@@ -37,4 +39,4 @@
|
||||
"@nuxtjs/tailwindcss": "^4.2.0",
|
||||
"postcss": "^8.3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ const isWeb = Capacitor.getPlatform() == 'web'
|
||||
class DbService {
|
||||
constructor() { }
|
||||
|
||||
// Please dont use this, it is not implemented in ios (maybe key: primary value: any ?)
|
||||
save(db, key, value) {
|
||||
if (isWeb) return
|
||||
return AbsDatabase.saveFromWebview({ db, key, value }).then(() => {
|
||||
@@ -15,6 +16,7 @@ class DbService {
|
||||
})
|
||||
}
|
||||
|
||||
// Please dont use this, it is not implemented in ios
|
||||
load(db, key) {
|
||||
if (isWeb) return null
|
||||
return AbsDatabase.loadFromWebview({ db, key }).then((data) => {
|
||||
|
||||