Skip to content

Commit aaf28ae

Browse files
committed
request(metadata): expanded metadata support
1 parent 1bf0db1 commit aaf28ae

File tree

13 files changed

+497
-105
lines changed

13 files changed

+497
-105
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package com.doublesymmetry.trackplayer.model
2+
3+
import android.os.Bundle
4+
import com.google.android.exoplayer2.MediaMetadata
5+
import com.google.android.exoplayer2.metadata.Metadata
6+
import com.google.android.exoplayer2.metadata.flac.VorbisComment
7+
import com.google.android.exoplayer2.metadata.icy.IcyHeaders
8+
import com.google.android.exoplayer2.metadata.icy.IcyInfo
9+
import com.google.android.exoplayer2.metadata.id3.ChapterFrame
10+
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
11+
import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame
12+
import com.google.android.exoplayer2.metadata.mp4.MdtaMetadataEntry
13+
import timber.log.Timber
14+
15+
sealed class MetadataAdapter {
16+
companion object {
17+
fun fromMetadata(metadata: Metadata): List<Bundle> {
18+
val group = mutableListOf<Bundle>()
19+
20+
(0 until metadata.length()).forEach { i ->
21+
group.add(Bundle().apply {
22+
val rawEntries = mutableListOf<Bundle>()
23+
24+
when (val entry = metadata[i]) {
25+
is ChapterFrame -> {
26+
Timber.d("ChapterFrame: ${entry.id}")
27+
}
28+
is TextInformationFrame -> {
29+
val rawEntry = Bundle()
30+
31+
when (entry.id.uppercase()) {
32+
"TIT2", "TT2" -> {
33+
putString("title", entry.value)
34+
rawEntry.putString("commonKey", "title")
35+
}
36+
"TALB", "TOAL", "TAL" -> {
37+
putString("albumName", entry.value)
38+
rawEntry.putString("commonKey", "albumName")
39+
}
40+
"TOPE", "TPE1", "TP1" -> {
41+
putString("artist", entry.value)
42+
rawEntry.putString("commonKey", "artist")
43+
}
44+
"TDRC", "TOR" -> {
45+
putString("creationDate", entry.value)
46+
rawEntry.putString("commonKey", "creationDate")
47+
}
48+
"TCON", "TCO" -> {
49+
putString("genre", entry.value)
50+
rawEntry.putString("commonKey", "genre")
51+
}
52+
}
53+
54+
rawEntry.putString("key", entry.id.uppercase())
55+
rawEntry.putString("keySpace", "org.id3")
56+
rawEntry.putString("value", entry.value)
57+
rawEntry.putString("time", "-1")
58+
rawEntries.add(rawEntry)
59+
}
60+
61+
is UrlLinkFrame -> {
62+
rawEntries.add(Bundle().apply {
63+
putString("value", entry.url)
64+
putString("key", entry.id.uppercase())
65+
putString("keySpace", "org.id3")
66+
putString("time", "-1")
67+
})
68+
}
69+
70+
is IcyHeaders -> {
71+
putString("title", entry.name)
72+
putString("genre", entry.genre)
73+
74+
rawEntries.add(Bundle().apply {
75+
putString("value", entry.name)
76+
putString("commonKey", "title")
77+
putString("key", "StreamTitle")
78+
putString("keySpace", "icy")
79+
putString("time", "-1")
80+
})
81+
82+
rawEntries.add(Bundle().apply {
83+
putString("value", entry.url)
84+
putString("key", "StreamURL")
85+
putString("keySpace", "icy")
86+
putString("time", "-1")
87+
})
88+
89+
rawEntries.add(Bundle().apply {
90+
putString("value", entry.genre)
91+
putString("commonKey", "genre")
92+
putString("key", "StreamGenre")
93+
putString("keySpace", "icy")
94+
putString("time", "-1")
95+
})
96+
}
97+
98+
is IcyInfo -> {
99+
putString("title", entry.title)
100+
101+
rawEntries.add(Bundle().apply {
102+
putString("value", entry.url)
103+
putString("key", "StreamURL")
104+
putString("keySpace", "icy")
105+
putString("time", "-1")
106+
})
107+
108+
rawEntries.add(Bundle().apply {
109+
putString("value", entry.title)
110+
putString("commonKey", "title")
111+
putString("key", "StreamTitle")
112+
putString("keySpace", "icy")
113+
putString("time", "-1")
114+
})
115+
}
116+
117+
is VorbisComment -> {
118+
val rawEntry = Bundle()
119+
120+
when (entry.key) {
121+
"TITLE" -> {
122+
putString("title", entry.value)
123+
rawEntry.putString("commonKey", "title")
124+
}
125+
"ARTIST" -> {
126+
putString("artist", entry.value)
127+
rawEntry.putString("commonKey", "artist")
128+
}
129+
"ALBUM" -> {
130+
putString("albumName", entry.value)
131+
rawEntry.putString("commonKey", "albumName")
132+
}
133+
"DATE" -> {
134+
putString("creationDate", entry.value)
135+
rawEntry.putString("commonKey", "creationDate")
136+
}
137+
"GENRE" -> {
138+
putString("genre", entry.value)
139+
rawEntry.putString("commonKey", "genre")
140+
}
141+
"URL" -> {
142+
putString("url", entry.value)
143+
}
144+
}
145+
146+
rawEntry.putString("key", entry.key)
147+
rawEntry.putString("keySpace", "org.vorbis")
148+
rawEntry.putString("value", entry.value)
149+
rawEntry.putString("time", "-1")
150+
rawEntries.add(rawEntry)
151+
}
152+
153+
is MdtaMetadataEntry -> {
154+
val rawEntry = Bundle()
155+
when (entry.key) {
156+
"com.apple.quicktime.title" -> {
157+
putString("title", entry.value.toString())
158+
rawEntry.putString("commonKey", "title")
159+
}
160+
"com.apple.quicktime.artist" -> {
161+
putString("artist", entry.value.toString())
162+
rawEntry.putString("commonKey", "artist")
163+
}
164+
"com.apple.quicktime.album" -> {
165+
putString("albumName", entry.value.toString())
166+
rawEntry.putString("commonKey", "albumName")
167+
}
168+
"com.apple.quicktime.creationdate" -> {
169+
putString("creationDate", entry.value.toString())
170+
rawEntry.putString("commonKey", "creationDate")
171+
}
172+
"com.apple.quicktime.genre" -> {
173+
putString("genre", entry.value.toString())
174+
rawEntry.putString("commonKey", "genre")
175+
}
176+
}
177+
178+
rawEntry.putString("key", entry.key.substringAfterLast("."))
179+
rawEntry.putString("keySpace", "com.apple.quicktime")
180+
rawEntry.putString("value", entry.value.toString())
181+
rawEntry.putString("time", "-1")
182+
rawEntries.add(rawEntry)
183+
}
184+
}
185+
186+
putParcelableArray("raw", rawEntries.toTypedArray())
187+
})
188+
}
189+
190+
return group
191+
}
192+
193+
fun fromMediaMetadata(metadata: MediaMetadata): Bundle {
194+
return Bundle().apply {
195+
metadata.title?.let { putString("title", it.toString()) }
196+
metadata.artist?.let { putString("artist", it.toString()) }
197+
metadata.albumTitle?.let { putString("albumName", it.toString()) }
198+
metadata.subtitle?.let { putString("subtitle", it.toString()) }
199+
metadata.description?.let { putString("description", it.toString()) }
200+
metadata.artworkUri?.let { putString("artworkUri", it.toString()) }
201+
metadata.trackNumber?.let { putInt("trackNumber", it) }
202+
metadata.composer?.let { putString("composer", it.toString()) }
203+
metadata.conductor?.let { putString("conductor", it.toString()) }
204+
metadata.genre?.let { putString("genre", it.toString()) }
205+
metadata.compilation?.let { putString("compilation", it.toString()) }
206+
metadata.station?.let { putString("station", it.toString()) }
207+
metadata.mediaType?.let { putInt("mediaType", it) }
208+
209+
// This is how SwiftAudioEx outputs it in the metadata dictionary
210+
(metadata.recordingDay to metadata.recordingMonth).let { (day, month) ->
211+
// if both are not null, combine them into a single string
212+
if (day != null && month != null) {
213+
putString("creationDate", "${String.format("%02d", day)}${String.format("%02d", month)}")
214+
} else if (day != null) {
215+
putString("creationDate", String.format("%02d", day))
216+
} else if (month != null) {
217+
putString("creationDate", String.format("%02d", month))
218+
}
219+
}
220+
metadata.recordingYear?.let { putString("creationYear", it.toString()) }
221+
}
222+
}
223+
}
224+
}

