diff --git a/build.gradle b/build.gradle
index 8c2186522ae..a03f1e0c133 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,6 +20,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.novoda:bintray-release:0.9.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1'
+ classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10'
}
}
allprojects {
diff --git a/core_settings.gradle b/core_settings.gradle
index 241b94a19ba..9cf91d379b5 100644
--- a/core_settings.gradle
+++ b/core_settings.gradle
@@ -49,6 +49,7 @@ include modulePrefix + 'extension-rtmp'
include modulePrefix + 'extension-leanback'
include modulePrefix + 'extension-jobdispatcher'
include modulePrefix + 'extension-workmanager'
+include modulePrefix + 'extension-multi-track'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
project(modulePrefix + 'library-common').projectDir = new File(rootDir, 'library/common')
@@ -78,3 +79,4 @@ project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensi
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')
+project(modulePrefix + 'extension-multi-track').projectDir = new File(rootDir, 'extensions/multi-track')
diff --git a/demos/multi-track/.gitignore b/demos/multi-track/.gitignore
new file mode 100644
index 00000000000..42afabfd2ab
--- /dev/null
+++ b/demos/multi-track/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/demos/multi-track/build.gradle b/demos/multi-track/build.gradle
new file mode 100644
index 00000000000..966a9c38f45
--- /dev/null
+++ b/demos/multi-track/build.gradle
@@ -0,0 +1,57 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+apply from: '../../constants.gradle'
+
+android {
+ compileSdkVersion 32
+
+ defaultConfig {
+ versionName project.ext.releaseVersion
+ versionCode project.ext.releaseVersionCode
+ minSdkVersion project.ext.minSdkVersion
+ targetSdkVersion project.ext.appTargetSdkVersion
+ multiDexEnabled true
+ }
+
+ buildTypes {
+ release {
+ shrinkResources true
+ minifyEnabled true
+ proguardFiles = [
+ "proguard-rules.txt",
+ getDefaultProguardFile('proguard-android.txt')
+ ]
+ }
+ debug {
+ jniDebuggable = true
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+ implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-dash')
+ implementation project(modulePrefix + 'library-hls')
+ implementation project(modulePrefix + 'library-smoothstreaming')
+ implementation project(modulePrefix + 'library-ui')
+ implementation project(modulePrefix + 'extension-cast')
+ implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
+ implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'com.google.android.material:material:1.2.1'
+
+ implementation 'com.google.code.gson:gson:2.8.6'
+
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+}
\ No newline at end of file
diff --git a/demos/multi-track/proguard-rules.pro b/demos/multi-track/proguard-rules.pro
new file mode 100644
index 00000000000..481bb434814
--- /dev/null
+++ b/demos/multi-track/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/demos/multi-track/src/androidTest/java/com/github/qingmei2/exoplayer/multi_track/ExampleInstrumentedTest.kt b/demos/multi-track/src/androidTest/java/com/github/qingmei2/exoplayer/multi_track/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000000..a32d33ee263
--- /dev/null
+++ b/demos/multi-track/src/androidTest/java/com/github/qingmei2/exoplayer/multi_track/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.github.qingmei2.exoplayer.multi_track
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.github.qingmei2.exoplayer.multi_track", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/demos/multi-track/src/main/AndroidManifest.xml b/demos/multi-track/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..5e0904c3f17
--- /dev/null
+++ b/demos/multi-track/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/multi-track/src/main/assets/media/eh_eh/BGV.wav b/demos/multi-track/src/main/assets/media/eh_eh/BGV.wav
new file mode 100644
index 00000000000..70022a17af4
Binary files /dev/null and b/demos/multi-track/src/main/assets/media/eh_eh/BGV.wav differ
diff --git a/demos/multi-track/src/main/assets/media/eh_eh/Bass.wav b/demos/multi-track/src/main/assets/media/eh_eh/Bass.wav
new file mode 100644
index 00000000000..446461ce507
Binary files /dev/null and b/demos/multi-track/src/main/assets/media/eh_eh/Bass.wav differ
diff --git a/demos/multi-track/src/main/assets/media/eh_eh/Drums.wav b/demos/multi-track/src/main/assets/media/eh_eh/Drums.wav
new file mode 100644
index 00000000000..c71f92a5de3
Binary files /dev/null and b/demos/multi-track/src/main/assets/media/eh_eh/Drums.wav differ
diff --git a/demos/multi-track/src/main/assets/media/eh_eh/Keys.wav b/demos/multi-track/src/main/assets/media/eh_eh/Keys.wav
new file mode 100644
index 00000000000..d9113322bb2
Binary files /dev/null and b/demos/multi-track/src/main/assets/media/eh_eh/Keys.wav differ
diff --git a/demos/multi-track/src/main/assets/media/eh_eh/Lead.wav b/demos/multi-track/src/main/assets/media/eh_eh/Lead.wav
new file mode 100644
index 00000000000..2a2adb64960
Binary files /dev/null and b/demos/multi-track/src/main/assets/media/eh_eh/Lead.wav differ
diff --git a/demos/multi-track/src/main/assets/media/half_moon.mp3 b/demos/multi-track/src/main/assets/media/half_moon.mp3
new file mode 100644
index 00000000000..38d6fd64080
Binary files /dev/null and b/demos/multi-track/src/main/assets/media/half_moon.mp3 differ
diff --git a/demos/multi-track/src/main/assets/media/songs.json b/demos/multi-track/src/main/assets/media/songs.json
new file mode 100644
index 00000000000..384c2d52bd0
--- /dev/null
+++ b/demos/multi-track/src/main/assets/media/songs.json
@@ -0,0 +1,48 @@
+[
+ {
+ "songName": "Eh,Eh(Nothing Else I Can Say)",
+ "singerName": "Lady Gaga",
+ "partItems": [
+ {
+ "partId": "1",
+ "partName": "贝斯",
+ "partType": "instrument",
+ "partPath": "asset:///media/eh_eh/Bass.wav",
+ "mainType": false,
+ "defaultPlay": true
+ },
+ {
+ "partId": "2",
+ "partName": "和声",
+ "partType": "harmony",
+ "partPath": "asset:///media/eh_eh/BGV.wav",
+ "mainType": false,
+ "defaultPlay": true
+ },
+ {
+ "partId": "3",
+ "partName": "鼓音",
+ "partType": "instrument",
+ "partPath": "asset:///media/eh_eh/Drums.wav",
+ "mainType": false,
+ "defaultPlay": true
+ },
+ {
+ "partId": "4",
+ "partName": "背景音",
+ "partType": "background",
+ "partPath": "asset:///media/eh_eh/Keys.wav",
+ "mainType": false,
+ "defaultPlay": true
+ },
+ {
+ "partId": "5",
+ "partName": "歌手",
+ "partType": "musician",
+ "partPath": "asset:///media/eh_eh/Lead.wav",
+ "mainType": true,
+ "defaultPlay": true
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/demos/multi-track/src/main/assets/media/summer.mp3 b/demos/multi-track/src/main/assets/media/summer.mp3
new file mode 100644
index 00000000000..e65da895542
Binary files /dev/null and b/demos/multi-track/src/main/assets/media/summer.mp3 differ
diff --git a/demos/multi-track/src/main/assets/media/sunny.mp4 b/demos/multi-track/src/main/assets/media/sunny.mp4
new file mode 100644
index 00000000000..7c0a2b860ee
Binary files /dev/null and b/demos/multi-track/src/main/assets/media/sunny.mp4 differ
diff --git a/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/MainActivity.kt b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/MainActivity.kt
new file mode 100644
index 00000000000..c6a05af69a7
--- /dev/null
+++ b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/MainActivity.kt
@@ -0,0 +1,28 @@
+package com.github.qingmei2.exoplayer.multi_track
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.view.View
+import com.github.qingmei2.exoplayer.multi_track.ui.MultiMusicActivity
+import com.github.qingmei2.exoplayer.multi_track.ui.MultiTrackMainActivity
+
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ // 同时播放多首音乐
+ findViewById(R.id.btn_multi_music).setOnClickListener(this::onMultiPlayerClicked)
+ // 同时播放单曲多个音轨文件
+ findViewById(R.id.btn_single_song_list).setOnClickListener(this::onSingleSongListClicked)
+ }
+
+ private fun onMultiPlayerClicked(view: View) {
+ MultiMusicActivity.launch(this)
+ }
+
+ private fun onSingleSongListClicked(view: View) {
+ MultiTrackMainActivity.launch(this)
+ }
+}
\ No newline at end of file
diff --git a/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/common/SimpleSeekBarListener.kt b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/common/SimpleSeekBarListener.kt
new file mode 100644
index 00000000000..c7be9c7de88
--- /dev/null
+++ b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/common/SimpleSeekBarListener.kt
@@ -0,0 +1,22 @@
+package com.github.qingmei2.exoplayer.multi_track.common
+
+import android.widget.SeekBar
+import androidx.annotation.RestrictTo
+import com.google.android.exoplayer2.Player
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class SimpleSeekBarListener(private val player: Player) : SeekBar.OnSeekBarChangeListener {
+
+ override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
+ if (progress == 0 || seekBar.max == 0) return
+ if (fromUser) {
+ player.seekTo(progress * 1000L)
+ }
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar) {
+ }
+
+ override fun onStopTrackingTouch(seekBar: SeekBar) {
+ }
+}
\ No newline at end of file
diff --git a/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/entity/SongItem.kt b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/entity/SongItem.kt
new file mode 100644
index 00000000000..f62370671f5
--- /dev/null
+++ b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/entity/SongItem.kt
@@ -0,0 +1,15 @@
+package com.github.qingmei2.exoplayer.multi_track.entity
+
+data class SongItem(val songName: String,
+ val singerName: String,
+ val partItems: List)
+
+
+data class SongPartItem(
+ val partId: String,
+ val partName: String,
+ val partType: String,
+ val partPath: String,
+ val mainType: Boolean,
+ val defaultPlay: Boolean
+)
\ No newline at end of file
diff --git a/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/IPartItemController.kt b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/IPartItemController.kt
new file mode 100644
index 00000000000..f6581e58291
--- /dev/null
+++ b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/IPartItemController.kt
@@ -0,0 +1,100 @@
+package com.github.qingmei2.exoplayer.multi_track.ui
+
+import android.content.Context
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import androidx.annotation.IntRange
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import com.github.qingmei2.exoplayer.multi_track.entity.SongPartItem
+import com.google.android.exoplayer2.ExoPlayer
+import com.google.android.exoplayer2.MediaItem
+import com.google.android.exoplayer2.SimpleExoPlayer
+import java.util.*
+
+interface IPartItemController : LifecycleEventObserver {
+
+ fun onBindItem(pos: Int, trackItem: SongPartItem, switchCallback: OnTrackSwitchChangedCallback)
+
+ fun isTrackOpen(): Boolean
+
+ fun setTrackOpen(isOpen: Boolean)
+
+ fun onSeek(progress: Int, byUser: Boolean = false)
+
+ fun onClickPlay()
+
+ fun onClickPause()
+}
+
+typealias OnTrackSwitchChangedCallback = (Boolean) -> Unit
+
+class SinglePartItemController(context: Context,
+ private val position: Int,
+ private val item: SongPartItem) : IPartItemController {
+
+
+ private val mExoPlayer: SimpleExoPlayer
+
+ private var mSwitchChangedCallback: OnTrackSwitchChangedCallback? = null
+
+ private var isTrackOpen: Boolean = true
+
+ private val mHandler = Handler(Looper.myLooper()!!)
+
+ init {
+ mExoPlayer = SimpleExoPlayer.Builder(context).build()
+ .apply {
+ addMediaItem(MediaItem.fromUri(Uri.parse(item.partPath)))
+ prepare()
+ }
+ }
+
+ override fun onBindItem(pos: Int,
+ trackItem: SongPartItem,
+ switchCallback: OnTrackSwitchChangedCallback) {
+ this.mSwitchChangedCallback = switchCallback
+ }
+
+ override fun setTrackOpen(isOpen: Boolean) {
+ isTrackOpen = isOpen
+ if (!mExoPlayer.isPlaying) {
+ mExoPlayer.play()
+ mSwitchChangedCallback?.invoke(true)
+ } else {
+ mExoPlayer.pause()
+ mSwitchChangedCallback?.invoke(false)
+ }
+ }
+
+ override fun isTrackOpen(): Boolean {
+ return isTrackOpen
+ }
+
+ override fun onSeek(@IntRange(from = 0, to = 100) progress: Int, byUser: Boolean) {
+ mExoPlayer.volume = progress / 100f
+ }
+
+ override fun onClickPlay() {
+ if (!mExoPlayer.isPlaying) {
+ mExoPlayer.play()
+ }
+ }
+
+ override fun onClickPause() {
+ if (mExoPlayer.isPlaying) {
+ mExoPlayer.pause()
+ }
+ }
+
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ when (event) {
+ Lifecycle.Event.ON_DESTROY -> {
+ mExoPlayer.stop()
+ mExoPlayer.release()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/MultiMusicActivity.kt b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/MultiMusicActivity.kt
new file mode 100644
index 00000000000..96e6daa57b5
--- /dev/null
+++ b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/MultiMusicActivity.kt
@@ -0,0 +1,154 @@
+package com.github.qingmei2.exoplayer.multi_track.ui
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.SeekBar
+import androidx.appcompat.app.AppCompatActivity
+import com.github.qingmei2.exoplayer.multi_track.R
+import com.github.qingmei2.exoplayer.multi_track.common.SimpleSeekBarListener
+import com.github.qingmei2.exoplayer.multi_track.utils.DemoDataSources
+import com.google.android.exoplayer2.DefaultRenderersFactory
+import com.google.android.exoplayer2.ExoPlayer
+import com.google.android.exoplayer2.SimpleExoPlayer
+import com.google.android.exoplayer2.ui.StyledPlayerView
+import java.util.*
+
+/**
+ * 页面同时播放多首音乐
+ */
+class MultiMusicActivity : AppCompatActivity() {
+
+ companion object Launcher {
+
+ fun launch(context: Context) {
+ val intent = Intent(context, MultiMusicActivity::class.java)
+ context.startActivity(intent)
+ }
+ }
+
+ private lateinit var mBtnPlayTrack1: View
+ private lateinit var mBtnPlayTrack2: View
+
+ private lateinit var mSeekBar1: SeekBar
+ private lateinit var mSeekBar2: SeekBar
+
+ private lateinit var mExoPlayer1: ExoPlayer
+ private lateinit var mExoPlayer2: ExoPlayer
+
+ private lateinit var mExoVideoView: StyledPlayerView
+ private lateinit var mExoVideoPlayer: ExoPlayer
+
+ private var mTimer: Timer? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_multi_player)
+
+ mBtnPlayTrack1 = findViewById(R.id.btn_play_track1)
+ mBtnPlayTrack2 = findViewById(R.id.btn_play_track2)
+
+ mSeekBar1 = findViewById(R.id.seek_bar_01)
+ mSeekBar2 = findViewById(R.id.seek_bar_02)
+
+ mExoVideoView = findViewById(R.id.player_view)
+
+ initVideoPlayer()
+ initAudioPlayer()
+ initSeekbars()
+ }
+
+ private fun initVideoPlayer() {
+ mExoVideoPlayer = SimpleExoPlayer.Builder(this).build()
+ .apply {
+ mExoVideoView.player = this
+ addMediaItem(DemoDataSources.Assets.SUNNY_MP4)
+ prepare()
+ }
+ }
+
+ private fun initAudioPlayer() {
+ mExoPlayer1 = SimpleExoPlayer.Builder(this, DefaultRenderersFactory(this)).build()
+ .apply {
+ addMediaItem(DemoDataSources.Assets.SUMMER_MP3)
+ prepare()
+ }
+
+ mExoPlayer2 = SimpleExoPlayer.Builder(this, DefaultRenderersFactory(this)).build()
+ .apply {
+ addMediaItem(DemoDataSources.Assets.HALF_MOON_MP3)
+ prepare()
+ }
+
+ mBtnPlayTrack1.setOnClickListener {
+ if (mExoPlayer1.isPlaying) {
+ mExoPlayer1.pause()
+ } else {
+ mExoPlayer1.play()
+ }
+ }
+
+ mBtnPlayTrack2.setOnClickListener {
+ if (mExoPlayer2.isPlaying) {
+ mExoPlayer2.pause()
+ } else {
+ mExoPlayer2.play()
+ }
+ }
+ }
+
+ private fun initSeekbars() {
+ mSeekBar1.setOnSeekBarChangeListener(SimpleSeekBarListener(mExoPlayer1))
+ mSeekBar2.setOnSeekBarChangeListener(SimpleSeekBarListener(mExoPlayer2))
+ }
+
+ private fun startTimer() {
+ stopTimer()
+ mTimer = Timer().apply {
+ scheduleAtFixedRate(object : TimerTask() {
+ override fun run() {
+ mSeekBar1.post {
+ val durationSec = mExoPlayer1.duration / 1000L
+ val curDurationSec = mExoPlayer1.currentPosition / 1000L
+ mSeekBar1.max = durationSec.toInt()
+ mSeekBar1.progress = curDurationSec.toInt()
+ }
+
+ mSeekBar2.post {
+ val durationSec1 = mExoPlayer2.duration / 1000L
+ val curDurationSec1 = mExoPlayer2.currentPosition / 1000L
+ mSeekBar2.max = durationSec1.toInt()
+ mSeekBar2.progress = curDurationSec1.toInt()
+ }
+ }
+ }, 1000L, 1000L)
+ }
+ }
+
+ private fun stopTimer() {
+ mTimer?.apply {
+ cancel()
+ purge()
+ }
+ mTimer = null
+ }
+
+ override fun onResume() {
+ super.onResume()
+ startTimer()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ stopTimer()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mExoPlayer1.stop()
+ mExoPlayer1.release()
+ mExoPlayer2.stop()
+ mExoPlayer2.release()
+ }
+}
\ No newline at end of file
diff --git a/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/MultiTrackListAdapter.kt b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/MultiTrackListAdapter.kt
new file mode 100644
index 00000000000..dec00274026
--- /dev/null
+++ b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/MultiTrackListAdapter.kt
@@ -0,0 +1,133 @@
+package com.github.qingmei2.exoplayer.multi_track.ui
+
+import android.content.Context
+import android.graphics.Color
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.SeekBar
+import android.widget.TextView
+import androidx.annotation.MainThread
+import androidx.collection.ArrayMap
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.github.qingmei2.exoplayer.multi_track.R
+import com.github.qingmei2.exoplayer.multi_track.entity.SongPartItem
+import com.google.android.material.switchmaterial.SwitchMaterial
+
+class MultiTrackListAdapter(
+ diffUtil: DiffUtil.ItemCallback = MultiTrackListDiffUtil
+) : ListAdapter(diffUtil), LifecycleEventObserver {
+
+ private val mTrackControllers = ArrayMap()
+
+ @MainThread
+ fun bindItems(items: List) {
+ this.submitList(items)
+ }
+
+ @MainThread
+ fun onClickPlay() {
+ mTrackControllers.forEach { entry ->
+ entry.value.onClickPlay()
+ }
+ }
+
+ @MainThread
+ fun onClickPause() {
+ mTrackControllers.forEach { entry ->
+ entry.value.onClickPause()
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MultiTrackListViewHolder {
+ return LayoutInflater.from(parent.context)
+ .inflate(R.layout.layout_listitem_song_track, parent, false)
+ .let { MultiTrackListViewHolder(it) }
+ }
+
+ override fun onBindViewHolder(holder: MultiTrackListViewHolder, position: Int) {
+ val item = getItem(position)
+ val controller = getTrackController(holder.itemView.context, position, item)
+ holder.binds(position, item, controller)
+ }
+
+ private fun getTrackController(context: Context, pos: Int, item: SongPartItem): IPartItemController {
+ return when (mTrackControllers[pos] == null) {
+ true -> {
+ SinglePartItemController(context, pos, item).also {
+ mTrackControllers[pos] = it
+ }
+ }
+ false -> mTrackControllers[pos]!!
+ }
+ }
+
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ mTrackControllers.forEach { entry ->
+ entry.value.onStateChanged(source, event)
+ }
+ }
+}
+
+private object MultiTrackListDiffUtil : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: SongPartItem, newItem: SongPartItem): Boolean {
+ return oldItem.partId == newItem.partId
+ }
+
+ override fun areContentsTheSame(oldItem: SongPartItem, newItem: SongPartItem): Boolean {
+ return oldItem == newItem
+ }
+}
+
+class MultiTrackListViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+
+ private val seekbar: SeekBar
+ private val trackName: TextView
+ private val trackSwitch: SwitchMaterial
+
+ init {
+ seekbar = view.findViewById(R.id.seek_bar)
+ trackName = view.findViewById(R.id.tv_track_name)
+ trackSwitch = view.findViewById(R.id.swt_track)
+ }
+
+ fun binds(pos: Int, trackItem: SongPartItem,
+ controller: IPartItemController) {
+ val isTrackOpen = controller.isTrackOpen()
+ this.onTrackOpenChanged(isTrackOpen)
+
+ trackName.text = trackItem.partName
+ trackSwitch.isChecked = isTrackOpen
+ trackSwitch.setOnCheckedChangeListener { _, isChecked ->
+ controller.setTrackOpen(isChecked)
+ }
+
+ seekbar.progress = 100
+ seekbar.max = 100
+ seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(seekBar: SeekBar, progress: Int, byUser: Boolean) {
+ controller.onSeek(progress, byUser)
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar) {
+ }
+
+ override fun onStopTrackingTouch(seekBar: SeekBar) {
+ }
+ })
+
+ val switchCallback: OnTrackSwitchChangedCallback = this::onTrackOpenChanged
+
+ controller.onBindItem(pos, trackItem, switchCallback)
+ }
+
+ private fun onTrackOpenChanged(isTrackOpen: Boolean) {
+ trackName.setTextColor(if (isTrackOpen) Color.BLACK else Color.GRAY)
+ }
+}
\ No newline at end of file
diff --git a/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/MultiTrackMainActivity.kt b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/MultiTrackMainActivity.kt
new file mode 100644
index 00000000000..fb8107e37d1
--- /dev/null
+++ b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/ui/MultiTrackMainActivity.kt
@@ -0,0 +1,60 @@
+package com.github.qingmei2.exoplayer.multi_track.ui
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.widget.Button
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.github.qingmei2.exoplayer.multi_track.MainActivity
+import com.github.qingmei2.exoplayer.multi_track.R
+import com.github.qingmei2.exoplayer.multi_track.utils.DemoDataSources
+import java.util.*
+
+/**
+ * 同时播放单曲多个音轨文件
+ */
+class MultiTrackMainActivity : AppCompatActivity() {
+
+ companion object Launcher {
+
+ fun launch(context: Context) {
+ val intent = Intent(context, MultiTrackMainActivity::class.java)
+ context.startActivity(intent)
+ }
+ }
+
+ private lateinit var mRecyclerView: RecyclerView
+ private lateinit var mListAdapter: MultiTrackListAdapter
+
+ private lateinit var mBtnPlay: Button
+ private lateinit var mBtnPause: Button
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_multi_track)
+
+ mRecyclerView = findViewById(R.id.recyclerview)
+ mBtnPlay = findViewById(R.id.btn_play)
+ mBtnPause = findViewById(R.id.btn_pause)
+
+ initList()
+
+ mBtnPlay.setOnClickListener { mListAdapter.onClickPlay() }
+ mBtnPause.setOnClickListener { mListAdapter.onClickPause() }
+ }
+
+ private fun initList() {
+ val songItem = DemoDataSources.Assets.getSongs(this)[0]
+
+ mListAdapter = MultiTrackListAdapter().apply {
+ mRecyclerView.adapter = this
+ mRecyclerView.layoutManager = LinearLayoutManager(this@MultiTrackMainActivity)
+
+ lifecycle.addObserver(this)
+
+ bindItems(songItem.partItems)
+ }
+ }
+}
\ No newline at end of file
diff --git a/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/utils/DemoDataSources.kt b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/utils/DemoDataSources.kt
new file mode 100644
index 00000000000..c6340f6fd49
--- /dev/null
+++ b/demos/multi-track/src/main/java/com/github/qingmei2/exoplayer/multi_track/utils/DemoDataSources.kt
@@ -0,0 +1,56 @@
+package com.github.qingmei2.exoplayer.multi_track.utils
+
+import android.content.Context
+import android.content.res.AssetManager
+import android.net.Uri
+import com.github.qingmei2.exoplayer.multi_track.entity.SongItem
+import com.google.android.exoplayer2.MediaItem
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import java.io.BufferedReader
+import java.io.IOException
+import java.io.InputStreamReader
+
+object DemoDataSources {
+
+ object Assets {
+
+ private const val SUMMER_MP3_URL = "asset:///media/summer.mp3"
+ private const val HALF_MOON_MP3_URL = "asset:///media/half_moon.mp3"
+
+ private const val SUNNY_MP4_URL = "asset:///media/sunny.mp4"
+
+ // 菊次郎的夏天.mp3
+ val SUMMER_MP3: MediaItem
+ get() = MediaItem.fromUri(Uri.parse(SUMMER_MP3_URL))
+
+ // 月半小夜曲.mp3
+ val HALF_MOON_MP3: MediaItem
+ get() = MediaItem.fromUri(Uri.parse(HALF_MOON_MP3_URL))
+
+ // 晴天.mp4
+ val SUNNY_MP4: MediaItem
+ get() = MediaItem.fromUri(Uri.parse(SUNNY_MP4_URL))
+
+ fun getSongs(context: Context): List {
+ val stringBuilder = StringBuilder()
+ //获得assets资源管理器
+ val assetManager: AssetManager = context.assets
+ //使用IO流读取json文件内容
+ try {
+ val bufferedReader = BufferedReader(InputStreamReader(
+ assetManager.open("media/songs.json"), "utf-8"))
+ var line: String?
+ while (bufferedReader.readLine().also { line = it } != null) {
+ stringBuilder.append(line)
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ val json = stringBuilder.toString()
+
+ val type = object : TypeToken>() {}.type
+ return Gson().fromJson(json, type)
+ }
+ }
+}
\ No newline at end of file
diff --git a/demos/multi-track/src/main/res/layout/activity_main.xml b/demos/multi-track/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000000..0956ead8f90
--- /dev/null
+++ b/demos/multi-track/src/main/res/layout/activity_main.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/multi-track/src/main/res/layout/activity_multi_player.xml b/demos/multi-track/src/main/res/layout/activity_multi_player.xml
new file mode 100644
index 00000000000..a595838e156
--- /dev/null
+++ b/demos/multi-track/src/main/res/layout/activity_multi_player.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/multi-track/src/main/res/layout/activity_multi_track.xml b/demos/multi-track/src/main/res/layout/activity_multi_track.xml
new file mode 100644
index 00000000000..8ddce1d6fca
--- /dev/null
+++ b/demos/multi-track/src/main/res/layout/activity_multi_track.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/multi-track/src/main/res/layout/layout_listitem_song_track.xml b/demos/multi-track/src/main/res/layout/layout_listitem_song_track.xml
new file mode 100644
index 00000000000..94219e347d7
--- /dev/null
+++ b/demos/multi-track/src/main/res/layout/layout_listitem_song_track.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/multi-track/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/multi-track/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000000..adaa93220eb
Binary files /dev/null and b/demos/multi-track/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/multi-track/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/multi-track/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..223ec8bd113
Binary files /dev/null and b/demos/multi-track/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/multi-track/src/main/res/values-night/themes.xml b/demos/multi-track/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000000..fcc4e789d0a
--- /dev/null
+++ b/demos/multi-track/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/demos/multi-track/src/main/res/values/colors.xml b/demos/multi-track/src/main/res/values/colors.xml
new file mode 100644
index 00000000000..09837df62f4
--- /dev/null
+++ b/demos/multi-track/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/demos/multi-track/src/main/res/values/strings.xml b/demos/multi-track/src/main/res/values/strings.xml
new file mode 100644
index 00000000000..49531f1ed79
--- /dev/null
+++ b/demos/multi-track/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ MultiTrack
+
\ No newline at end of file
diff --git a/demos/multi-track/src/main/res/values/themes.xml b/demos/multi-track/src/main/res/values/themes.xml
new file mode 100644
index 00000000000..20bce03d13a
--- /dev/null
+++ b/demos/multi-track/src/main/res/values/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/extensions/multi-track/.gitignore b/extensions/multi-track/.gitignore
new file mode 100644
index 00000000000..42afabfd2ab
--- /dev/null
+++ b/extensions/multi-track/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/extensions/multi-track/build.gradle b/extensions/multi-track/build.gradle
new file mode 100644
index 00000000000..577ddc781f3
--- /dev/null
+++ b/extensions/multi-track/build.gradle
@@ -0,0 +1,47 @@
+plugins {
+ id 'org.jetbrains.kotlin.android'
+}
+apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
+
+android {
+ compileSdkVersion 32
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 32
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+ext {
+ releaseArtifact = 'extension-multi-track'
+ releaseDescription = 'Multi-track extension for ExoPlayer.'
+}
+
+dependencies {
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'androidx.appcompat:appcompat:1.3.0'
+
+ implementation project(modulePrefix + 'library-core')
+
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}
diff --git a/extensions/multi-track/proguard-rules.pro b/extensions/multi-track/proguard-rules.pro
new file mode 100644
index 00000000000..481bb434814
--- /dev/null
+++ b/extensions/multi-track/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/extensions/multi-track/src/androidTest/java/com/github/qingmei2/exoplayer/multi_track/ExampleInstrumentedTest.kt b/extensions/multi-track/src/androidTest/java/com/github/qingmei2/exoplayer/multi_track/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000000..a32d33ee263
--- /dev/null
+++ b/extensions/multi-track/src/androidTest/java/com/github/qingmei2/exoplayer/multi_track/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.github.qingmei2.exoplayer.multi_track
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.github.qingmei2.exoplayer.multi_track", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/extensions/multi-track/src/main/AndroidManifest.xml b/extensions/multi-track/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..1cc62c85a98
--- /dev/null
+++ b/extensions/multi-track/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/extensions/multi-track/src/main/res/values/strings.xml b/extensions/multi-track/src/main/res/values/strings.xml
new file mode 100644
index 00000000000..e5f8fdc2aa9
--- /dev/null
+++ b/extensions/multi-track/src/main/res/values/strings.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/extensions/multi-track/src/test/java/com/github/qingmei2/exoplayer/multi_track/ExampleUnitTest.kt b/extensions/multi-track/src/test/java/com/github/qingmei2/exoplayer/multi_track/ExampleUnitTest.kt
new file mode 100644
index 00000000000..c7e3234708b
--- /dev/null
+++ b/extensions/multi-track/src/test/java/com/github/qingmei2/exoplayer/multi_track/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.github.qingmei2.exoplayer.multi_track
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 946b5b78de8..e220072be63 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -22,11 +22,13 @@ include modulePrefix + 'demo'
include modulePrefix + 'demo-cast'
include modulePrefix + 'demo-gl'
include modulePrefix + 'demo-surface'
+include modulePrefix + 'demo-multi-track'
include modulePrefix + 'playbacktests'
project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main')
project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast')
project(modulePrefix + 'demo-gl').projectDir = new File(rootDir, 'demos/gl')
project(modulePrefix + 'demo-surface').projectDir = new File(rootDir, 'demos/surface')
+project(modulePrefix + 'demo-multi-track').projectDir = new File(rootDir, 'demos/multi-track')
project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests')
apply from: 'core_settings.gradle'