Skip to content

🔥M3u/M3u8/Tv/网络电视应用/Tv application

License

Notifications You must be signed in to change notification settings

blog2i2j/bytebyte6.._..Tv

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Android M3u-Tv 播放器

一款播放IpTv资源的播放器,导入M3u格式的文件,支持下载点播文件。

项目记录了一些框架和技术的用法,如ExoPlayer、Gradle Kotlin Dsl、Koin...

使用Mvvm-UseCase架构,Kotlin为开发语言,Room数据库,RxJava异步框架,Koin依赖注入。

使用Material Design 组件开发界面,构建过渡动画。

使用Kotlin Dsl 管理依赖

项目地址:gitee github

直播源推荐

Download apk

功能

  • 首页-按国家地区、语言、类型浏览直播源-搜索
  • 导入-解析M3u文件-删除列表
  • 收藏-取消收藏
  • 下载-缓存点播文件-删除缓存
  • 设置
  • 关于
  • 直播源播放

界面概览

数据层

1、什么是M3u文件

M3u文件是纯文本文件,文件里保存这资源的地址,以#EXTINF开头,逗号后面是资源的名称(如下面,AM,资源的名称就是AM),紧接着下一行是资源的地址,以下是Github上一个开源的iptv资源文件,比正常m3u文件多了一些信息(tvg-name、tvg-logo...)。

#EXTM3U
#EXTINF:-1 tvg-id="" tvg-name="AM" tvg-country="AD" tvg-language="Spanish" tvg-logo="https://graph.facebook.com/rtva.andorra/picture?width=320&height=320" group-title="",AM
https://videos.rtva.ad/live/am/playlist.m3u8
#EXTINF:-1 tvg-id="" tvg-name="RNA" tvg-country="AD" tvg-language="Spanish" tvg-logo="https://graph.facebook.com/rtva.andorra/picture?width=320&height=320" group-title="",RNA
http://videos.rtva.ad:1935/live/rna/playlist.m3u8
#EXTINF:-1 tvg-id="" tvg-name="" tvg-country="AD" tvg-language="Catalan" tvg-logo="https://i.imgur.com/kJCjeQ4.png" group-title="",RTVA (720p)
http://videos.rtva.ad:1935/live/web/playlist.m3u8

2、如何解析M3u文件

利用#EXTINF分割字符串

利用\n分割字符串(有一些文件是\r\n,要替换为\n)

private fun getM3uString(byteArray: ByteArray): String {
    return byteArray.commonToUtf8String()
        .removePrefix("#EXTM3U")
        .replace("\r\n", "\n")
        .trim()
}

利用正则表达式解析tvg属性

//获取logo的正则表达式
private const val logoPattern = "(?<=tvg-logo=\").*?(?=\")"
private val logoRegex = Regex(logoPattern)

具体实现 M3u.kt

3、Room实体定义

@Parcelize
@Entity
@Keep
data class Tv(
    @PrimaryKey(autoGenerate = true)
    var tvId: Long = 0,
    var url: String = "",
    var category: String = "",
    var logo: String = "",
    var name: String = "",
    var favorite: Boolean = false,
    var language: String = "",
    var countryId: Long = 0,
    var countryName: String = "",
    var countryCode: String = ""
) : Parcelable

4、M3u.kt单元测试

@Test
fun test_m3u_parse() {
    // ..代表当前目录的父目录
    // 父目录下的channels文件夹,里面包含用于测试的m3u格式的文件
    val modelDir = File("..", "channels")
    val listFiles = modelDir.listFiles()
    listFiles?.forEach { file ->
        val tvs = M3u.getTvs(file)
        println("${file.name} size = ${tvs.size}")
        tvs.forEach {
            assert(it.name.isNotEmpty())
            assert(it.countryCode.isNotEmpty())
            assert(it.url.isNotEmpty())
            assert(it.language.isNotEmpty())
            assert(it.category.isNotEmpty())
        }
    }
    println("Total File: = ${listFiles?.size}")
}

5、Room实体关系映射

6、Room Dao定义

7、DataManager的定义,提供数据的增删改查方法

所有数据操作交给DataManager处理

interface DataManager {
    //User
    fun insertUser(user: User): Long
    fun insertUser(users: List<User>): List<Long>
    fun deleteUser(user: User)
    fun updateUser(user: User)
    fun getCurrentUserIfNotExistCreate(): User
    fun hasUser(): Boolean
    fun user(): LiveData<User>
    fun getUsers(): List<User>

    //Language
    fun insertLanguages(languages: List<Language>)
    fun getLangCount(): Int
    
    //...
}

8、Koin依赖注入

val dataModule = module {
    // 提供DataManager的单例
    single<DataManager> { DataManagerImpl(get(AppDatabase::class)) }
}

9、单元测试

对DataManager的单元测试

多对多关系测试

增删改查测试

UseCase层

1、什么是UseCase

  • 替ViewModel分担逻辑,减少ViewModel层级代码。
  • 如请求一个接口是一个UseCase,收藏一个直播源也是,解析m3u文件也是。
  • 有点像Runnable的意思,处理某个任务,或者用户的某个操作。

2、RxUseCase抽象和Result密封类

