@capgo/capacitor-media-session 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapgoMediaSession.podspec +17 -0
- package/Package.swift +28 -0
- package/README.md +164 -0
- package/android/build.gradle +60 -0
- package/android/src/main/AndroidManifest.xml +26 -0
- package/android/src/main/java/com/capgo/mediasession/MediaSessionCallback.java +55 -0
- package/android/src/main/java/com/capgo/mediasession/MediaSessionPlugin.java +277 -0
- package/android/src/main/java/com/capgo/mediasession/MediaSessionService.java +366 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/android/src/main/res/drawable/ic_baseline_forward_30_24.xml +16 -0
- package/android/src/main/res/drawable/ic_baseline_pause_24.xml +10 -0
- package/android/src/main/res/drawable/ic_baseline_play_arrow_24.xml +10 -0
- package/android/src/main/res/drawable/ic_baseline_replay_30_24.xml +16 -0
- package/android/src/main/res/drawable/ic_baseline_skip_next_24.xml +10 -0
- package/android/src/main/res/drawable/ic_baseline_skip_previous_24.xml +10 -0
- package/android/src/main/res/drawable/ic_baseline_stop_24.xml +10 -0
- package/android/src/main/res/drawable/ic_baseline_volume_up_24.xml +10 -0
- package/dist/docs.json +326 -0
- package/dist/esm/definitions.d.ts +48 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +8 -0
- package/dist/esm/web.js +38 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +53 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +56 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/MediaSessionPlugin/MediaSessionPlugin.swift +9 -0
- package/ios/Tests/MediaSessionPluginTests/MediaSessionPluginTests.swift +15 -0
- package/package.json +83 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'CapgoMediaSession'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.homepage = package['repository']['url']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
|
|
13
|
+
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
|
+
s.ios.deployment_target = '14.0'
|
|
15
|
+
s.dependency 'Capacitor'
|
|
16
|
+
s.swift_version = '5.1'
|
|
17
|
+
end
|
package/Package.swift
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "CapgoMediaSession",
|
|
6
|
+
platforms: [.iOS(.v14)],
|
|
7
|
+
products: [
|
|
8
|
+
.library(
|
|
9
|
+
name: "CapgoMediaSession",
|
|
10
|
+
targets: ["MediaSessionPlugin"])
|
|
11
|
+
],
|
|
12
|
+
dependencies: [
|
|
13
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0")
|
|
14
|
+
],
|
|
15
|
+
targets: [
|
|
16
|
+
.target(
|
|
17
|
+
name: "MediaSessionPlugin",
|
|
18
|
+
dependencies: [
|
|
19
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
20
|
+
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
21
|
+
],
|
|
22
|
+
path: "ios/Sources/MediaSessionPlugin"),
|
|
23
|
+
.testTarget(
|
|
24
|
+
name: "MediaSessionPluginTests",
|
|
25
|
+
dependencies: ["MediaSessionPlugin"],
|
|
26
|
+
path: "ios/Tests/MediaSessionPluginTests")
|
|
27
|
+
]
|
|
28
|
+
)
|
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @capgo/capacitor-media-session
|
|
2
|
+
<a href="https://capgo.app/"><img src='https://raw.githubusercontent.com/Cap-go/capgo/main/assets/capgo_banner.png' alt='Capgo - Instant updates for capacitor'/></a>
|
|
3
|
+
|
|
4
|
+
<div align="center">
|
|
5
|
+
<h2><a href="https://capgo.app/?ref=plugin"> ➡️ Get Instant updates for your App with Capgo</a></h2>
|
|
6
|
+
<h2><a href="https://capgo.app/consulting/?ref=plugin"> Missing a feature? We’ll build the plugin for you 💪</a></h2>
|
|
7
|
+
</div>
|
|
8
|
+
Expose media session controls for Capacitor apps
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @capgo/capacitor-media-session
|
|
14
|
+
npx cap sync
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
|
|
19
|
+
<docgen-index>
|
|
20
|
+
|
|
21
|
+
* [`setMetadata(...)`](#setmetadata)
|
|
22
|
+
* [`setPlaybackState(...)`](#setplaybackstate)
|
|
23
|
+
* [`setActionHandler(...)`](#setactionhandler)
|
|
24
|
+
* [`setPositionState(...)`](#setpositionstate)
|
|
25
|
+
* [Interfaces](#interfaces)
|
|
26
|
+
* [Type Aliases](#type-aliases)
|
|
27
|
+
|
|
28
|
+
</docgen-index>
|
|
29
|
+
|
|
30
|
+
<docgen-api>
|
|
31
|
+
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
32
|
+
|
|
33
|
+
### setMetadata(...)
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
setMetadata(options: MetadataOptions) => Promise<void>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Sets metadata of the currently playing media.
|
|
40
|
+
|
|
41
|
+
| Param | Type |
|
|
42
|
+
| ------------- | ----------------------------------------------------------- |
|
|
43
|
+
| **`options`** | <code><a href="#metadataoptions">MetadataOptions</a></code> |
|
|
44
|
+
|
|
45
|
+
--------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### setPlaybackState(...)
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
setPlaybackState(options: PlaybackStateOptions) => Promise<void>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Updates the playback state of the media session.
|
|
55
|
+
|
|
56
|
+
| Param | Type |
|
|
57
|
+
| ------------- | --------------------------------------------------------------------- |
|
|
58
|
+
| **`options`** | <code><a href="#playbackstateoptions">PlaybackStateOptions</a></code> |
|
|
59
|
+
|
|
60
|
+
--------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### setActionHandler(...)
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
setActionHandler(options: ActionHandlerOptions, handler: ActionHandler | null) => Promise<void>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Registers a handler for a media session action.
|
|
70
|
+
|
|
71
|
+
| Param | Type |
|
|
72
|
+
| ------------- | --------------------------------------------------------------------- |
|
|
73
|
+
| **`options`** | <code><a href="#actionhandleroptions">ActionHandlerOptions</a></code> |
|
|
74
|
+
| **`handler`** | <code><a href="#actionhandler">ActionHandler</a> \| null</code> |
|
|
75
|
+
|
|
76
|
+
--------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
### setPositionState(...)
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
setPositionState(options: PositionStateOptions) => Promise<void>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Updates position state for the active media session.
|
|
86
|
+
|
|
87
|
+
| Param | Type |
|
|
88
|
+
| ------------- | --------------------------------------------------------------------- |
|
|
89
|
+
| **`options`** | <code><a href="#positionstateoptions">PositionStateOptions</a></code> |
|
|
90
|
+
|
|
91
|
+
--------------------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
### Interfaces
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
#### MetadataOptions
|
|
98
|
+
|
|
99
|
+
| Prop | Type |
|
|
100
|
+
| ------------- | ------------------------- |
|
|
101
|
+
| **`album`** | <code>string</code> |
|
|
102
|
+
| **`artist`** | <code>string</code> |
|
|
103
|
+
| **`artwork`** | <code>MediaImage[]</code> |
|
|
104
|
+
| **`title`** | <code>string</code> |
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
#### MediaImage
|
|
108
|
+
|
|
109
|
+
| Prop | Type |
|
|
110
|
+
| ----------- | ------------------- |
|
|
111
|
+
| **`src`** | <code>string</code> |
|
|
112
|
+
| **`sizes`** | <code>string</code> |
|
|
113
|
+
| **`type`** | <code>string</code> |
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
#### PlaybackStateOptions
|
|
117
|
+
|
|
118
|
+
| Prop | Type |
|
|
119
|
+
| ------------------- | ------------------------------------------------------------------------------- |
|
|
120
|
+
| **`playbackState`** | <code><a href="#mediasessionplaybackstate">MediaSessionPlaybackState</a></code> |
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
#### ActionHandlerOptions
|
|
124
|
+
|
|
125
|
+
| Prop | Type |
|
|
126
|
+
| ------------ | ----------------------------------------------------------------- |
|
|
127
|
+
| **`action`** | <code><a href="#mediasessionaction">MediaSessionAction</a></code> |
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
#### ActionDetails
|
|
131
|
+
|
|
132
|
+
| Prop | Type |
|
|
133
|
+
| -------------- | ----------------------------------------------------------------- |
|
|
134
|
+
| **`action`** | <code><a href="#mediasessionaction">MediaSessionAction</a></code> |
|
|
135
|
+
| **`seekTime`** | <code>number \| null</code> |
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
#### PositionStateOptions
|
|
139
|
+
|
|
140
|
+
| Prop | Type |
|
|
141
|
+
| ------------------ | ------------------- |
|
|
142
|
+
| **`duration`** | <code>number</code> |
|
|
143
|
+
| **`playbackRate`** | <code>number</code> |
|
|
144
|
+
| **`position`** | <code>number</code> |
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
### Type Aliases
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
#### MediaSessionPlaybackState
|
|
151
|
+
|
|
152
|
+
<code>'none' | 'paused' | 'playing'</code>
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
#### MediaSessionAction
|
|
156
|
+
|
|
157
|
+
<code>'play' | 'pause' | 'seekbackward' | 'seekforward' | 'previoustrack' | 'nexttrack' | 'seekto' | 'stop'</code>
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
#### ActionHandler
|
|
161
|
+
|
|
162
|
+
<code>(details: <a href="#actiondetails">ActionDetails</a>): void</code>
|
|
163
|
+
|
|
164
|
+
</docgen-api>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
ext {
|
|
2
|
+
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
+
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
|
|
4
|
+
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
|
|
5
|
+
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
|
|
6
|
+
androidxMediaVersion = project.hasProperty('androidxMediaVersion') ? rootProject.ext.androidxMediaVersion : '1.7.0'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
buildscript {
|
|
10
|
+
repositories {
|
|
11
|
+
google()
|
|
12
|
+
mavenCentral()
|
|
13
|
+
}
|
|
14
|
+
dependencies {
|
|
15
|
+
classpath 'com.android.tools.build:gradle:8.7.2'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
apply plugin: 'com.android.library'
|
|
20
|
+
|
|
21
|
+
android {
|
|
22
|
+
namespace "com.capgo.mediasession"
|
|
23
|
+
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
|
|
24
|
+
defaultConfig {
|
|
25
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
|
26
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
|
|
27
|
+
versionCode 1
|
|
28
|
+
versionName "1.0"
|
|
29
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
30
|
+
}
|
|
31
|
+
buildTypes {
|
|
32
|
+
release {
|
|
33
|
+
minifyEnabled false
|
|
34
|
+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
lintOptions {
|
|
38
|
+
abortOnError false
|
|
39
|
+
}
|
|
40
|
+
compileOptions {
|
|
41
|
+
sourceCompatibility JavaVersion.VERSION_21
|
|
42
|
+
targetCompatibility JavaVersion.VERSION_21
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
repositories {
|
|
47
|
+
google()
|
|
48
|
+
mavenCentral()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
dependencies {
|
|
53
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
54
|
+
implementation project(':capacitor-android')
|
|
55
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
56
|
+
implementation "androidx.media:media:$androidxMediaVersion"
|
|
57
|
+
testImplementation "junit:junit:$junitVersion"
|
|
58
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
59
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
|
|
4
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
5
|
+
|
|
6
|
+
<application>
|
|
7
|
+
<service
|
|
8
|
+
android:name=".MediaSessionService"
|
|
9
|
+
android:enabled="true"
|
|
10
|
+
android:exported="true"
|
|
11
|
+
android:foregroundServiceType="mediaPlayback">
|
|
12
|
+
<intent-filter>
|
|
13
|
+
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
|
14
|
+
</intent-filter>
|
|
15
|
+
</service>
|
|
16
|
+
|
|
17
|
+
<receiver
|
|
18
|
+
android:name="androidx.media.session.MediaButtonReceiver"
|
|
19
|
+
android:exported="true">
|
|
20
|
+
<intent-filter>
|
|
21
|
+
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
|
22
|
+
</intent-filter>
|
|
23
|
+
</receiver>
|
|
24
|
+
</application>
|
|
25
|
+
|
|
26
|
+
</manifest>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.capgo.mediasession;
|
|
2
|
+
|
|
3
|
+
import android.support.v4.media.session.MediaSessionCompat;
|
|
4
|
+
import com.getcapacitor.JSObject;
|
|
5
|
+
|
|
6
|
+
public class MediaSessionCallback extends MediaSessionCompat.Callback {
|
|
7
|
+
|
|
8
|
+
private final MediaSessionPlugin plugin;
|
|
9
|
+
|
|
10
|
+
MediaSessionCallback(MediaSessionPlugin plugin) {
|
|
11
|
+
this.plugin = plugin;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Override
|
|
15
|
+
public void onPlay() {
|
|
16
|
+
plugin.actionCallback("play");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Override
|
|
20
|
+
public void onPause() {
|
|
21
|
+
plugin.actionCallback("pause");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Override
|
|
25
|
+
public void onSeekTo(long pos) {
|
|
26
|
+
JSObject data = new JSObject();
|
|
27
|
+
data.put("seekTime", (double) pos / 1000.0);
|
|
28
|
+
plugin.actionCallback("seekto", data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Override
|
|
32
|
+
public void onRewind() {
|
|
33
|
+
plugin.actionCallback("seekbackward");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Override
|
|
37
|
+
public void onFastForward() {
|
|
38
|
+
plugin.actionCallback("seekforward");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Override
|
|
42
|
+
public void onSkipToPrevious() {
|
|
43
|
+
plugin.actionCallback("previoustrack");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Override
|
|
47
|
+
public void onSkipToNext() {
|
|
48
|
+
plugin.actionCallback("nexttrack");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Override
|
|
52
|
+
public void onStop() {
|
|
53
|
+
plugin.actionCallback("stop");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
package com.capgo.mediasession;
|
|
2
|
+
|
|
3
|
+
import android.content.ComponentName;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.content.Intent;
|
|
6
|
+
import android.content.ServiceConnection;
|
|
7
|
+
import android.graphics.Bitmap;
|
|
8
|
+
import android.graphics.BitmapFactory;
|
|
9
|
+
import android.os.IBinder;
|
|
10
|
+
import android.support.v4.media.session.PlaybackStateCompat;
|
|
11
|
+
import android.util.Base64;
|
|
12
|
+
import android.util.Log;
|
|
13
|
+
import androidx.core.content.ContextCompat;
|
|
14
|
+
import com.getcapacitor.JSArray;
|
|
15
|
+
import com.getcapacitor.JSObject;
|
|
16
|
+
import com.getcapacitor.Plugin;
|
|
17
|
+
import com.getcapacitor.PluginCall;
|
|
18
|
+
import com.getcapacitor.PluginMethod;
|
|
19
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
20
|
+
import java.io.IOException;
|
|
21
|
+
import java.io.InputStream;
|
|
22
|
+
import java.net.HttpURLConnection;
|
|
23
|
+
import java.net.URL;
|
|
24
|
+
import java.util.HashMap;
|
|
25
|
+
import java.util.List;
|
|
26
|
+
import java.util.Map;
|
|
27
|
+
import org.json.JSONException;
|
|
28
|
+
import org.json.JSONObject;
|
|
29
|
+
|
|
30
|
+
@CapacitorPlugin(name = "MediaSession")
|
|
31
|
+
public class MediaSessionPlugin extends Plugin {
|
|
32
|
+
|
|
33
|
+
private static final String TAG = "CapgoMediaSession";
|
|
34
|
+
|
|
35
|
+
private boolean startServiceOnlyDuringPlayback = true;
|
|
36
|
+
|
|
37
|
+
private String title = "";
|
|
38
|
+
private String artist = "";
|
|
39
|
+
private String album = "";
|
|
40
|
+
private Bitmap artwork;
|
|
41
|
+
private String playbackState = "none";
|
|
42
|
+
private double duration = 0.0;
|
|
43
|
+
private double position = 0.0;
|
|
44
|
+
private double playbackRate = 1.0;
|
|
45
|
+
|
|
46
|
+
private final Map<String, PluginCall> actionHandlers = new HashMap<>();
|
|
47
|
+
|
|
48
|
+
private MediaSessionService service;
|
|
49
|
+
|
|
50
|
+
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
|
51
|
+
@Override
|
|
52
|
+
public void onServiceConnected(ComponentName componentName, IBinder binder) {
|
|
53
|
+
MediaSessionService.LocalBinder localBinder = (MediaSessionService.LocalBinder) binder;
|
|
54
|
+
service = localBinder.getService();
|
|
55
|
+
if (getActivity() == null) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
Intent launchIntent = new Intent(getActivity(), getActivity().getClass());
|
|
59
|
+
service.connectAndInitialize(MediaSessionPlugin.this, launchIntent);
|
|
60
|
+
updateServiceMetadata();
|
|
61
|
+
updateServicePlaybackState();
|
|
62
|
+
updateServicePositionState();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Override
|
|
66
|
+
public void onServiceDisconnected(ComponentName componentName) {
|
|
67
|
+
Log.d(TAG, "Disconnected from MediaSessionService");
|
|
68
|
+
service = null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
@Override
|
|
73
|
+
public void load() {
|
|
74
|
+
super.load();
|
|
75
|
+
|
|
76
|
+
String foregroundServiceConfig = getConfig().getString("foregroundService", "");
|
|
77
|
+
if ("always".equals(foregroundServiceConfig)) {
|
|
78
|
+
startServiceOnlyDuringPlayback = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!startServiceOnlyDuringPlayback) {
|
|
82
|
+
startMediaService();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Override
|
|
87
|
+
protected void handleAppStop() {
|
|
88
|
+
super.handleAppStop();
|
|
89
|
+
|
|
90
|
+
if (startServiceOnlyDuringPlayback && service != null && !isPlaybackActive()) {
|
|
91
|
+
stopMediaService();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private void startMediaService() {
|
|
96
|
+
Context context = getContext();
|
|
97
|
+
if (context == null || getActivity() == null) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Intent intent = new Intent(context, MediaSessionService.class);
|
|
102
|
+
ContextCompat.startForegroundService(context, intent);
|
|
103
|
+
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private void stopMediaService() {
|
|
107
|
+
Context context = getContext();
|
|
108
|
+
if (context == null || service == null) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
context.unbindService(serviceConnection);
|
|
114
|
+
} catch (IllegalArgumentException ex) {
|
|
115
|
+
Log.w(TAG, "Service already unbound", ex);
|
|
116
|
+
}
|
|
117
|
+
service = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private boolean isPlaybackActive() {
|
|
121
|
+
return "playing".equals(playbackState) || "paused".equals(playbackState);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private void updateServiceMetadata() {
|
|
125
|
+
if (service == null) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
service.setTitle(title);
|
|
129
|
+
service.setArtist(artist);
|
|
130
|
+
service.setAlbum(album);
|
|
131
|
+
service.setArtwork(artwork);
|
|
132
|
+
service.update();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private void updateServicePlaybackState() {
|
|
136
|
+
if (service == null) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if ("playing".equals(playbackState)) {
|
|
141
|
+
service.setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
|
|
142
|
+
} else if ("paused".equals(playbackState)) {
|
|
143
|
+
service.setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
|
|
144
|
+
} else {
|
|
145
|
+
service.setPlaybackState(PlaybackStateCompat.STATE_NONE);
|
|
146
|
+
}
|
|
147
|
+
service.update();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private void updateServicePositionState() {
|
|
151
|
+
if (service == null) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
service.setDuration(Math.round(duration * 1000));
|
|
156
|
+
service.setPosition(Math.round(position * 1000));
|
|
157
|
+
float playbackSpeed = playbackRate == 0.0 ? 1.0F : (float) playbackRate;
|
|
158
|
+
service.setPlaybackSpeed(playbackSpeed);
|
|
159
|
+
service.update();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private Bitmap urlToBitmap(String url) throws IOException {
|
|
163
|
+
if (url == null || url.isEmpty()) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
boolean blobUrl = url.startsWith("blob:");
|
|
168
|
+
if (blobUrl) {
|
|
169
|
+
Log.i(TAG, "Blob URLs are not supported for media artwork");
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
boolean httpUrl = url.startsWith("http");
|
|
174
|
+
if (httpUrl) {
|
|
175
|
+
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
|
176
|
+
connection.setDoInput(true);
|
|
177
|
+
connection.connect();
|
|
178
|
+
try (InputStream inputStream = connection.getInputStream()) {
|
|
179
|
+
return BitmapFactory.decodeStream(inputStream);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
int base64Index = url.indexOf(";base64,");
|
|
184
|
+
if (base64Index != -1) {
|
|
185
|
+
String base64Data = url.substring(base64Index + 8);
|
|
186
|
+
byte[] decoded = Base64.decode(base64Data, Base64.DEFAULT);
|
|
187
|
+
return BitmapFactory.decodeByteArray(decoded, 0, decoded.length);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@PluginMethod
|
|
194
|
+
public void setMetadata(PluginCall call) {
|
|
195
|
+
title = call.getString("title", title);
|
|
196
|
+
artist = call.getString("artist", artist);
|
|
197
|
+
album = call.getString("album", album);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
JSArray artworkArray = call.getArray("artwork");
|
|
201
|
+
if (artworkArray != null) {
|
|
202
|
+
List<JSONObject> artworkList = artworkArray.toList();
|
|
203
|
+
for (JSONObject artworkJson : artworkList) {
|
|
204
|
+
String src = artworkJson.optString("src", null);
|
|
205
|
+
if (src != null) {
|
|
206
|
+
artwork = urlToBitmap(src);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch (JSONException | IOException ex) {
|
|
212
|
+
Log.w(TAG, "Unable to parse artwork", ex);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
updateServiceMetadata();
|
|
216
|
+
call.resolve();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@PluginMethod
|
|
220
|
+
public void setPlaybackState(PluginCall call) {
|
|
221
|
+
playbackState = call.getString("playbackState", playbackState);
|
|
222
|
+
|
|
223
|
+
boolean playbackActive = isPlaybackActive();
|
|
224
|
+
if (startServiceOnlyDuringPlayback && service == null && playbackActive) {
|
|
225
|
+
startMediaService();
|
|
226
|
+
} else if (startServiceOnlyDuringPlayback && service != null && !playbackActive) {
|
|
227
|
+
stopMediaService();
|
|
228
|
+
} else {
|
|
229
|
+
updateServicePlaybackState();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
call.resolve();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@PluginMethod
|
|
236
|
+
public void setPositionState(PluginCall call) {
|
|
237
|
+
duration = call.getDouble("duration", duration);
|
|
238
|
+
position = call.getDouble("position", position);
|
|
239
|
+
playbackRate = call.getFloat("playbackRate", (float) playbackRate);
|
|
240
|
+
|
|
241
|
+
updateServicePositionState();
|
|
242
|
+
call.resolve();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@PluginMethod(returnType = PluginMethod.RETURN_CALLBACK)
|
|
246
|
+
public void setActionHandler(PluginCall call) {
|
|
247
|
+
call.setKeepAlive(true);
|
|
248
|
+
String action = call.getString("action");
|
|
249
|
+
if (action != null) {
|
|
250
|
+
actionHandlers.put(action, call);
|
|
251
|
+
if (service != null) {
|
|
252
|
+
service.updatePossibleActions();
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
call.resolve();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public boolean hasActionHandler(String action) {
|
|
260
|
+
PluginCall handler = actionHandlers.get(action);
|
|
261
|
+
return handler != null && !PluginCall.CALLBACK_ID_DANGLING.equals(handler.getCallbackId());
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public void actionCallback(String action) {
|
|
265
|
+
actionCallback(action, new JSObject());
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
public void actionCallback(String action, JSObject data) {
|
|
269
|
+
PluginCall handler = actionHandlers.get(action);
|
|
270
|
+
if (handler != null && !PluginCall.CALLBACK_ID_DANGLING.equals(handler.getCallbackId())) {
|
|
271
|
+
data.put("action", action);
|
|
272
|
+
handler.resolve(data);
|
|
273
|
+
} else {
|
|
274
|
+
Log.d(TAG, "No handler for action " + action);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|