react-native-media-notification 0.2.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/LICENSE +20 -0
- package/MediaControls.podspec +30 -0
- package/README.md +237 -0
- package/android/build.gradle +89 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +29 -0
- package/android/src/main/java/com/mediacontrols/AudioFocusListener.kt +79 -0
- package/android/src/main/java/com/mediacontrols/Controls.kt +22 -0
- package/android/src/main/java/com/mediacontrols/CustomCommandButton.kt +72 -0
- package/android/src/main/java/com/mediacontrols/MediaControlsModule.kt +188 -0
- package/android/src/main/java/com/mediacontrols/MediaControlsPackage.kt +36 -0
- package/android/src/main/java/com/mediacontrols/MediaControlsPlayer.kt +321 -0
- package/android/src/main/java/com/mediacontrols/MediaControlsService.kt +233 -0
- package/android/src/main/java/com/mediacontrols/MediaNotificationProvider.kt +74 -0
- package/ios/MediaControls.h +5 -0
- package/ios/MediaControls.mm +300 -0
- package/lib/module/NativeMediaControls.js +7 -0
- package/lib/module/NativeMediaControls.js.map +1 -0
- package/lib/module/index.js +75 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeMediaControls.d.ts +31 -0
- package/lib/typescript/src/NativeMediaControls.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +34 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +169 -0
- package/src/NativeMediaControls.ts +54 -0
- package/src/index.tsx +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Marius Butz
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
|
@@ -0,0 +1,30 @@
|
|
|
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 = "MediaControls"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/mbpictures/react-native-media-notification.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
install_modules_dependencies(s)
|
|
21
|
+
|
|
22
|
+
# iOS frameworks
|
|
23
|
+
s.frameworks = 'MediaPlayer', 'AVFoundation'
|
|
24
|
+
|
|
25
|
+
s.pod_target_xcconfig = {
|
|
26
|
+
'DEFINES_MODULE' => 'YES'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
install_modules_dependencies(s)
|
|
30
|
+
end
|
package/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
Welcome to react-native-media-notification👋<br />
|
|
3
|
+
</h1>
|
|
4
|
+
<h3 align="center">
|
|
5
|
+
Your React Native library for media notifications and controls
|
|
6
|
+
</h3>
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="LICENSE" target="_blank">
|
|
9
|
+
<img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge" />
|
|
10
|
+
</a>
|
|
11
|
+
<img alt="Build Status" src="https://img.shields.io/github/actions/workflow/status/mbpictures/react-native-media-notification/ci.yml?style=for-the-badge" />
|
|
12
|
+
<a href="https://badge.fury.io/js/react-native-media-notification">
|
|
13
|
+
<img src="https://img.shields.io/npm/v/react-native-media-notification?style=for-the-badge" alt="npm version">
|
|
14
|
+
</a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
> A react native package for media notifications and controls, using AndroidX Media3 for Android and the ControlCenter API for iOS.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- 🎵 Media Notifications with Play/Pause/Stop Controls
|
|
22
|
+
- ⏭️ Skip Forward/Backward Support
|
|
23
|
+
- 🎨 Album Artwork Support (URL-based)
|
|
24
|
+
- 🔊 Audio Interruption Handling
|
|
25
|
+
- 📱 iOS Control Center Integration
|
|
26
|
+
- 🤖 Android Media3 Session Support
|
|
27
|
+
- 🎯 TypeScript Support
|
|
28
|
+
- ⚡ New Architecture (Turbo Modules) Ready
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install react-native-media-notification
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
yarn add react-native-media-notification
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### iOS
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd ios && pod install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Android
|
|
47
|
+
|
|
48
|
+
#### Android Auto
|
|
49
|
+
To enable Android Auto support, you need to add the following in the application tag of your `android/app/src/main/AndroidManifest.xml`:
|
|
50
|
+
|
|
51
|
+
```xml
|
|
52
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
53
|
+
package="com.yourapp">
|
|
54
|
+
<!--...-->
|
|
55
|
+
<application>
|
|
56
|
+
<!--...-->
|
|
57
|
+
<meta-data
|
|
58
|
+
android:name="com.google.android.gms.car.application"
|
|
59
|
+
android:resource="@xml/automotive_app_desc" />
|
|
60
|
+
</application>
|
|
61
|
+
</manifest>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
And create the file `android/app/src/main/res/xml/automotive_app_desc.xml` with the following content:
|
|
65
|
+
|
|
66
|
+
```xml
|
|
67
|
+
<automotiveApp>
|
|
68
|
+
<uses name="media"/>
|
|
69
|
+
</automotiveApp>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Customize Appearance
|
|
73
|
+
|
|
74
|
+
If you want to customize the small media notification icon on Android, you can add the following to your `android/app/src/main/res/values/styles.xml`:
|
|
75
|
+
|
|
76
|
+
```xml
|
|
77
|
+
<resources>
|
|
78
|
+
<!--...-->
|
|
79
|
+
<drawable name="media3_notification_small_icon">@drawable/my_custom_icon</drawable>
|
|
80
|
+
</resources>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Verwendung
|
|
84
|
+
|
|
85
|
+
### Basic Setup
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import * as MediaControls from 'react-native-media-notification';
|
|
89
|
+
|
|
90
|
+
// register event listeners
|
|
91
|
+
const removePlayListener = MediaControls.addEventListener('play', () => {
|
|
92
|
+
console.log('Play button pressed');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const removePauseListener = MediaControls.addEventListener('pause', () => {
|
|
96
|
+
console.log('Pause button pressed');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Cleanup
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
return () => {
|
|
102
|
+
removePlayListener.remove();
|
|
103
|
+
removePauseListener.remove();
|
|
104
|
+
};
|
|
105
|
+
}, []);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Update Metadata or Create Media Notification
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
await MediaControls.updateMetadata({
|
|
112
|
+
title: 'Song Title',
|
|
113
|
+
artist: 'Artist Name',
|
|
114
|
+
album: 'Album Name',
|
|
115
|
+
duration: 240, // in seconds
|
|
116
|
+
position: 30, // current position in seconds
|
|
117
|
+
isPlaying: true,
|
|
118
|
+
artwork: 'https://example.com/artwork.jpg',
|
|
119
|
+
shuffle: false, // optional, default is false
|
|
120
|
+
repeat: 'one', // optional, default is 'off', can be 'all', 'one', or 'off'
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### All available Events
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Playback Controls
|
|
128
|
+
MediaControls.addEventListener('play', () => {});
|
|
129
|
+
MediaControls.addEventListener('pause', () => {});
|
|
130
|
+
MediaControls.addEventListener('stop', () => {});
|
|
131
|
+
MediaControls.addEventListener('shuffle', () => {});
|
|
132
|
+
MediaControls.addEventListener('repeatMode', () => {});
|
|
133
|
+
|
|
134
|
+
// Navigation
|
|
135
|
+
MediaControls.addEventListener('skipToNext', () => {});
|
|
136
|
+
MediaControls.addEventListener('skipToPrevious', () => {});
|
|
137
|
+
|
|
138
|
+
// Seeking
|
|
139
|
+
MediaControls.addEventListener('seekForward', () => {});
|
|
140
|
+
MediaControls.addEventListener('seekBackward', () => {});
|
|
141
|
+
MediaControls.addEventListener('seek', (data) => {
|
|
142
|
+
console.log('Seek to position:', data?.position);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Interruptions
|
|
146
|
+
MediaControls.addEventListener('duck', () => {}); // reduce volume for interruption
|
|
147
|
+
MediaControls.addEventListener('unduck', () => {}); // restore volume after interruption
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Stop Media Notification
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
await MediaControls.stopMediaNotification();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## API Reference
|
|
157
|
+
|
|
158
|
+
### Functions
|
|
159
|
+
|
|
160
|
+
#### `updateMetadata(metadata: MediaTrackMetadata): Promise<void>`
|
|
161
|
+
|
|
162
|
+
Updates the media track metadata for the notification. When called for the first time, it creates the media notification.
|
|
163
|
+
|
|
164
|
+
**Parameter:**
|
|
165
|
+
- `metadata`: Object with media track information, including title, artist, album, duration, artwork URL, current position, and playback state.
|
|
166
|
+
|
|
167
|
+
#### `stopMediaNotification(): Promise<void>`
|
|
168
|
+
|
|
169
|
+
Stops media notification and removes audio focus (if enabled)
|
|
170
|
+
|
|
171
|
+
#### `enableAudioInterruption(enabled: boolean): Promise<void>`
|
|
172
|
+
|
|
173
|
+
Enable or disable audio interruption handling. When enabled, the media controls will respond to audio interruptions (like incoming calls) by pausing playback and resuming when the interruption ends.
|
|
174
|
+
|
|
175
|
+
#### `enableBackgroundMode(enabled: boolean): Promise<void>`
|
|
176
|
+
|
|
177
|
+
**iOS ONLY**. Enable or disable background mode for iOS.
|
|
178
|
+
|
|
179
|
+
#### `addEventListener(event: MediaControlEvent, handler: Function): EventSubscription`
|
|
180
|
+
|
|
181
|
+
Registers an event listener for a specific media control event. Returns a function to remove the listener.
|
|
182
|
+
|
|
183
|
+
#### `removeAllListeners(event?: MediaControlEvent): void`
|
|
184
|
+
|
|
185
|
+
Entfernt alle Event Listener für ein bestimmtes Event oder alle Events, wenn kein Event angegeben ist.
|
|
186
|
+
|
|
187
|
+
### Types
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
interface MediaTrackMetadata {
|
|
191
|
+
title: string;
|
|
192
|
+
artist: string;
|
|
193
|
+
album?: string;
|
|
194
|
+
duration?: number; // in seconds
|
|
195
|
+
artwork?: string; // URL for album artwork
|
|
196
|
+
position?: number; // current position in seconds
|
|
197
|
+
isPlaying?: boolean;
|
|
198
|
+
shuffle?: boolean; // optional, default is false
|
|
199
|
+
repeat?: 'off' | 'all' | 'one'; // optional, default is
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type MediaControlEvent =
|
|
203
|
+
| 'play'
|
|
204
|
+
| 'pause'
|
|
205
|
+
| 'stop'
|
|
206
|
+
| 'skipToNext'
|
|
207
|
+
| 'skipToPrevious'
|
|
208
|
+
| 'seekForward'
|
|
209
|
+
| 'seekBackward'
|
|
210
|
+
| 'seek';
|
|
211
|
+
|
|
212
|
+
type MediaControlEventData = {
|
|
213
|
+
position?: number; // for seek events
|
|
214
|
+
};
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Platform specific notes
|
|
218
|
+
|
|
219
|
+
### Android
|
|
220
|
+
|
|
221
|
+
- Uses AndroidX Media3
|
|
222
|
+
- Creates notification channel automatically
|
|
223
|
+
|
|
224
|
+
### iOS
|
|
225
|
+
|
|
226
|
+
- Uses MPNowPlayingInfoCenter and MPRemoteCommandCenter
|
|
227
|
+
- Integrates in Control Center and Lock Screen
|
|
228
|
+
- Supports ear bud and other external controls
|
|
229
|
+
- Requires Background Audio Capability for Background Playback
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT
|
|
234
|
+
|
|
235
|
+
## Contributing
|
|
236
|
+
|
|
237
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.getExtOrDefault = {name ->
|
|
3
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['MediaControls_' + name]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
repositories {
|
|
7
|
+
google()
|
|
8
|
+
mavenCentral()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
dependencies {
|
|
12
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
13
|
+
// noinspection DifferentKotlinGradleVersion
|
|
14
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
apply plugin: "com.android.library"
|
|
20
|
+
apply plugin: "kotlin-android"
|
|
21
|
+
|
|
22
|
+
apply plugin: "com.facebook.react"
|
|
23
|
+
|
|
24
|
+
def getExtOrIntegerDefault(name) {
|
|
25
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["MediaControls_" + name]).toInteger()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
android {
|
|
29
|
+
namespace "com.mediacontrols"
|
|
30
|
+
|
|
31
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
32
|
+
|
|
33
|
+
defaultConfig {
|
|
34
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
35
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
buildFeatures {
|
|
39
|
+
buildConfig true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
buildTypes {
|
|
43
|
+
release {
|
|
44
|
+
minifyEnabled false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lintOptions {
|
|
49
|
+
disable "GradleCompatible"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
compileOptions {
|
|
53
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
54
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sourceSets {
|
|
58
|
+
main {
|
|
59
|
+
java.srcDirs += [
|
|
60
|
+
"generated/java",
|
|
61
|
+
"generated/jni"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
repositories {
|
|
68
|
+
mavenCentral()
|
|
69
|
+
google()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
73
|
+
|
|
74
|
+
dependencies {
|
|
75
|
+
implementation "com.facebook.react:react-android"
|
|
76
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
77
|
+
|
|
78
|
+
// androidx Media3 dependencies
|
|
79
|
+
implementation "androidx.media3:media3-session:1.8.0"
|
|
80
|
+
implementation "androidx.media3:media3-common:1.8.0"
|
|
81
|
+
implementation "androidx.media3:media3-ui:1.8.0"
|
|
82
|
+
implementation "androidx.core:core-ktx:1.16.0"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
react {
|
|
86
|
+
jsRootDir = file("../src/")
|
|
87
|
+
libraryName = "MediaControls"
|
|
88
|
+
codegenJavaPackageName = "com.mediacontrols"
|
|
89
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
|
|
3
|
+
<!-- Permissions for media playback and notifications -->
|
|
4
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
5
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
|
6
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
7
|
+
|
|
8
|
+
<application>
|
|
9
|
+
<!-- MediaSessionService for background playback -->
|
|
10
|
+
<service
|
|
11
|
+
android:name=".MediaControlsService"
|
|
12
|
+
android:enabled="true"
|
|
13
|
+
android:exported="true"
|
|
14
|
+
android:foregroundServiceType="mediaPlayback">
|
|
15
|
+
<intent-filter>
|
|
16
|
+
<action android:name="androidx.media3.session.MediaSessionService" />
|
|
17
|
+
</intent-filter>
|
|
18
|
+
<!-- Android Auto discovery -->
|
|
19
|
+
<intent-filter>
|
|
20
|
+
<action android:name="android.media.browse.MediaBrowserService" />
|
|
21
|
+
</intent-filter>
|
|
22
|
+
</service>
|
|
23
|
+
|
|
24
|
+
<meta-data
|
|
25
|
+
android:name="com.samsung.android.support.ongoing_activity"
|
|
26
|
+
android:value="true" />
|
|
27
|
+
</application>
|
|
28
|
+
|
|
29
|
+
</manifest>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package com.mediacontrols
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.media.AudioFocusRequest
|
|
5
|
+
import android.media.AudioManager
|
|
6
|
+
import android.media.AudioManager.OnAudioFocusChangeListener
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.os.Handler
|
|
9
|
+
import androidx.media3.common.util.UnstableApi
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
|
|
12
|
+
@UnstableApi
|
|
13
|
+
class AudioFocusListener(
|
|
14
|
+
context: ReactApplicationContext,
|
|
15
|
+
private val module: MediaControlsModule,
|
|
16
|
+
private val player: MediaControlsPlayer
|
|
17
|
+
) : OnAudioFocusChangeListener {
|
|
18
|
+
|
|
19
|
+
private val mAudioManager: AudioManager =
|
|
20
|
+
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
21
|
+
private var mFocusRequest: AudioFocusRequest? = null
|
|
22
|
+
|
|
23
|
+
private var mPlayOnAudioFocus = false
|
|
24
|
+
private var ducked = false
|
|
25
|
+
|
|
26
|
+
private var hasFocus = false
|
|
27
|
+
|
|
28
|
+
override fun onAudioFocusChange(focusChange: Int) {
|
|
29
|
+
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
|
|
30
|
+
abandonAudioFocus()
|
|
31
|
+
mPlayOnAudioFocus = false
|
|
32
|
+
module.sendEvent(Controls.STOP, null)
|
|
33
|
+
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
|
|
34
|
+
Handler(player.applicationLooper).post {
|
|
35
|
+
if (player.isPlaying) {
|
|
36
|
+
mPlayOnAudioFocus = true
|
|
37
|
+
module.sendEvent(Controls.PAUSE, null)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
|
|
41
|
+
module.sendEvent(Controls.DUCK, null)
|
|
42
|
+
ducked = true
|
|
43
|
+
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
|
|
44
|
+
if (mPlayOnAudioFocus) {
|
|
45
|
+
module.sendEvent(Controls.PLAY, null)
|
|
46
|
+
}
|
|
47
|
+
if (ducked) {
|
|
48
|
+
module.sendEvent(Controls.UN_DUCK, null)
|
|
49
|
+
}
|
|
50
|
+
mPlayOnAudioFocus = false
|
|
51
|
+
ducked = false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fun requestAudioFocus() {
|
|
56
|
+
if (hasFocus) return
|
|
57
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
58
|
+
mFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
|
59
|
+
.setOnAudioFocusChangeListener(this).build()
|
|
60
|
+
|
|
61
|
+
hasFocus = mAudioManager.requestAudioFocus(mFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
62
|
+
} else {
|
|
63
|
+
hasFocus = mAudioManager.requestAudioFocus(
|
|
64
|
+
this,
|
|
65
|
+
AudioManager.STREAM_MUSIC,
|
|
66
|
+
AudioManager.AUDIOFOCUS_GAIN
|
|
67
|
+
) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fun abandonAudioFocus() {
|
|
72
|
+
if (!hasFocus) return
|
|
73
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mFocusRequest != null) {
|
|
74
|
+
mAudioManager.abandonAudioFocusRequest(mFocusRequest!!)
|
|
75
|
+
} else mAudioManager.abandonAudioFocus(this)
|
|
76
|
+
|
|
77
|
+
hasFocus = false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.mediacontrols
|
|
2
|
+
|
|
3
|
+
enum class Controls(val code: String) {
|
|
4
|
+
PLAY("play"),
|
|
5
|
+
PAUSE("pause"),
|
|
6
|
+
STOP("stop"),
|
|
7
|
+
NEXT("skipToNext"),
|
|
8
|
+
PREVIOUS("skipToPrevious"),
|
|
9
|
+
SEEK("seek"),
|
|
10
|
+
SEEK_BACKWARD("seekBackward"),
|
|
11
|
+
SEEK_FORWARD("seekForward"),
|
|
12
|
+
DUCK("duck"),
|
|
13
|
+
UN_DUCK("unDuck"),
|
|
14
|
+
SHUFFLE("shuffle"),
|
|
15
|
+
REPEAT_MODE("repeatMode");
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
fun fromString(value: String): Controls? {
|
|
19
|
+
return entries.find { it.code == value }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package com.mediacontrols
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import androidx.media3.session.CommandButton
|
|
5
|
+
import androidx.media3.session.CommandButton.ICON_FAST_FORWARD
|
|
6
|
+
import androidx.media3.session.CommandButton.ICON_REWIND
|
|
7
|
+
import androidx.media3.session.CommandButton.ICON_SHUFFLE_OFF
|
|
8
|
+
import androidx.media3.session.CommandButton.ICON_SHUFFLE_ON
|
|
9
|
+
import androidx.media3.session.SessionCommand
|
|
10
|
+
|
|
11
|
+
private const val CUSTOM_COMMAND_REWIND_ACTION_ID = "REWIND_15"
|
|
12
|
+
private const val CUSTOM_COMMAND_FORWARD_ACTION_ID = "FAST_FWD_15"
|
|
13
|
+
private const val CUSTOM_COMMAND_SHUFFLE_ON_ACTION_ID = "SHUFFLE_ON"
|
|
14
|
+
private const val CUSTOM_COMMAND_SHUFFLE_OFF_ACTION_ID = "SHUFFLE_OFF"
|
|
15
|
+
private const val CUSTOM_COMMAND_REPEAT_ONE_ACTION_ID = "REPEAT_ONE"
|
|
16
|
+
private const val CUSTOM_COMMAND_REPEAT_ALL_ACTION_ID = "REPEAT_ALL"
|
|
17
|
+
private const val CUSTOM_COMMAND_REPEAT_OFF_ACTION_ID = "REPEAT_OFF"
|
|
18
|
+
|
|
19
|
+
enum class CustomCommandButton(
|
|
20
|
+
val customAction: String,
|
|
21
|
+
val commandButton: CommandButton,
|
|
22
|
+
) {
|
|
23
|
+
REWIND(
|
|
24
|
+
customAction = CUSTOM_COMMAND_REWIND_ACTION_ID,
|
|
25
|
+
commandButton = CommandButton.Builder(ICON_REWIND)
|
|
26
|
+
.setDisplayName("Rewind")
|
|
27
|
+
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_REWIND_ACTION_ID, Bundle()))
|
|
28
|
+
.build(),
|
|
29
|
+
),
|
|
30
|
+
FORWARD(
|
|
31
|
+
customAction = CUSTOM_COMMAND_FORWARD_ACTION_ID,
|
|
32
|
+
commandButton = CommandButton.Builder(ICON_FAST_FORWARD)
|
|
33
|
+
.setDisplayName("Forward")
|
|
34
|
+
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_FORWARD_ACTION_ID, Bundle()))
|
|
35
|
+
.build(),
|
|
36
|
+
),
|
|
37
|
+
SHUFFLE_ON(
|
|
38
|
+
customAction = CUSTOM_COMMAND_SHUFFLE_ON_ACTION_ID,
|
|
39
|
+
commandButton = CommandButton.Builder(ICON_SHUFFLE_ON)
|
|
40
|
+
.setDisplayName("ShuffleOn")
|
|
41
|
+
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_SHUFFLE_ON_ACTION_ID, Bundle()))
|
|
42
|
+
.build(),
|
|
43
|
+
),
|
|
44
|
+
SHUFFLE_OFF(
|
|
45
|
+
customAction = CUSTOM_COMMAND_SHUFFLE_OFF_ACTION_ID,
|
|
46
|
+
commandButton = CommandButton.Builder(ICON_SHUFFLE_OFF)
|
|
47
|
+
.setDisplayName("ShuffleOn")
|
|
48
|
+
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_SHUFFLE_OFF_ACTION_ID, Bundle()))
|
|
49
|
+
.build(),
|
|
50
|
+
),
|
|
51
|
+
REPEAT_ONE(
|
|
52
|
+
customAction = CUSTOM_COMMAND_REPEAT_ONE_ACTION_ID,
|
|
53
|
+
commandButton = CommandButton.Builder(CommandButton.ICON_REPEAT_ONE)
|
|
54
|
+
.setDisplayName("Repeat One")
|
|
55
|
+
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_REPEAT_ONE_ACTION_ID, Bundle()))
|
|
56
|
+
.build(),
|
|
57
|
+
),
|
|
58
|
+
REPEAT_ALL(
|
|
59
|
+
customAction = CUSTOM_COMMAND_REPEAT_ALL_ACTION_ID,
|
|
60
|
+
commandButton = CommandButton.Builder(CommandButton.ICON_REPEAT_ALL)
|
|
61
|
+
.setDisplayName("Repeat All")
|
|
62
|
+
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_REPEAT_ALL_ACTION_ID, Bundle()))
|
|
63
|
+
.build(),
|
|
64
|
+
),
|
|
65
|
+
REPEAT_OFF(
|
|
66
|
+
customAction = CUSTOM_COMMAND_REPEAT_OFF_ACTION_ID,
|
|
67
|
+
commandButton = CommandButton.Builder(CommandButton.ICON_REPEAT_OFF)
|
|
68
|
+
.setDisplayName("Repeat Off")
|
|
69
|
+
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_REPEAT_OFF_ACTION_ID, Bundle()))
|
|
70
|
+
.build(),
|
|
71
|
+
),;
|
|
72
|
+
}
|