RxUseCase就是使用RxJava的方式去实现UseCase

abstract class RxUseCase<I, O> {

    // UseCase的结果 
    // Success Error Loading
    private val result: MutableLiveData<Result<O>> = MutableLiveData()

    fun result(): LiveData<Result<O>> = result

    // 提交一个UseCase
    fun execute(param: I): Single<O> = Single.create<O> {
        try {
            result.postValue((Result.Loading()))
            val o = run(param)
            result.postValue((Result.Success(o)))
            it.onSuccess(o)
        } catch (e: Exception) {
            result.postValue((Result.Error(e)))
            it.onError(e)
        }
    }
	
    abstract fun run(param: I): O
}
sealed class Result<out R> {
    // 代表这个结果是否已被处理,一些UI事件会用到这个字段,如showDialog,showToast,showSnackbar
    var handled: Boolean = false

    data class Success<out T>(
        val data: T,
        // 加载更多的情况下使用表示数据全部加载完成,用于加载列表数据的情况,其他情况忽略
        val end: Boolean = false
    ) : Result<T>()

    data class Error(
        val error: Throwable
    ) : Result<Nothing>()

    object Loading : Result<Nothing>()
}

反复执行某个任务的UseCase

abstract class IntervalUseCase<I,O> : RxUseCase<I,O>(){
    fun interval(param: I, period: Long = 2): Observable<Long> =
        Observable.interval(period, TimeUnit.SECONDS)
            .doOnNext {
                val o = run(param)
                result.postValue((Result.Success(o)))
            }.doOnError {
                result.postValue((Result.Error(it)))
            }
}

3、UseCase的单元测试

UseCase的param参数在设计的时候要考虑到测试用例的编写,比如解析M3u文件的时候,用户选择了一个m3u文件并返回了一个Uri,不要将这个Uri直接作为参数,应当定义一个data class,如:

ParseM3uUseCase.kt

@Keep
data class ParseM3uParam(
    // 用户选择后获得的Uri
    val uri: Uri? = null,
    // assets文件夹下的文件名,创建Firebase测试版本apk的时候会打包一个index.m3u的文件到apk中
    val assetsFileName: String? = null,
    // 只在测试中使用到
    val forTest: List<Tv>? = null
)

ViewModel层

1、提交RxUseCase

定义BaseViewModel

abstract class BaseViewModel : ViewModel() {

    private val compositeDisposable = CompositeDisposable()

    protected fun addDisposable(disposable: Disposable) {
        compositeDisposable.add(disposable)
    }

    // 清理资源
    override fun onCleared() {
        super.onCleared()
        compositeDisposable.dispose()
    }
}
// 提交一个UseCase
fun loadDownloadList() {
    // Unit意味不需要参数
    // onIo为Single的扩展函数,订阅在Io线程
    addDisposable(downloadListUseCase.execute(Unit).onIo())
}

fun <T> Single<T>.onIo(): Disposable {
    return subscribeOn(Schedulers.io()).subscribe()
}

2、暴露LiveData

// Success、Error、Loading
val downloadListResult = downloadListUseCase.result()

View层

1、观察LiveData

viewModel.downloadListResult.observe(viewLifecycleOwner, Observer { result ->
    // emit函数                                                          
    result.emit({
        downloadAdapter.submitList(it.data)
        hideSwipeRefresh()
    }, {
        hideSwipeRefresh()
        view.longSnack(it.error.message.toString())
    }, {
        showSwipeRefresh()
    })
})

/**
 * Result 搭配 LiveData使用时,当配置更改或其他情况会导致LiveData重新订阅,所以定义handled变量来处理
 * 这种情况,一次性的事件,但是success的情况是有例外的,比如展示数据(在配置更改后,数据要继续显示,
 * Ui事件不需再次执行),所以应该按情况调用runIfNotHandled()
 * @param success 可能会执行多次
 * @param error 一次
 * @param loading 一次
 */
inline fun <T> Result<T>.emit(
    success: ((s: Result.Success<T>) -> Unit),
    error: ((e: Result.Error) -> Unit),
    loading: ((l: Result.Loading) -> Unit)
) {
    when (this) {
        is Result.Success -> success(this)
        is Result.Error -> runIfNotHandled { error(this) }
        is Result.Loading -> runIfNotHandled { loading(this) }
    }
}

2、设置点击事件

3、Material Design 过渡动画

4、界面测试

  • 测试按钮是否可点击,导航键是否可点击
  • 测试标题是否显示或是否显示正确
  • 测试收藏按钮是否显示,是否可点击,点击后预期值和实际值是否正确
  • 跨应用测试,如导入m3u文件
  • UI测试例子

RxJava

RxUseCase

ExoPlayer

基本用法

Material Design

过渡动画、MotionLayout

Room

一对多关系,多对多关系映射,fts全文检索,单元测试

网络状态监听

NetworkHelper.kt

Kotlin Gradle DSL

依赖管理

Kotlin作用域函数

作用域函数

自定义apk名称

APK-Name

About

🔥M3u/M3u8/Tv/网络电视应用/Tv application

Resources

License

Stars

Watchers

Forks

Packages

No packages published