android/src/main/java/com/doublesymmetry/trackplayer/module/MusicEvents.kt

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ class MusicEvents(private val reactContext: ReactContext) : BroadcastReceiver()
4646
const val PLAYBACK_PROGRESS_UPDATED = "playback-progress-updated"
4747
const val PLAYBACK_ERROR = "playback-error"
4848

49+
// Metadata Events
50+
const val METADATA_CHAPTER_RECEIVED = "metadata-chapter-received"
51+
const val METADATA_TIMED_RECEIVED = "metadata-timed-received"
52+
const val METADATA_COMMON_RECEIVED = "metadata-common-received"
53+
4954
// Other
5055
const val PLAYER_ERROR = "player-error"
5156

android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt

+2-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import com.doublesymmetry.trackplayer.model.Track
1414
import com.doublesymmetry.trackplayer.module.MusicEvents.Companion.EVENT_INTENT
1515
import com.doublesymmetry.trackplayer.service.MusicService
1616
import com.doublesymmetry.trackplayer.utils.AppForegroundTracker
17-
import com.doublesymmetry.trackplayer.utils.BundleUtils
1817
import com.doublesymmetry.trackplayer.utils.RejectionException
1918
import com.facebook.react.bridge.*
2019
import com.google.android.exoplayer2.DefaultLoadControl.*
@@ -292,7 +291,7 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
292291
callback.resolve(null)
293292
return@launch
294293
}
295-
var bundle = Arguments.toBundle(data);
294+
val bundle = Arguments.toBundle(data);
296295
if (bundle is Bundle) {
297296
musicService.load(bundleToTrack(bundle))
298297
callback.resolve(null)
@@ -314,7 +313,7 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
314313
val inputIndexes = Arguments.toList(data)
315314
if (inputIndexes != null) {
316315
val size = musicService.tracks.size
317-
var indexes: ArrayList<Int> = ArrayList();
316+
val indexes: ArrayList<Int> = ArrayList();
318317
for (inputIndex in inputIndexes) {
319318
val index = if (inputIndex is Int) inputIndex else inputIndex.toString().toInt()
320319
if (index < 0 || index >= size) {

android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt

+27-9
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import android.support.v4.media.RatingCompat
1313
import androidx.annotation.MainThread
1414
import androidx.core.app.NotificationCompat
1515
import androidx.core.app.NotificationCompat.PRIORITY_LOW
16-
import androidx.localbroadcastmanager.content.LocalBroadcastManager
1716
import com.doublesymmetry.kotlinaudio.models.*
1817
import com.doublesymmetry.kotlinaudio.models.NotificationButton.*
1918
import com.doublesymmetry.kotlinaudio.players.QueuedAudioPlayer
@@ -22,17 +21,17 @@ import com.doublesymmetry.trackplayer.extensions.NumberExt.Companion.toMilliseco
2221
import com.doublesymmetry.trackplayer.extensions.NumberExt.Companion.toSeconds
2322
import com.doublesymmetry.trackplayer.extensions.asLibState
2423
import com.doublesymmetry.trackplayer.extensions.find
24+
import com.doublesymmetry.trackplayer.model.MetadataAdapter
2525
import com.doublesymmetry.trackplayer.model.PlaybackMetadata
2626
import com.doublesymmetry.trackplayer.model.Track
2727
import com.doublesymmetry.trackplayer.model.TrackAudioItem
2828
import com.doublesymmetry.trackplayer.module.MusicEvents
29-
import com.doublesymmetry.trackplayer.module.MusicEvents.Companion.EVENT_INTENT
30-
import com.doublesymmetry.trackplayer.utils.AppForegroundTracker
3129
import com.doublesymmetry.trackplayer.utils.BundleUtils
3230
import com.doublesymmetry.trackplayer.utils.BundleUtils.setRating
3331
import com.facebook.react.HeadlessJsTaskService
3432
import com.facebook.react.bridge.Arguments
3533
import com.facebook.react.jstasks.HeadlessJsTaskConfig
34+
import com.facebook.react.modules.core.DeviceEventManagerModule
3635
import com.google.android.exoplayer2.ui.R as ExoPlayerR
3736
import kotlinx.coroutines.*
3837
import kotlinx.coroutines.flow.flow
@@ -459,7 +458,7 @@ class MusicService : HeadlessJsTaskService() {
459458

460459
val b = Bundle()
461460
b.putDouble("lastPosition", oldPosition)
462-
if (tracks.size > 0) {
461+
if (tracks.isNotEmpty()) {
463462
b.putInt("index", player.currentIndex)
464463
b.putBundle("track", tracks[player.currentIndex].originalItem)
465464
if (previousIndex != null) {
@@ -662,6 +661,9 @@ class MusicService : HeadlessJsTaskService() {
662661

663662
scope.launch {
664663
event.onTimedMetadata.collect {
664+
val data = MetadataAdapter.fromMetadata(it)
665+
emitList(MusicEvents.METADATA_TIMED_RECEIVED, data)
666+
665667
// TODO: Handle the different types of metadata and publish to new events
666668
val metadata = PlaybackMetadata.fromId3Metadata(it)
667669
?: PlaybackMetadata.fromIcy(it)
@@ -683,6 +685,13 @@ class MusicService : HeadlessJsTaskService() {
683685
}
684686
}
685687

688+
scope.launch {
689+
event.onCommonMetadata.collect {
690+
val data = MetadataAdapter.fromMediaMetadata(it)
691+
emit(MusicEvents.METADATA_COMMON_RECEIVED, data)
692+
}
693+
}
694+
686695
scope.launch {
687696
event.playWhenReadyChange.collect {
688697
Bundle().apply {
@@ -712,11 +721,20 @@ class MusicService : HeadlessJsTaskService() {
712721
}
713722

714723
@MainThread
715-
private fun emit(event: String?, data: Bundle? = null) {
716-
val intent = Intent(EVENT_INTENT)
717-
intent.putExtra(EVENT_KEY, event)
718-
if (data != null) intent.putExtra(DATA_KEY, data)
719-
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
724+
private fun emit(event: String, data: Bundle? = null) {
725+
reactNativeHost.reactInstanceManager.currentReactContext
726+
?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
727+
?.emit(event, data?.let { Arguments.fromBundle(it) })
728+
}
729+
730+
@MainThread
731+
private fun emitList(event: String, data: List<Bundle> = emptyList()) {
732+
val payload = Arguments.createArray()
733+
data.forEach { payload.pushMap(Arguments.fromBundle(it)) }
734+
735+
reactNativeHost.reactInstanceManager.currentReactContext
736+
?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
737+
?.emit(event, payload)
720738
}
721739

722740
override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig {

example/src/assets/data/playlist.json

+8
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,13 @@
4040
"artist": "New York, NY",
4141
"artwork": "https://rntp.dev/example/smooth-jazz-24-7.jpeg",
4242
"isLiveStream": true
43+
},
44+
{
45+
"url": "https://traffic.libsyn.com/atpfm/atp545.mp3",
46+
"title": "Chapters"
47+
},
48+
{
49+
"url": "https://kut.streamguys1.com/kutx-app.aac?listenerId=123456784123",
50+
"title": "KUTX"
4351
}
4452
]

example/src/services/PlaybackService.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,19 @@ export async function PlaybackService() {
5757
});
5858

5959
TrackPlayer.addEventListener(Event.PlaybackMetadataReceived, (event) => {
60-
console.log('Event.PlaybackMetadataReceived', event);
60+
console.log('[Deprecated] Event.PlaybackMetadataReceived', event);
61+
});
62+
63+
TrackPlayer.addEventListener(Event.MetatadataChapterReceived, (event) => {
64+
console.log('Event.MetatadataChapterReceived', event);
65+
});
66+
67+
TrackPlayer.addEventListener(Event.MetadataTimedReceived, (event) => {
68+
console.log('Event.MetadataTimedReceived', event);
69+
});
70+
71+
TrackPlayer.addEventListener(Event.MetadataCommonReceived, (event) => {
72+
console.log('Event.MetadataCommonReceived', event);
6173
});
6274

6375
TrackPlayer.addEventListener(

0 commit comments

Comments
 (0)