Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/android-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Android Build

on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17

- name: Make gradlew executable
run: chmod +x ./gradlew

- name: Build debug APK
run: ./gradlew assembleDebug --stacktrace

- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: dddplayer-debug-apk
path: app/build/outputs/apk/debug/*.apk
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ dependencies {
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.fragment.ktx)
implementation(libs.okhttp)
implementation(libs.gson)

// Media3 dependencies
implementation(libs.media3.exoplayer)
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/java/top/rootu/dddplayer/bridge/BridgeConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package top.rootu.dddplayer.bridge

data class BridgeConfig(
val enabled: Boolean = false,
val sessionId: String? = null,
val mode: BridgeMode = BridgeMode.BROADCAST,
val emitPosition: Boolean = true,
val emitUserActions: Boolean = true,
val positionIntervalMs: Long = 1000L,
val client: String = "lampa",
val eventAction: String = BroadcastTransport.DEFAULT_ACTION_EVENT,
val receiverPackage: String? = null,
val schemaVersion: Int = 1,
val localPort: Int = 39677,
val localToken: String? = null
)

enum class BridgeMode {
BROADCAST,
LOCAL,
BOTH
}
17 changes: 17 additions & 0 deletions app/src/main/java/top/rootu/dddplayer/bridge/BridgeDispatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package top.rootu.dddplayer.bridge

import android.util.Log

class BridgeDispatcher(
private val config: BridgeConfig,
private val transport: BridgeTransport
) {
fun emit(event: BridgeEvent) {
if (!config.enabled) return
try {
transport.send(event)
} catch (e: Exception) {
Log.w("DDDPlayerBridge", "Failed to send bridge event: ${event::class.simpleName}", e)
}
}
}
126 changes: 126 additions & 0 deletions app/src/main/java/top/rootu/dddplayer/bridge/BridgeEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package top.rootu.dddplayer.bridge

data class BridgeEnvelope(
val schema: Int = 1,
val type: String,
val client: String,
val sessionId: String?,
val ts: Long,
val payload: BridgeEvent
)

data class BridgeMediaItem(
val uri: String?,
val title: String?,
val filename: String? = null,
val externalId: String? = null,
val season: Int? = null,
val episode: Int? = null,
val source: String? = null
)

sealed class BridgeEvent {
abstract val sessionId: String?
abstract val ts: Long
abstract val uri: String?

data class SessionStarted(
override val sessionId: String?,
override val ts: Long,
override val uri: String?,
val title: String?,
val playlistSize: Int,
val startIndex: Int,
val startPosition: Long? = null,
val currentItem: BridgeMediaItem? = null
) : BridgeEvent()

data class PlaybackStateChanged(
override val sessionId: String?,
override val ts: Long,
override val uri: String?,
val isPlaying: Boolean,
val isBuffering: Boolean,
val position: Long?,
val duration: Long?,
val windowIndex: Int? = null,
val title: String? = null
) : BridgeEvent()

data class PositionTick(
override val sessionId: String?,
override val ts: Long,
override val uri: String?,
val position: Long?,
val duration: Long?,
val bufferedPosition: Long?,
val bufferedPercentage: Int?,
val windowIndex: Int? = null,
val title: String? = null
) : BridgeEvent()

data class SeekCompleted(
override val sessionId: String?,
override val ts: Long,
override val uri: String?,
val fromPosition: Long?,
val toPosition: Long?,
val windowIndex: Int? = null
) : BridgeEvent()

data class PlaylistItemChanged(
override val sessionId: String?,
override val ts: Long,
override val uri: String?,
val windowIndex: Int,
val playlistSize: Int,
val title: String?,
val reason: String,
val position: Long?,
val duration: Long?,
val hasPrevious: Boolean,
val hasNext: Boolean,
val currentItem: BridgeMediaItem? = null
) : BridgeEvent()

data class PlaybackEnded(
override val sessionId: String?,
override val ts: Long,
override val uri: String?,
val windowIndex: Int,
val playlistSize: Int,
val title: String?,
val position: Long?,
val duration: Long?
) : BridgeEvent()

data class SessionFinished(
override val sessionId: String?,
override val ts: Long,
override val uri: String?,
val position: Long?,
val duration: Long?,
val endBy: String,
val windowIndex: Int? = null,
val playlistSize: Int? = null,
val title: String? = null
) : BridgeEvent()

data class Error(
override val sessionId: String?,
override val ts: Long,
override val uri: String?,
val code: String?,
val message: String?,
val windowIndex: Int? = null
) : BridgeEvent()

data class UserAction(
override val sessionId: String?,
override val ts: Long,
override val uri: String?,
val action: String,
val payload: Map<String, String> = emptyMap(),
val windowIndex: Int? = null
) : BridgeEvent()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package top.rootu.dddplayer.bridge

interface BridgeTransport {
fun send(event: BridgeEvent)
}
46 changes: 46 additions & 0 deletions app/src/main/java/top/rootu/dddplayer/bridge/BroadcastTransport.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package top.rootu.dddplayer.bridge

import android.content.Context
import android.content.Intent
import android.util.Log
import com.google.gson.Gson

class BroadcastTransport(
private val context: Context,
private val config: BridgeConfig
) : BridgeTransport {

private val gson = Gson()

override fun send(event: BridgeEvent) {
val type = event::class.simpleName ?: "Unknown"
val envelope = BridgeEnvelope(
schema = config.schemaVersion,
type = type,
client = config.client,
sessionId = event.sessionId,
ts = event.ts,
payload = event
)

val intent = Intent(config.eventAction)
config.receiverPackage?.takeIf { it.isNotBlank() }?.let { intent.setPackage(it) }
intent.putExtra(EXTRA_SCHEMA, config.schemaVersion)
intent.putExtra(EXTRA_CLIENT, config.client)
intent.putExtra(EXTRA_SESSION_ID, event.sessionId)
intent.putExtra(EXTRA_EVENT_TYPE, type)
val json = gson.toJson(envelope)
Log.d("DDDPlayerBridge", json)
intent.putExtra(EXTRA_EVENT_JSON, json)
context.sendBroadcast(intent)
}

companion object {
const val DEFAULT_ACTION_EVENT = "top.rootu.dddplayer.bridge.EVENT"
const val EXTRA_SCHEMA = "schema"
const val EXTRA_CLIENT = "client"
const val EXTRA_SESSION_ID = "session_id"
const val EXTRA_EVENT_TYPE = "event_type"
const val EXTRA_EVENT_JSON = "event_json"
}
}
21 changes: 21 additions & 0 deletions app/src/main/java/top/rootu/dddplayer/bridge/CompositeTransport.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package top.rootu.dddplayer.bridge

import android.util.Log

class CompositeTransport(
private val transports: List<BridgeTransport>
) : BridgeTransport {
override fun send(event: BridgeEvent) {
transports.forEach { transport ->
try {
transport.send(event)
} catch (e: Exception) {
Log.w(
"DDDPlayerBridge",
"transport failed=${transport::class.simpleName}, event=${event::class.simpleName}",
e
)
}
}
}
}
Loading