├── .gitignore ├── LICENSE ├── LICENSE-Apache ├── README.md ├── app_live ├── .gitignore ├── README.md ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── duqian │ │ └── app │ │ ├── MainActivity.kt │ │ ├── base │ │ ├── BaseActivity.kt │ │ ├── BaseApplication.kt │ │ └── BaseFragment.kt │ │ ├── helper │ │ ├── CommonUtils.kt │ │ ├── EventBusHelper.kt │ │ ├── PageRecordHelper.kt │ │ ├── ToastUtils.kt │ │ └── UEToolHelper.kt │ │ ├── live │ │ ├── base_comm │ │ │ ├── BaseController.kt │ │ │ ├── BaseDataBindingDialog.kt │ │ │ ├── BaseDialogFragment.kt │ │ │ ├── BaseEvent.kt │ │ │ ├── BaseLiveActivity.kt │ │ │ ├── BaseLiveFragment.kt │ │ │ ├── BaseRoomParams.kt │ │ │ └── RoomConstants.kt │ │ ├── controller │ │ │ ├── AnchorInfoController.kt │ │ │ ├── RoomChatBtnController.kt │ │ │ ├── RoomCommonController.kt │ │ │ └── RoomGiftBtnController.kt │ │ ├── fragment │ │ │ ├── PublicChatFragment.kt │ │ │ ├── RoomBottomBtnFragment.kt │ │ │ ├── RoomEffectFragment.kt │ │ │ ├── RoomMainFragment.kt │ │ │ ├── RoomTestFragment.kt │ │ │ └── dialog │ │ │ │ └── RoomGiftDialog.kt │ │ ├── liveroom │ │ │ ├── LiveRoomActivity.kt │ │ │ ├── controller │ │ │ │ ├── RoomCallBtnController.kt │ │ │ │ ├── RoomCoinsBtnController.kt │ │ │ │ └── RoomLivePlayerController.kt │ │ │ └── data │ │ │ │ ├── LiveRoomEvent.kt │ │ │ │ └── LiveRoomParams.kt │ │ ├── pushroom │ │ │ ├── PushRoomActivity.kt │ │ │ ├── controller │ │ │ │ ├── AgoraPusherController.kt │ │ │ │ ├── PushCameraController.kt │ │ │ │ └── PushMicBtnController.kt │ │ │ ├── data │ │ │ │ ├── PushRoomEvent.kt │ │ │ │ └── PushRoomParams.kt │ │ │ └── ui │ │ │ │ └── LivePreviewFragment.kt │ │ ├── repository │ │ │ ├── RoomApiImpl.kt │ │ │ └── RoomModule.kt │ │ ├── services │ │ │ ├── AutoServiceHelper.kt │ │ │ └── IRoomApi.kt │ │ ├── utils │ │ │ ├── FragmentUtils.kt │ │ │ ├── KotlinExtUtils.kt │ │ │ ├── LiveEventBus.kt │ │ │ └── UIUtil.kt │ │ └── viewmodel │ │ │ ├── GlobalViewModel.kt │ │ │ └── RoomGlobalViewModel.kt │ │ ├── main │ │ ├── MainFragment.kt │ │ └── MainViewModel.kt │ │ └── navigator │ │ ├── AppNavigator.kt │ │ ├── AppNavigatorImpl.kt │ │ └── NavigationModule.kt │ └── res │ ├── anim │ └── loading.xml │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_launcher_background.xml │ ├── shape_live_room_bg.xml │ ├── shape_live_top_anchor_bg.xml │ └── shape_push_start_live_bg.xml │ ├── layout │ ├── activity_live_push_room.xml │ ├── activity_live_room.xml │ ├── activity_main.xml │ ├── dialog_room_gift_layout.xml │ ├── fragment_liveroom_bottom_btns.xml │ ├── fragment_main.xml │ ├── fragment_pushroom_bottom_btns.xml │ ├── fragment_pushroom_preview.xml │ ├── fragment_room_gift_effect.xml │ ├── fragment_room_main.xml │ ├── fragment_room_public_chat.xml │ ├── fragment_room_test_layout.xml │ ├── include_liveroom_player.xml │ ├── include_pushroom_preview.xml │ ├── include_room_close_layout.xml │ ├── include_room_common_layout.xml │ ├── include_room_public_chat_layout.xml │ └── include_room_top_layout.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── bg_dq_live_splash.jpeg │ ├── bg_live_common_btn.webp │ ├── bg_live_pushroom_go_live.webp │ ├── bg_live_room_loading_default.webp │ ├── ic_launcher.webp │ ├── ic_launcher_round.webp │ ├── ic_live_close_room.webp │ ├── ic_live_pushroom_btn_mic_off.webp │ ├── ic_live_pushroom_btn_mic_on.webp │ ├── ic_live_room_anchor_follow.webp │ ├── ic_live_room_btn_call.png │ ├── ic_live_room_btn_chat.webp │ ├── ic_live_room_btn_coins.webp │ ├── ic_live_room_btn_coins_activity.webp │ ├── ic_live_room_btn_gift.webp │ ├── ic_live_room_hot_num.webp │ └── ic_live_room_loading_circle.webp │ ├── mipmap-xxxhdpi │ ├── bg_room_gift_shop.webp │ ├── ic_launcher.webp │ ├── ic_launcher_round.webp │ ├── ic_live_default_head_icon.webp │ ├── ic_live_default_icon.png │ └── ic_live_default_icon2.png │ ├── values-night │ └── themes.xml │ ├── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ ├── styles.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ ├── data_extraction_rules.xml │ ├── file_provider_paths.xml │ └── network_security_config.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib_livepusher ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── duqian │ └── live │ └── pusher │ ├── AgoraPusherImpl.kt │ ├── IBasePusher.kt │ ├── ILiveCallback.kt │ └── helper │ ├── CommonState.kt │ └── PusherConfigHelper.kt ├── lib_mediaplayer ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── duqian │ └── live │ └── mediaplayer │ ├── ILivePlayerService.kt │ ├── PlayState.kt │ ├── RtmpLivePlayer.kt │ └── SimpleLivePlayer.kt ├── nativelib ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── cpp │ ├── CMakeLists.txt │ └── nativelib.cpp │ └── java │ └── com │ └── duqian │ └── nativelib │ └── NativeLib.kt ├── screenshot ├── DQLive.mp4 ├── DQLive_Anchor.jpeg ├── DQLive_Landscape.png ├── DQLive_Main.png ├── DQLive_Room.jpeg └── DQLive_Splash.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Android 2 | *.iml 3 | .gradle 4 | /local.properties 5 | .idea/ 6 | /.idea/caches 7 | /.idea/libraries 8 | /.idea/modules.xml 9 | /.idea/workspace.xml 10 | /.idea/navEditor.xml 11 | /.idea/assetWizardSettings.xml 12 | .DS_Store 13 | /build 14 | /captures 15 | .externalNativeBuild 16 | .cxx 17 | 18 | # Built application files 19 | *.apk 20 | *.ap_ 21 | 22 | # Files for the Dalvik VM 23 | *.dex 24 | 25 | # Java class files 26 | *.class 27 | 28 | # Generated files 29 | bin/ 30 | gen/ 31 | 32 | # Gradle files 33 | .gradle/ 34 | build/ 35 | 36 | # Local configuration file (sdk path, etc) 37 | local.properties 38 | 39 | # Proguard folder generated by Eclipse 40 | proguard/ 41 | 42 | # Log Files 43 | *.log 44 | 45 | # Android Studio Navigation editor temp files 46 | .navigation/ 47 | 48 | # Android Studio captures folder 49 | captures/ 50 | 51 | conf*.json 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Agora.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### DQLive 2 | 3 | #### Android直播客户项目 4 | 5 | #### 直播应用实战!欢迎Star,持续更新。 6 | 7 | 8 | 基于声网SDK实现的推拉流,项目虽小,五脏俱全。 9 | 特色:优良的直播间的架构设计,观众端直播间和主播端,模块复用、可扩展性强,自定义生命周期感知的controller组件,实现按钮粒度的逻辑分离、自由组合。 10 | 11 | Github项目地址:[https://github.com/duqian291902259/DQLive](https://github.com/duqian291902259/DQLive) 12 | 13 | 仅供学习交流技术,应用的sdk、资源和图片素材,如有侵权请联系删除。 14 | 15 | ### 项目截图 16 | ![DQLive_Main.png](https://github.com/duqian291902259/DQLive/blob/main/screenshot/DQLive_Main.png) 17 | 18 | 视频效果:[DQLive.mp4](https://github.com/duqian291902259/DQLive/blob/main/screenshot/DQLive.mp4) 19 | 20 | ### 直播技术选型 21 | 22 | 1. 主播端:推流基于声网SDK,然后旁路推流到CDN,所以独特的地方是开播的流程和主播端的音视频处理。其他常规的功能跟观众端类似。 23 | 1. 观众端:拉流播放,可选的播放器有ijkplayer,exo,或者声网MPK;其他主播资料卡,IM功能,打赏功能。 24 | 1. UI架构:Activity+Fragment+ViewModle+LiveData 25 | 1. 自定义的controller(支持热插拔的模块化,生命周期感知) 26 | 1. 组件通信:目前EventBus,会新增LiveDataBus。 27 | 1. 模块化、组件化:AutoService。 28 | 1. 依赖注入:Hilt。 29 | 1. 网络相关:Retrofit + Kotlin Coroutine + Gson。 30 | 1. 持久化:Room,MMKV。 31 | 1. 动画:前期SVGA,后期会+Lottie、Alpha MP4。 32 | 1. 直播使用哪种技术,需要结合各自项目的技术特点,考虑项目同学的开发习惯和技术掌握程度。我也会逐步引入一些项目中没有使用过的,但是很Nice的技术。 33 | 34 | ### 注意事项 35 | 36 | * 直播间的层级,很重要! 37 | 38 | * 直播间的功能可以很多,设计、新增每一个模块,都必须考虑的尽可能兼顾高可用、稳定性、可拓展性、可复用性。 39 | 40 | * 基础组件下沉,做到面向接口编程,尽量模块化、组件化。 41 | 42 | * 方便项目功能模块热插拔,新项目快速复用或者删减。 43 | 44 | * 目前直播间涉及的Modules,很多在直播间和主播端复用。 45 | 46 | * 推拉流的模块LivePusher+MediaPlayer,都是做成单独的module工程,可以打aar,对外提供Service。 47 | 48 | ### 开发的原则和规范 49 | 50 | 规范,可以避免很多没有必要发生的问题,细节不一定决定成败,但可能会影响效率和心情。 51 | 52 | #### 命名规范 53 | 54 | * 各种文件、资源文件命名规范,能方便的检索到想要的内容,也方便以后模块化分离或者重构。 55 | 56 | * 比如图片,一般前缀ic_ ,bg_或者img_. +大模块名+功能名+状态(如果有) 57 | * 。比如ic_live_room_chat_normal.png,ic_push_room_gift_selected.png,大图bg_live_room_loading_default.png。 58 | 59 | * 通用的资源,cm_ 60 | * 布局常规的就还按照常规的来:activity_,fragment_,dialog_,layout_,item_ 61 | 62 | * 加上模块名+功能名,表意明确。 63 | 64 | * 一般做过插件化的话,资源名太普通或者太短都比较容易冲突。 65 | 66 | * 而规范的命名,做组件化、模块化的时候也比较容易分离资源。 67 | 68 | * 一般room表示直播间和开播端通用资源,pushroom才是开播端独有的。 69 | 70 | 71 | #### 开发约定 72 | 73 | 1. 直播间是一个单Activity的复杂业务载体,附加在里面的功能,一般都要继承直播间的基类开发,方便统一管理。 74 | 75 | 1. 属于直播间内部的新创建的弹窗,满屏或者半屏幕页面,不要使用Activity,继承BaseFragment,或者BaseModule开发。 76 | 77 | 1. 弹窗推荐使用BaseDataBindingDialog(DialogFragment),这样的话,方便子模块的统一管理,也不会因为activity的生命周期变化,影响了直播间的推拉流的逻辑。 78 | 79 | 1. 大的UI,请使用ViewStub懒加载布局。 80 | 81 | ### 其他 82 | 推拉流是正常的,只需要修改推拉流地址即可测试。 83 | 84 | 其实还有很多可以加入的功能,如AI主播,换脸等。后续持续更新。 85 | 86 | duqian2010@gmail.com 87 | -------------------------------------------------------------------------------- /app_live/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | build/ 3 | .idea/ -------------------------------------------------------------------------------- /app_live/README.md: -------------------------------------------------------------------------------- 1 | ### DQLive 2 | 3 | 基于声网SDK实现的推拉流,项目虽小,五脏俱全。 4 | 特色:优秀的直播间的架构设计,观众端直播间和主播端,模块复用、可扩展性强,自定义生命周期感知的controller组件,实现按钮粒度的逻辑分离、自由组合。 5 | 6 | ### 直播技术选型 7 | 8 | 主播端:推流基于声网SDK,然后旁路推流到CDN,所以独特的地方是开播的流程和主播端的音视频处理。其他常规的功能跟观众端类似。 9 | 观众端:拉流播放,可选的播放器有ijkplayer,exo,或者声网MPK;其他主播资料卡,IM功能,打赏功能。 10 | UI架构:Activity+Fragment+ViewModle+LiveData 11 | 自定义的controller(支持热插拔的模块化,生命周期感知) 12 | 组件通信:目前EventBus,会新增LiveDataBus。 13 | 模块化、组件化:AutoService。 14 | 依赖注入:Hilt。 15 | 网络相关:Retrofit + Kotlin Coroutine + Gson。 16 | 持久化:Room,MMKV。 17 | 动画:前期SVGA,后期会+Lottie、Alpha MP4。 18 | 直播使用哪种技术,需要结合各自项目的技术特点,考虑项目同学的开发习惯和技术掌握程度。我也会逐步引入一些项目中没有使用过的,但是很Nice的技术。 19 | 20 | ### 注意事项 21 | 22 | 直播间的层级,很重要!一般需要谨慎设计,不要随意改动,目前设计的符合大部分直播场景下的View层级处理。 23 | 房间内的数据如果要暴露给外部使用,可以用Application级别的GlobalViewModel。否则使用RoomGlobalViewModel。 24 | 直播间的功能可以很多,单个模块也可以做的很复杂很庞大(如礼物特效,视频播放器),所以设计、新增每一个模块,都必须考虑的尽可能兼顾高可用、稳定性、可拓展性、可复用性。 25 | 基础组件下沉,做到面向接口编程,尽量模块化、组件化,方便项目功能模块热插拔,新项目快速复用或者删减。 目前直播间涉及的Modules,很多在直播间和主播端复用。 26 | 推拉流的模块LivePusher+MediaPlayer,都是做成单独的module工程,可以打aar,对外提供Service。 27 | 28 | ### 开发的原则和规范 29 | 30 | 规范,可以避免很多没有必要发生的问题,细节不一定决定成败,但可能会影响效率和心情。 31 | 32 | #### 命名规范 33 | 34 | 各种文件、资源文件命名规范,能方便的检索到想要的内容,也方便以后模块化分离或者重构。 比如图片,一般前缀ic_ ,bg_或者img_. +大模块名+功能名+状态(如果有) 35 | 。比如ic_live_room_chat_normal.png,ic_push_room_gift_selected.png,大图bg_live_room_loading_default.png。 36 | 通用的资源,cm_ 37 | activity_,fragment_,dialog_,layout_,item_ 38 | ,这些布局常规的就还按照常规的来,加上模块名+功能名。要表意明确。一般做过插件化的话,资源名太普通或者太短都比较容易冲突。 39 | 而规范的命名,做组件化、模块化的时候也比较容易分离资源。如下图所示:一般room表示直播间和开播端通用资源,pushroom才是开播端独有的。 40 | image.png image.png 41 | 42 | #### 开发约定 43 | 44 | 直播间是一个单Activity的复杂业务载体,附加在里面的功能,一般都要继承直播间的基类开发,方便统一管理,享受通用逻辑的封装带来的丝滑开发体验。 45 | 属于直播间内部的新创建的弹窗,满屏或者半屏幕页面,不要使用Activity,继承BaseFragment,或者BaseModule开发, 46 | 弹窗推荐使用BaseDataBindingDialog(DialogFragment),这样的话,方便子模块的统一管理,也不会因为activity的生命周期变化,影响了直播间的推拉流的逻辑。 47 | 大的UI,请使用ViewStub懒加载布局。 48 | 49 | ### 其他 50 | 待更新维护。 51 | 52 | duqian2010@gmail.com 53 | Wechat:AndroidDQ,在路上 54 | -------------------------------------------------------------------------------- /app_live/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.android' 3 | id 'kotlin-android' 4 | id 'kotlin-android-extensions' 5 | id 'kotlin-kapt' 6 | id 'dagger.hilt.android.plugin' 7 | } 8 | if (isLiveModuleLib.toBoolean()) { 9 | apply plugin: 'com.android.library' 10 | } else { 11 | apply plugin: 'com.android.application' 12 | } 13 | 14 | ext { 15 | kotlin_version = '1.4.31' 16 | androidx_appcompat = '1.3.1' 17 | androidx_constraintlayout = '2.1.1' 18 | } 19 | 20 | android { 21 | compileSdk 32 22 | 23 | defaultConfig { 24 | applicationId "com.duqian.app.live" 25 | minSdk 23 26 | targetSdk 31 27 | versionCode 1 28 | versionName "1.0" 29 | } 30 | 31 | buildTypes { 32 | release { 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 35 | } 36 | } 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_1_8 39 | targetCompatibility JavaVersion.VERSION_1_8 40 | } 41 | kotlinOptions { 42 | jvmTarget = '1.8' 43 | } 44 | buildFeatures { 45 | viewBinding true 46 | } 47 | 48 | dataBinding { 49 | enabled = true 50 | } 51 | } 52 | 53 | dependencies { 54 | 55 | // androidx 56 | implementation 'androidx.core:core-ktx:1.6.0' 57 | implementation 'androidx.appcompat:appcompat:1.5.0' 58 | implementation 'com.google.android.material:material:1.6.1' 59 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 60 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' 61 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' 62 | //implementation 'androidx.lifecycle:lifecycle-common-java8:2.5.1' 63 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 64 | 65 | implementation project(':lib_mediaplayer') 66 | implementation project(':lib_livepusher') 67 | implementation project(path: ':nativelib') 68 | //autoService 69 | kapt 'com.google.auto.service:auto-service:1.0-rc6' 70 | implementation 'com.google.auto.service:auto-service:1.0-rc6' 71 | //mmkv 72 | implementation 'com.tencent:mmkv:1.2.14' 73 | //hilt 74 | implementation "com.google.dagger:hilt-android:2.40" 75 | kapt "com.google.dagger:hilt-android-compiler:2.40" 76 | //cameraX 77 | implementation "androidx.camera:camera-lifecycle:1.2.0-alpha04" 78 | implementation "androidx.camera:camera-view:1.2.0-alpha04" 79 | implementation "androidx.camera:camera-camera2:1.2.0-alpha04" 80 | // Room 81 | implementation "androidx.room:room-runtime:2.3.0" 82 | kapt "androidx.room:room-compiler:2.3.0" 83 | //event bus 84 | implementation 'org.greenrobot:eventbus:3.2.0' 85 | 86 | //statusBar 87 | implementation 'com.gyf.immersionbar:immersionbar:3.0.0' 88 | implementation 'com.gyf.immersionbar:immersionbar-components:3.0.0' 89 | implementation 'com.gyf.immersionbar:immersionbar-ktx:3.0.0' 90 | 91 | // 替换字体 92 | implementation 'io.github.inflationx:calligraphy3:3.1.1' 93 | implementation 'io.github.inflationx:viewpump:2.0.3' 94 | 95 | //easyPermissions 96 | implementation 'pub.devrel:easypermissions:3.0.0' 97 | 98 | //UETool 99 | debugImplementation 'com.github.eleme.UETool:uetool:1.3.4' 100 | debugImplementation 'com.github.eleme.UETool:uetool-base:1.3.4' 101 | releaseImplementation 'com.github.eleme.UETool:uetool-no-op:1.3.4' 102 | 103 | //LiveEventBus 104 | //implementation 'io.github.jeremyliao:live-event-bus-x:1.6.0' 105 | 106 | //recyclerView相关库 107 | //implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.46' 108 | //implementation 'com.google.code.gson:gson:2.9.0' 109 | 110 | //glide 111 | //api 'com.github.bumptech.glide:glide:4.13.1' 112 | //annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' 113 | //日志 114 | //api 'com.elvishew:xlog:1.6.1' 115 | } -------------------------------------------------------------------------------- /app_live/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app_live/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 42 | 43 | 51 | 52 | 53 | 61 | 62 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app 2 | 3 | import android.Manifest 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.Window 7 | import android.view.WindowManager 8 | import com.duqian.app.base.BaseActivity 9 | import com.duqian.app.live.R 10 | import com.duqian.app.helper.CommonUtils 11 | import com.duqian.app.helper.ToastUtils 12 | import com.duqian.app.live.base_comm.RoomConstants 13 | import com.duqian.app.main.MainFragment 14 | import com.duqian.app.navigator.AppNavigator 15 | import com.duqian.nativelib.NativeLib 16 | import dagger.hilt.android.AndroidEntryPoint 17 | import pub.devrel.easypermissions.EasyPermissions 18 | import javax.inject.Inject 19 | 20 | /** 21 | * Description:首页 22 | * 23 | * Created by 杜乾 on 2022/8/08 - 07:02. 24 | * E-mail: duqian2010@gmail.com 25 | */ 26 | @AndroidEntryPoint 27 | class MainActivity : BaseActivity(), EasyPermissions.PermissionCallbacks, 28 | EasyPermissions.RationaleCallbacks { 29 | 30 | @Inject 31 | lateinit var navigator: AppNavigator 32 | 33 | lateinit var mainFragment: MainFragment 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | this.requestWindowFeature(Window.FEATURE_NO_TITLE) 36 | this.window.setFlags( 37 | WindowManager.LayoutParams.FLAG_FULLSCREEN, 38 | WindowManager.LayoutParams.FLAG_FULLSCREEN 39 | ) 40 | super.onCreate(savedInstanceState) 41 | setContentView(R.layout.activity_main) 42 | } 43 | 44 | private fun requirePermission() { 45 | if (!CommonUtils.checkSelfPermission( 46 | this, 47 | Manifest.permission.READ_EXTERNAL_STORAGE, 48 | Manifest.permission.WRITE_EXTERNAL_STORAGE, 49 | ) 50 | ) { 51 | EasyPermissions.requestPermissions( 52 | this, 53 | getString(R.string.text_external_storage_permission_rationale), 54 | RoomConstants.REQUEST_CODE_EXTERNAL_STORAGE, 55 | Manifest.permission.READ_EXTERNAL_STORAGE, 56 | Manifest.permission.WRITE_EXTERNAL_STORAGE, 57 | ) 58 | } 59 | } 60 | 61 | override fun getLayoutId(): Int { 62 | return R.layout.activity_main 63 | } 64 | 65 | override fun initData() { 66 | initMainFragment() 67 | 68 | requirePermission() 69 | 70 | testNative() 71 | } 72 | 73 | private fun testNative() { 74 | ToastUtils.show("Native:${NativeLib().stringFromJNI()}") 75 | } 76 | 77 | override fun isStatusBarDark(): Boolean { 78 | return false 79 | } 80 | 81 | private fun initMainFragment() { 82 | mainFragment = MainFragment.newInstance() 83 | supportFragmentManager.beginTransaction() 84 | .replace(R.id.container, mainFragment) 85 | .commitNow() 86 | } 87 | 88 | override fun onRequestPermissionsResult( 89 | requestCode: Int, 90 | permissions: Array, 91 | grantResults: IntArray 92 | ) { 93 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 94 | EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) 95 | } 96 | 97 | override fun onPermissionsGranted(requestCode: Int, perms: MutableList) { 98 | if (requestCode == RoomConstants.REQUEST_CODE_CAMERA_AND_AUDIO) { 99 | Log.d(TAG, "Granted camera permission....") 100 | mainFragment.startLiveActivity() 101 | } 102 | } 103 | 104 | override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { 105 | ToastUtils.show("no camera permission") 106 | } 107 | 108 | override fun onRationaleAccepted(requestCode: Int) { 109 | } 110 | 111 | override fun onRationaleDenied(requestCode: Int) { 112 | } 113 | 114 | companion object { 115 | private const val TAG = "dq-main" 116 | } 117 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.base 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.os.Looper 7 | import android.view.* 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.duqian.app.helper.EventBusHelper 10 | import com.gyf.immersionbar.ktx.immersionBar 11 | import com.duqian.app.live.utils.LiveEventBus 12 | import io.github.inflationx.viewpump.ViewPumpContextWrapper 13 | 14 | /** 15 | * Description:Activity 基类 16 | * Created by 杜小菜 on 2022/8/9 - 19:42. 17 | * E-mail: duqian2010@gmail.com 18 | */ 19 | abstract class BaseActivity : AppCompatActivity() { 20 | 21 | val mHandler = Handler(Looper.getMainLooper()) 22 | lateinit var mRootView: View 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | handleUnmarshalling(savedInstanceState) 25 | super.onCreate(savedInstanceState) 26 | val root = getRootView() 27 | when { 28 | root != null -> { 29 | this.mRootView = root 30 | } 31 | else -> { 32 | val layoutId = getLayoutId() 33 | this.mRootView = LayoutInflater.from(this).inflate(layoutId, null) 34 | } 35 | } 36 | setContentView(mRootView) 37 | EventBusHelper.bind(this) 38 | initConfig() 39 | initData() 40 | initView() 41 | initListener() 42 | observerData() 43 | } 44 | 45 | override fun finish() { 46 | super.finish() 47 | EventBusHelper.unbind(this) 48 | mHandler.removeCallbacksAndMessages(null) 49 | //确保去除粘性事件 50 | LiveEventBus.get().clear() 51 | } 52 | 53 | override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { 54 | return super.dispatchTouchEvent(ev) 55 | } 56 | 57 | private fun handleUnmarshalling(savedInstanceState: Bundle?) { 58 | try { 59 | savedInstanceState?.classLoader = this.javaClass.classLoader 60 | } catch (t: Throwable) { 61 | t.printStackTrace() 62 | } 63 | } 64 | 65 | open fun initConfig() { 66 | immersionBar { 67 | transparentStatusBar() 68 | statusBarDarkFont(isStatusBarDark()) 69 | } 70 | } 71 | 72 | open fun isStatusBarDark(): Boolean { 73 | return true 74 | } 75 | 76 | abstract fun getLayoutId(): Int 77 | 78 | abstract fun initData() 79 | 80 | open fun initView() {} 81 | 82 | open fun initListener() {} 83 | 84 | open fun observerData() {} 85 | 86 | open fun getRootView(): View? { 87 | return null 88 | } 89 | 90 | override fun attachBaseContext(newBase: Context?) { 91 | if (newBase != null) { 92 | super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase)) 93 | } else { 94 | super.attachBaseContext(newBase) 95 | } 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/base/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.base 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import com.duqian.app.helper.UEToolHelper 6 | import com.tencent.mmkv.MMKV 7 | import dagger.hilt.android.HiltAndroidApp 8 | import io.github.inflationx.calligraphy3.CalligraphyConfig 9 | import io.github.inflationx.calligraphy3.CalligraphyInterceptor 10 | import io.github.inflationx.viewpump.ViewPump 11 | 12 | /** 13 | * Description:Application基类 14 | * Created by 杜小菜 on 2022/8/9 - 18:22. 15 | * E-mail: duqian2010@gmail.com 16 | */ 17 | @HiltAndroidApp 18 | class BaseApplication : Application() { 19 | 20 | companion object { 21 | lateinit var instance: BaseApplication 22 | } 23 | 24 | var mmkv: MMKV? = null 25 | 26 | override fun onCreate() { 27 | super.onCreate() 28 | instance = this 29 | 30 | //Hilt 31 | //mmkv 32 | val rootDir = MMKV.initialize(this) 33 | mmkv = MMKV.defaultMMKV() 34 | Log.d("dq-app", "mmkv root: $rootDir.mmkv=$mmkv") 35 | 36 | //自定义字体 37 | initFonts() 38 | 39 | //dev工具,debug包使用,release包是空实现 40 | UEToolHelper.initUeTool(this) 41 | } 42 | 43 | private fun initFonts() { 44 | ViewPump.init( 45 | ViewPump.builder() 46 | .addInterceptor( 47 | CalligraphyInterceptor( 48 | CalligraphyConfig.Builder() 49 | .setDefaultFontPath("fonts/font_normal.ttf") 50 | //.setFontAttrId(R.attr.fontPath) 51 | .build() 52 | ) 53 | ).build() 54 | ) 55 | } 56 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.base 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.os.Looper 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.annotation.CallSuper 11 | import androidx.fragment.app.Fragment 12 | import androidx.fragment.app.FragmentActivity 13 | import com.duqian.app.helper.EventBusHelper 14 | import com.duqian.app.live.utils.LiveEventBus 15 | 16 | /** 17 | * Description:Fragment基类 18 | * Created by 杜乾 on 2022/8/10 - 15:00. 19 | * E-mail: duqian2010@gmail.com 20 | */ 21 | abstract class BaseFragment : Fragment() { 22 | 23 | companion object { 24 | private val TAG = "BaseFragment" 25 | } 26 | 27 | lateinit var rootView: View 28 | var isVisibleToUser: Boolean = false 29 | var mHandler = Handler(Looper.getMainLooper()) 30 | open lateinit var mContext: Context 31 | open lateinit var mActivity: FragmentActivity 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | parseParams() 36 | } 37 | 38 | open fun parseParams() { 39 | //解析参数 40 | } 41 | 42 | override fun onCreateView( 43 | inflater: LayoutInflater, 44 | container: ViewGroup?, 45 | savedInstanceState: Bundle? 46 | ): View? { 47 | rootView = inflater.inflate(getLayoutId(), container, false) 48 | return rootView 49 | } 50 | 51 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 52 | mContext = requireContext() 53 | mActivity = requireActivity() 54 | EventBusHelper.bind(this) 55 | initData() 56 | initView() 57 | initListener() 58 | observerData() 59 | initControllers() 60 | } 61 | 62 | /** 63 | * 初始化宿主fragment的所有逻辑之后,开始初始化子模块,非必须实现 64 | */ 65 | open fun initControllers() {} 66 | 67 | 68 | override fun onResume() { 69 | super.onResume() 70 | // onResume并不代表fragment可见 71 | if (userVisibleHint && !isHidden) { 72 | onVisible() 73 | } 74 | } 75 | 76 | override fun onPause() { 77 | super.onPause() 78 | if (userVisibleHint && !isHidden) { 79 | onInvisible() 80 | } 81 | } 82 | 83 | 84 | override fun onDestroyView() { 85 | super.onDestroyView() 86 | EventBusHelper.unbind(this) 87 | mHandler.removeCallbacksAndMessages(null) 88 | //确保去除粘性事件 89 | LiveEventBus.get().clear() 90 | } 91 | 92 | abstract fun getLayoutId(): Int 93 | 94 | abstract fun initData() 95 | 96 | abstract fun initView() 97 | 98 | open fun initListener() {} 99 | 100 | open fun observerData() {} 101 | 102 | /** 103 | * 当fragment与viewpager、FragmentPagerAdapter一起使用时,切换页面时会调用此方法 104 | * 105 | * @param isVisibleToUser 是否对用户可见 106 | */ 107 | override fun setUserVisibleHint(isVisibleToUser: Boolean) { 108 | val change = isVisibleToUser != userVisibleHint 109 | super.setUserVisibleHint(isVisibleToUser) 110 | if (isResumed && change) { 111 | if (userVisibleHint) { 112 | onVisible() 113 | } else { 114 | onInvisible() 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * 当使用show/hide方法时,会触发此回调 121 | * 122 | * @param hidden fragment是否被隐藏 123 | */ 124 | override fun onHiddenChanged(hidden: Boolean) { 125 | super.onHiddenChanged(hidden) 126 | if (hidden) { 127 | onInvisible() 128 | } else { 129 | onVisible() 130 | } 131 | } 132 | 133 | @CallSuper 134 | open fun onVisible() { 135 | isVisibleToUser = true 136 | 137 | } 138 | 139 | @CallSuper 140 | open fun onInvisible() { 141 | isVisibleToUser = false 142 | } 143 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/helper/CommonUtils.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.helper 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import androidx.core.app.ActivityCompat 7 | import androidx.core.content.ContextCompat 8 | 9 | /** 10 | * Description:通用工具方法 11 | * 12 | * Created by 杜乾 on 2022/8/12 - 14:10. 13 | * E-mail: duqian2010@gmail.com 14 | */ 15 | object CommonUtils { 16 | 17 | /** 18 | * 检查是否有某个权限 19 | */ 20 | fun checkSelfPermission(ctx: Context, vararg permission: String): Boolean { 21 | var isGranted = false 22 | for (per in permission) { 23 | isGranted = isGranted && (ContextCompat.checkSelfPermission(ctx.applicationContext, per) 24 | == PackageManager.PERMISSION_GRANTED) 25 | } 26 | return isGranted 27 | } 28 | 29 | /** 30 | * 动态申请多个权限 31 | */ 32 | fun requestPermissions(activity: Activity?, permissions: Array, code: Int) { 33 | ActivityCompat.requestPermissions(activity!!, permissions, code) 34 | } 35 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/helper/EventBusHelper.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.helper 2 | 3 | import com.duqian.app.live.base_comm.BaseEvent 4 | import org.greenrobot.eventbus.EventBus 5 | 6 | @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) 7 | @Retention(AnnotationRetention.RUNTIME) 8 | annotation class RegisterEvent 9 | 10 | object EventBusHelper { 11 | private val eventBus = EventBus.getDefault() 12 | 13 | /** 14 | * 新版,发送事件 15 | */ 16 | fun sendEvent(event: BaseEvent) { 17 | eventBus.post(event) 18 | } 19 | 20 | fun notifyStickyEvent(event: BaseEvent) { 21 | eventBus.postSticky(event) 22 | } 23 | 24 | fun bind(obj: Any): EventBusHelper { 25 | val clazz: Class<*> = obj.javaClass 26 | if (clazz.isAnnotationPresent(RegisterEvent::class.java)) { 27 | if (!eventBus.isRegistered(obj)) { 28 | eventBus.register(obj) 29 | } 30 | } 31 | return this 32 | } 33 | 34 | fun unbind(obj: Any): EventBusHelper { 35 | if (eventBus.isRegistered(obj)) { 36 | eventBus.unregister(obj) 37 | } 38 | return this 39 | } 40 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/helper/PageRecordHelper.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.helper 2 | 3 | object PageRecordHelper { 4 | 5 | private val recordList = arrayListOf() 6 | 7 | fun record(pageCode: Int) { 8 | if (pageCode == 0) { 9 | recordList.clear() 10 | } 11 | recordList.add(pageCode) 12 | if (recordList.size > 20) { 13 | recordList.removeAt(0) 14 | } 15 | } 16 | 17 | fun getPageRecord(): String { 18 | return recordList.toString() 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/helper/ToastUtils.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.helper 2 | 3 | import android.widget.Toast 4 | import com.duqian.app.base.BaseApplication 5 | import com.duqian.app.live.utils.UIUtil 6 | 7 | /** 8 | * Description:简易的ToastUtils 9 | * 10 | * Created by 杜乾 on 2022/8/11 - 14:23. 11 | * E-mail: duqian2010@gmail.com 12 | */ 13 | object ToastUtils { 14 | fun show(content: String) { 15 | val context = BaseApplication.instance 16 | if (UIUtil.isOnUiThread) { 17 | Toast.makeText(context, content, Toast.LENGTH_SHORT).show() 18 | } else { 19 | UIUtil.runOnUiThread({ 20 | Toast.makeText(context, content, Toast.LENGTH_SHORT).show() 21 | }) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/helper/UEToolHelper.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.helper 2 | 3 | import android.app.Application.ActivityLifecycleCallbacks 4 | import android.app.Activity 5 | import android.app.Application 6 | import android.os.Bundle 7 | import android.util.Log 8 | import com.duqian.app.live.BuildConfig 9 | import com.duqian.app.base.BaseApplication 10 | import com.duqian.app.live.utils.dpInt 11 | import me.ele.uetool.UETool 12 | 13 | /** 14 | * Created by 杜小菜 on 2022/7/21 - 20:07. 15 | * E-mail: duqian2010@gmail.com 16 | * Description:初始化UETool,设置监听,给了个设置的开关 17 | */ 18 | object UEToolHelper { 19 | fun initUeTool(application: Application) { 20 | //仅供debug包使用,egg调试页面,也给了设置的开关 21 | if (!BuildConfig.DEBUG) return 22 | //UETool.putFilterClass(FilterOutView.class); 23 | //UETool.putAttrsProviderClass(CustomAttribution.class); 24 | Log.d("dq-ue", "initUeTool") 25 | application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { 26 | private var visibleActivityCount = 0 27 | private var UEToolDismissY = 80.dpInt //不要挡住返回按钮 28 | 29 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} 30 | 31 | override fun onActivityStarted(activity: Activity) { 32 | visibleActivityCount++ 33 | val enable: Boolean = 34 | BaseApplication.instance.mmkv?.decodeBool("UEToolEnable", false) ?: false 35 | if (visibleActivityCount == 1 && enable) { 36 | val showUETMenu = UETool.showUETMenu(UEToolDismissY) 37 | Log.d("dq-ue", "showUETMenu $showUETMenu") 38 | } 39 | } 40 | 41 | override fun onActivityResumed(activity: Activity) {} 42 | override fun onActivityPaused(activity: Activity) {} 43 | override fun onActivityStopped(activity: Activity) { 44 | visibleActivityCount-- 45 | if (visibleActivityCount == 0) { 46 | UEToolDismissY = UETool.dismissUETMenu() 47 | } 48 | } 49 | 50 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} 51 | override fun onActivityDestroyed(activity: Activity) {} 52 | }) 53 | } 54 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/base_comm/BaseController.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.base_comm 2 | 3 | import android.content.Context 4 | import android.os.Handler 5 | import android.os.Looper 6 | import android.view.View 7 | import androidx.fragment.app.FragmentActivity 8 | import androidx.lifecycle.* 9 | import com.duqian.app.base.BaseActivity 10 | import com.duqian.app.base.BaseFragment 11 | import com.duqian.app.helper.EventBusHelper 12 | import com.duqian.app.live.utils.UIUtil 13 | import com.duqian.app.live.viewmodel.RoomGlobalViewModel 14 | 15 | /** 16 | * Description:controller基类,生命周期感知 17 | * Created by 杜乾 on 2022/8/11 - 11:15. 18 | * E-mail: duqian2010@gmail.com 19 | */ 20 | //如果要监听event消息,请在实现类中添加注解: @RegisterEvent 21 | abstract class BaseController(val mRootView: View, val mLifecycleOwner: LifecycleOwner) : 22 | DefaultLifecycleObserver { 23 | 24 | var mHandler = Handler(Looper.getMainLooper()) 25 | lateinit var mActivity: FragmentActivity //这个不为空 26 | lateinit var mContext: Context //这个不为空 27 | var mGlobalViewModel: RoomGlobalViewModel? = null 28 | 29 | //注意初始化的时候传入了fragment,才会不为空 30 | var mFragment: BaseFragment? = null 31 | 32 | init { 33 | initBaseContext() 34 | } 35 | 36 | private fun initBaseContext() { 37 | //约束LifecycleOwner必须实现基类BaseFragment或则BaseActivity 38 | if (mLifecycleOwner is BaseFragment) { 39 | this.mFragment = mLifecycleOwner 40 | this.mActivity = mFragment!!.activity as BaseActivity 41 | mContext = mLifecycleOwner.requireContext() 42 | } 43 | if (mLifecycleOwner is BaseActivity) { 44 | this.mActivity = mLifecycleOwner 45 | mContext = mLifecycleOwner 46 | } 47 | val owner = mActivity as ViewModelStoreOwner 48 | mGlobalViewModel = ViewModelProvider(owner)[RoomGlobalViewModel::class.java] 49 | mGlobalViewModel?.isLandscape?.observe(mLifecycleOwner) { 50 | onConfigurationChanged(it) 51 | } 52 | register(mLifecycleOwner) 53 | } 54 | 55 | open fun onConfigurationChanged(isLandscape: Boolean) { 56 | 57 | } 58 | 59 | abstract fun initView(rootView: View) 60 | open fun onResume() {} 61 | open fun onStart() {} 62 | open fun onPause() {} 63 | open fun onStop() {} 64 | abstract fun onDestroy() 65 | 66 | fun isPushRoom(): Boolean { 67 | return mGlobalViewModel?.isPushRoom?.value ?: false 68 | } 69 | 70 | private fun register(owner: LifecycleOwner) { 71 | owner.lifecycle.addObserver(this) 72 | } 73 | 74 | override fun onCreate(owner: LifecycleOwner) { 75 | super.onCreate(owner) 76 | EventBusHelper.bind(this) 77 | initView(mRootView) 78 | observerData() 79 | initData() 80 | } 81 | 82 | open fun observerData() {} 83 | 84 | open fun initData() {} 85 | 86 | override fun onStart(owner: LifecycleOwner) { 87 | super.onStart(owner) 88 | onStart() 89 | } 90 | 91 | override fun onResume(owner: LifecycleOwner) { 92 | super.onResume(owner) 93 | onResume() 94 | } 95 | 96 | override fun onPause(owner: LifecycleOwner) { 97 | super.onPause(owner) 98 | onPause() 99 | } 100 | 101 | override fun onStop(owner: LifecycleOwner) { 102 | super.onStop(owner) 103 | onStop() 104 | } 105 | 106 | override fun onDestroy(owner: LifecycleOwner) { 107 | super.onDestroy(owner) 108 | EventBusHelper.unbind(this) 109 | onDestroy() 110 | mHandler.removeCallbacksAndMessages(null) 111 | mLifecycleOwner.lifecycle.removeObserver(this) 112 | } 113 | 114 | open fun runOnUiThread(action: Runnable) { 115 | if (UIUtil.isOnUiThread) { 116 | action.run() 117 | } else { 118 | mHandler.post(action) 119 | } 120 | } 121 | 122 | open fun runOnUiThreadDelay(action: Runnable, delay: Long) { 123 | mHandler.postDelayed(action, delay) 124 | } 125 | 126 | open fun runOnUiThreadAtFront(action: Runnable) { 127 | mHandler.postAtFrontOfQueue(action) 128 | } 129 | 130 | open fun removeCallbacks(action: Runnable) { 131 | mHandler.removeCallbacks(action) 132 | } 133 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/base_comm/BaseDataBindingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.base_comm 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import android.os.Bundle 7 | import android.view.View 8 | import androidx.databinding.DataBindingUtil 9 | 10 | /** 11 | * Description:使用DataBinding的dialog基类 12 | * 13 | * Created by 杜乾 on 2022/8/13 - 22:21. 14 | * E-mail: duqian2010@gmail.com 15 | */ 16 | abstract class BaseDataBindingDialog : BaseDialogFragment() { 17 | //暴露给外部调用 18 | open var mDataBinding: V? = null 19 | 20 | override fun onCreateView( 21 | inflater: LayoutInflater, 22 | container: ViewGroup?, 23 | savedInstanceState: Bundle? 24 | ): View? { 25 | mDataBinding = DataBindingUtil.inflate(inflater, layoutId, container, true) 26 | return mDataBinding!!.root 27 | } 28 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/base_comm/BaseDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.base_comm 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.DialogFragment 8 | import pub.devrel.easypermissions.EasyPermissions.PermissionCallbacks 9 | import pub.devrel.easypermissions.EasyPermissions 10 | 11 | /** 12 | * Description:DialogFragment基类 13 | * 14 | * Created by 杜乾 on 2022/8/13 - 22:22. 15 | * E-mail: duqian2010@gmail.com 16 | */ 17 | abstract class BaseDialogFragment : DialogFragment() { 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | val bundle = arguments 21 | bundle?.let { parseBundle(it) } 22 | } 23 | 24 | private fun parseBundle(bundle: Bundle) {} 25 | override fun onCreateView( 26 | inflater: LayoutInflater, 27 | container: ViewGroup?, 28 | savedInstanceState: Bundle? 29 | ): View? { 30 | return inflater.inflate(layoutId, container) 31 | } 32 | 33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 34 | super.onViewCreated(view, savedInstanceState) 35 | iniView() 36 | } 37 | 38 | override fun onActivityCreated(savedInstanceState: Bundle?) { 39 | super.onActivityCreated(savedInstanceState) 40 | iniListener() 41 | iniData() 42 | } 43 | 44 | abstract val layoutId: Int 45 | abstract fun iniView() 46 | abstract fun iniData() 47 | abstract fun iniListener() 48 | 49 | override fun onRequestPermissionsResult( 50 | requestCode: Int, permissions: Array, 51 | grantResults: IntArray 52 | ) { 53 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 54 | if (this is PermissionCallbacks) { 55 | EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) 56 | } 57 | } 58 | 59 | val isShowing: Boolean get() = dialog != null && dialog!!.isShowing 60 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/base_comm/BaseEvent.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.base_comm 2 | 3 | /** 4 | * Description:Event基类,所有EventBus事件,继承这个类 5 | * Created by 杜小菜 on 2022/8/10 - 12:41. 6 | * E-mail: duqian2010@gmail.com 7 | */ 8 | open class BaseEvent(val eventCode: Int, val data: Any?) { 9 | //eventCode,int类型的常量, 10 | } 11 | 12 | -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/base_comm/BaseLiveActivity.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.base_comm 2 | 3 | import android.content.Intent 4 | import android.util.Log 5 | import android.view.View 6 | import android.view.WindowManager 7 | import androidx.lifecycle.ViewModelProvider 8 | import androidx.lifecycle.observe 9 | import com.duqian.app.live.R 10 | import com.duqian.app.base.BaseActivity 11 | import com.duqian.app.live.liveroom.data.LiveRoomEvent 12 | import com.duqian.app.live.liveroom.data.LiveRoomEventCode 13 | import com.duqian.app.helper.EventBusHelper 14 | import com.duqian.app.live.utils.UIUtil 15 | import com.duqian.app.live.viewmodel.RoomGlobalViewModel 16 | import pub.devrel.easypermissions.EasyPermissions 17 | 18 | /** 19 | * Description:房间专属的Activity基类,方便管理房间的状态和回调一些房间内的事件 20 | * 开播端和观众端共同继承的基类,提供给后续扩展相同等功能逻辑 21 | * Created by 杜乾 on 2022/8/10 - 15:39. 22 | * E-mail: duqian2010@gmail.com 23 | */ 24 | abstract class BaseLiveActivity : BaseActivity(), EasyPermissions.PermissionCallbacks { 25 | 26 | var mIsPushRoom = false //activity初始化的时候会设值 27 | var mRoomId = 0 28 | 29 | /** 30 | * 进入房间传递的参数,参数不同时,观众端/主播端按需转换成实现类 31 | */ 32 | lateinit var mRoomParams: BaseRoomParams 33 | lateinit var mGlobalViewModel: RoomGlobalViewModel 34 | 35 | override fun initData() { 36 | mRoomParams = intent.getSerializableExtra(ParamKey.PARAM_KEY_ROOM_DATA) as BaseRoomParams 37 | mRoomId = mRoomParams.roomId 38 | mIsPushRoom = mRoomParams.isPushRoom 39 | Log.d("dq-room", "mRoomParams=$mRoomParams") 40 | 41 | //全局的viewModel 42 | mGlobalViewModel = ViewModelProvider(this)[RoomGlobalViewModel::class.java] 43 | mGlobalViewModel.isPushRoom.value = mIsPushRoom 44 | mGlobalViewModel.roomId.value = mRoomId 45 | mGlobalViewModel.roomType.value = RoomType.ROOM_TYPE_SINGLE 46 | 47 | mGlobalViewModel.isLandscape.observe(this) { 48 | onConfigurationChanged(it) 49 | } 50 | 51 | } 52 | 53 | open fun onConfigurationChanged(isLandscape: Boolean) { 54 | 55 | } 56 | 57 | override fun initView() { 58 | super.initView() 59 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 60 | initControllers() 61 | } 62 | 63 | open fun initControllers() { 64 | //初始化各自activity的controllers 65 | } 66 | 67 | override fun onNewIntent(intent: Intent?) { 68 | super.onNewIntent(intent) 69 | } 70 | 71 | override fun isStatusBarDark(): Boolean { 72 | return false 73 | } 74 | 75 | override fun initListener() { 76 | super.initListener() 77 | mRootView.findViewById(R.id.iv_close_room)?.setOnClickListener { 78 | closeRoom() 79 | } 80 | } 81 | 82 | override fun onBackPressed() { 83 | super.onBackPressed() 84 | closeRoom() 85 | } 86 | 87 | private fun closeRoom() { 88 | //发送关闭直播间给其他业务逻辑处理下播和释放资源逻辑 89 | if (mIsPushRoom) { 90 | //ToastUtils.show("主播端,弹窗确认再退出直播间") 91 | } 92 | EventBusHelper.sendEvent( 93 | LiveRoomEvent( 94 | LiveRoomEventCode.EVENT_ID_LIVE_ROOM_END, 95 | mIsPushRoom 96 | ) 97 | ) 98 | finish() 99 | } 100 | 101 | open fun runUiThread(action: Runnable) { 102 | if (UIUtil.isOnUiThread) { 103 | action.run() 104 | } else { 105 | mHandler.post(action) 106 | } 107 | } 108 | 109 | open fun runOnUiThreadDelay(action: Runnable, delay: Long) { 110 | mHandler.postDelayed(action, delay) 111 | } 112 | 113 | open fun runOnUiThreadAtFront(action: Runnable) { 114 | mHandler.postAtFrontOfQueue(action) 115 | } 116 | 117 | open fun removeCallbacks(action: Runnable) { 118 | mHandler.removeCallbacks(action) 119 | } 120 | 121 | override fun onRequestPermissionsResult( 122 | requestCode: Int, 123 | permissions: Array, 124 | grantResults: IntArray 125 | ) { 126 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 127 | // Forward results to EasyPermissions 128 | EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) 129 | } 130 | 131 | override fun onPermissionsGranted(requestCode: Int, perms: MutableList) { 132 | Log.d("dq-room", "onPermissionsGranted requestCode=$requestCode") 133 | } 134 | 135 | override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { 136 | Log.d("dq-room", "onPermissionsDenied requestCode=$requestCode") 137 | } 138 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/base_comm/BaseLiveFragment.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.base_comm 2 | 3 | import android.text.TextUtils 4 | import androidx.fragment.app.Fragment 5 | import androidx.lifecycle.ViewModelProvider 6 | import androidx.lifecycle.ViewModelStoreOwner 7 | import androidx.lifecycle.observe 8 | import com.duqian.app.base.BaseFragment 9 | import com.duqian.app.live.utils.UIUtil 10 | import com.duqian.app.live.viewmodel.RoomGlobalViewModel 11 | 12 | /** 13 | * Description:房间专属的fragment基类,方便回调一些房间内的事件 14 | * 提供给后续扩展都有的功能逻辑 15 | * Created by 杜乾 on 2022/8/10 - 15:39. 16 | * E-mail: duqian2010@gmail.com 17 | */ 18 | abstract class BaseLiveFragment : BaseFragment() { 19 | var mRoomParams: BaseRoomParams? = null 20 | var mRoomId = 0 21 | var mGlobalViewModel: RoomGlobalViewModel? = null 22 | 23 | override fun initListener() { 24 | 25 | } 26 | 27 | override fun observerData() { 28 | val owner = activity as ViewModelStoreOwner 29 | mGlobalViewModel = ViewModelProvider(owner)[RoomGlobalViewModel::class.java] 30 | mGlobalViewModel?.isLandscape?.observe(this) { 31 | onConfigurationChanged(it) 32 | } 33 | } 34 | 35 | open fun onConfigurationChanged(isLandscape: Boolean) { 36 | 37 | } 38 | 39 | override fun parseParams() { 40 | mRoomParams = arguments?.getSerializable(ParamKey.PARAM_KEY_ROOM_DATA) as BaseRoomParams? 41 | mRoomId = mRoomParams?.roomId ?: 0 42 | } 43 | 44 | fun isPushRoom(): Boolean { 45 | return mRoomParams?.isPushRoom == true 46 | } 47 | 48 | open fun runOnUiThread(action: Runnable) { 49 | if (UIUtil.isOnUiThread) { 50 | action.run() 51 | } else { 52 | mHandler.post(action) 53 | } 54 | } 55 | 56 | open fun runOnUiThreadDelay(action: Runnable, delay: Long) { 57 | mHandler.postDelayed(action, delay) 58 | } 59 | 60 | open fun runOnUiThreadAtFront(action: Runnable) { 61 | mHandler.postAtFrontOfQueue(action) 62 | } 63 | 64 | open fun removeCallbacks(action: Runnable) { 65 | mHandler.removeCallbacks(action) 66 | } 67 | 68 | /** 69 | * 房间内只允许增加继承自BaseLiveFragment的fragment 70 | * 方便监听房间内的事件和通用处理逻辑 71 | */ 72 | open fun addFragment(contentId: Int, fragment: BaseLiveFragment, tag: String? = "") { 73 | activity?.supportFragmentManager?.beginTransaction() 74 | ?.replace(contentId, fragment, tag) 75 | //.addToBackStack((fragment as Any).javaClass.simpleName) 76 | ?.disallowAddToBackStack() 77 | ?.commitAllowingStateLoss() 78 | } 79 | 80 | open fun removeFragment(fragment: Fragment) { 81 | activity?.supportFragmentManager?.beginTransaction() 82 | ?.remove(fragment) 83 | ?.commitAllowingStateLoss() 84 | } 85 | 86 | open fun removeFragmentByTag(fragmentTag: String? = "") { 87 | if (TextUtils.isEmpty(fragmentTag)) return 88 | val fragmentByTag = 89 | activity?.supportFragmentManager?.findFragmentByTag(fragmentTag) as Fragment 90 | removeFragment(fragmentByTag) 91 | } 92 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/base_comm/BaseRoomParams.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.base_comm 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * Description:房间参数基类,外部进入房间,传参用对应的子类做下封装:直播间用RoomParams,开播端用LiveRoomParams 7 | * 8 | * Created by 杜乾 on 2022/8/10 - 18:23. 9 | * E-mail: duqian2010@gmail.com 10 | */ 11 | open class BaseRoomParams(val roomId: Int, val from: String? = "") : 12 | Serializable { 13 | //parcelable效率高,但是要实现比较多方法,暂时用Serializable,参数多后用parcelable 14 | 15 | var isPushRoom = false //activity初始化的时候会设值 16 | 17 | var roomType = RoomType.ROOM_TYPE_SINGLE 18 | 19 | var payFrom = "" 20 | 21 | override fun toString(): String { 22 | return "BaseRoomParams(roomId=$roomId, from=$from, isPushRoom=$isPushRoom, payFrom='$payFrom')" 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/base_comm/RoomConstants.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.base_comm 2 | 3 | /** 4 | * Description:房间常量 5 | * Created by 杜乾 on 2022/8/10 - 16:22. 6 | * E-mail: duqian2010@gmail.com 7 | */ 8 | //房间常量 9 | object RoomConstants { 10 | const val HOST_STATUS_ON_LIVE = 0 11 | const val HOST_STATUS_LEAVE = 1 12 | 13 | //权限申请 14 | private const val REQUEST_CODE_BASE = 5000 15 | const val REQUEST_CODE_CAMERA_AND_AUDIO = REQUEST_CODE_BASE + 1 16 | const val REQUEST_CODE_EXTERNAL_STORAGE = REQUEST_CODE_BASE + 2 17 | } 18 | 19 | //本地的房间类型 20 | object RoomType { 21 | const val ROOM_TYPE_SINGLE = 0 //目前只有单人直播的直播间 22 | const val ROOM_TYPE_DOUBLE = 2 //双人连麦的直播间 23 | } 24 | 25 | //from,进入直播间的来源 26 | object RoomFrom { 27 | const val ROOM_FROM_LIVE = "from_live" //从主页live列表进入直播间 28 | } 29 | 30 | //传参数的Key 31 | object ParamKey { 32 | const val PARAM_KEY_ROOM_DATA = "room_data" 33 | const val PARAM_KEY_DIALOG_DATA = "dialog_data" 34 | } 35 | 36 | //LiveEventBus的Key 37 | object LiveEventKey { 38 | const val EVENT_KEY_VIEWER_COUNT = "viewer_count" //房间人数变化 39 | const val EVENT_KEY_CLEAR_LIVE_SCREEN = "clear_live_screen" //清屏 40 | const val EVENT_KEY_SEND_GIFT_START = "send_gift_start" //开始送礼 41 | } 42 | 43 | -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/controller/AnchorInfoController.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.controller 2 | 3 | import android.util.Log 4 | import android.view.View 5 | import android.widget.ImageView 6 | import android.widget.TextView 7 | import androidx.lifecycle.LifecycleOwner 8 | import com.duqian.app.live.R 9 | import com.duqian.app.helper.RegisterEvent 10 | import com.duqian.app.helper.ToastUtils 11 | import com.duqian.app.live.utils.setViewVisible 12 | import com.duqian.app.live.base_comm.BaseController 13 | import com.duqian.app.live.base_comm.LiveEventKey 14 | import com.duqian.app.live.liveroom.data.LiveRoomEvent 15 | import com.duqian.app.live.liveroom.data.LiveRoomEventCode 16 | import com.duqian.app.live.utils.LiveEventBus 17 | import org.greenrobot.eventbus.Subscribe 18 | import org.greenrobot.eventbus.ThreadMode 19 | 20 | /** 21 | * Description:主播头像,昵称,观众人数,follow等逻辑,目前直播间/开播端目前公用 22 | * 23 | * Created by 杜乾 on 2022/8/11 - 13:38. 24 | * E-mail: duqian2010@gmail.com 25 | */ 26 | @RegisterEvent 27 | class AnchorInfoController(view: View, owner: LifecycleOwner) : BaseController(view, owner) { 28 | 29 | companion object { 30 | private const val TAG = "AnchorInfoController" 31 | } 32 | 33 | //获取view的方式,也可以搞成dataBinding,参看RoomCommonController 34 | override fun initView(rootView: View) { 35 | val tvViewerCount = rootView.findViewById(R.id.tvOnlineNum) 36 | //房间人数更新监听 37 | LiveEventBus.get().with(LiveEventKey.EVENT_KEY_VIEWER_COUNT, Int::class.java) 38 | .observe(mLifecycleOwner, true) { 39 | tvViewerCount.text = "$it" 40 | Log.d(TAG, "dq onCreate count = $it,activity=$mActivity fragment=$mFragment") 41 | } 42 | 43 | val followIcon = rootView.findViewById(R.id.ivAnchorFollow) 44 | followIcon.setViewVisible(!isPushRoom()) 45 | followIcon.setOnClickListener { 46 | ToastUtils.show("Followed") 47 | followIcon.setViewVisible(false) 48 | } 49 | } 50 | 51 | override fun initData() { 52 | super.initData() 53 | } 54 | 55 | override fun onResume() { 56 | Log.d(TAG, "dq onResume") 57 | } 58 | 59 | @Subscribe(threadMode = ThreadMode.MAIN) 60 | fun onRoomEvent(event: LiveRoomEvent) { 61 | when (event.eventCode) { 62 | LiveRoomEventCode.EVENT_ID_LIVE_ROOM_END -> { 63 | } 64 | } 65 | } 66 | 67 | 68 | override fun onDestroy() { 69 | Log.d(TAG, "dq onDestroy") 70 | } 71 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/controller/RoomChatBtnController.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.controller 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import androidx.lifecycle.LifecycleOwner 6 | import com.duqian.app.live.R 7 | import com.duqian.app.helper.RegisterEvent 8 | import com.duqian.app.helper.ToastUtils 9 | import com.duqian.app.live.base_comm.BaseController 10 | import com.duqian.app.live.liveroom.data.LiveRoomEvent 11 | import org.greenrobot.eventbus.Subscribe 12 | import org.greenrobot.eventbus.ThreadMode 13 | 14 | /** 15 | * Description:直播间底部,聊天按钮的处理逻辑 16 | * 17 | * Created by 杜乾 on 2022/8/11 - 19:43. 18 | * E-mail: duqian2010@gmail.com 19 | */ 20 | @RegisterEvent 21 | class RoomChatBtnController(view: View, owner: LifecycleOwner) : BaseController(view, owner) { 22 | 23 | override fun initView(rootView: View) { 24 | rootView.findViewById(R.id.ivRoomChat)?.setOnClickListener { 25 | ToastUtils.show("弹起面板,发送聊天消息,isPushRoom=${isPushRoom()}") 26 | } 27 | } 28 | 29 | override fun onResume() { 30 | } 31 | 32 | @Subscribe(threadMode = ThreadMode.MAIN) 33 | fun onRoomEvent(event: LiveRoomEvent) { 34 | when (event.eventCode) { 35 | 36 | } 37 | } 38 | 39 | override fun onDestroy() { 40 | } 41 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/controller/RoomCommonController.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.controller 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import android.widget.TextView 6 | import androidx.constraintlayout.widget.ConstraintLayout 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.lifecycle.LifecycleOwner 9 | import com.duqian.app.live.R 10 | import com.duqian.app.live.databinding.IncludeRoomCommonLayoutBinding 11 | import com.duqian.app.live.base_comm.BaseController 12 | import com.duqian.app.live.base_comm.LiveEventKey 13 | import com.duqian.app.live.base_comm.RoomType 14 | import com.duqian.app.live.utils.LiveEventBus 15 | import com.duqian.app.live.utils.dp 16 | import com.duqian.app.live.utils.setViewVisible 17 | 18 | /** 19 | * Description:直播间一些通用的处理逻辑 20 | * 21 | * Created by 杜乾 on 2022/8/11 - 19:43. 22 | * E-mail: duqian2010@gmail.com 23 | */ 24 | class RoomCommonController(view: View, owner: LifecycleOwner) : BaseController(view, owner) { 25 | 26 | override fun initView(rootView: View) { 27 | val binding: IncludeRoomCommonLayoutBinding = DataBindingUtil.inflate( 28 | mActivity.layoutInflater, 29 | R.layout.include_room_common_layout, 30 | rootView as ViewGroup, 31 | true 32 | ) 33 | 34 | //databinding层级最高的问题,父布局是Include的话,会直接加载直播间布局的最上层 35 | binding.tvRoomCommonTips.setViewVisible(!isPushRoom()) 36 | binding.tvRoomCommonTips.text = "点击模拟切换直播间" 37 | binding.tvRoomCommonTips.setOnClickListener { 38 | mGlobalViewModel?.roomType?.value = RoomType.ROOM_TYPE_DOUBLE 39 | } 40 | 41 | if (isPushRoom()) { 42 | binding.tvLive.text = "主播端" 43 | binding.tvLive.setTextColor(mContext.resources.getColor(R.color.white, null)) 44 | } else { 45 | binding.tvLive.text = "直播间" 46 | } 47 | 48 | binding.tvLive.setOnClickListener { 49 | LiveEventBus.get().with(LiveEventKey.EVENT_KEY_VIEWER_COUNT, Int::class.java) 50 | .postValue((Math.random() * 1000).toInt() + 1999) 51 | } 52 | } 53 | 54 | override fun onConfigurationChanged(isLandscape: Boolean) { 55 | val tvLive = mRootView.findViewById(R.id.tvLive) 56 | if (!isPushRoom()) { 57 | tvLive.text = if (isLandscape) "横屏直播间" else "直播间.." 58 | } else { 59 | tvLive.text = "主播端" 60 | } 61 | val layoutParams = tvLive.layoutParams as ConstraintLayout.LayoutParams 62 | layoutParams.topMargin = 100.dp 63 | tvLive.layoutParams = layoutParams 64 | tvLive.setOnClickListener { 65 | LiveEventBus.get().with(LiveEventKey.EVENT_KEY_VIEWER_COUNT, Int::class.java) 66 | .postValue((Math.random() * 1000).toInt() + 1999) 67 | } 68 | } 69 | 70 | override fun onResume() { 71 | 72 | } 73 | 74 | override fun onDestroy() { 75 | } 76 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/controller/RoomGiftBtnController.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.controller 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import androidx.lifecycle.LifecycleOwner 6 | import com.duqian.app.live.R 7 | import com.duqian.app.live.base_comm.BaseController 8 | import com.duqian.app.live.base_comm.LiveEventKey 9 | import com.duqian.app.live.fragment.dialog.RoomGiftDialog 10 | import com.duqian.app.live.services.AutoServiceHelper 11 | import com.duqian.app.live.utils.FragmentUtils 12 | import com.duqian.app.live.utils.LiveEventBus 13 | import com.duqian.live.mediaplayer.ILivePlayerService 14 | 15 | /** 16 | * Description:直播间底部,礼物按钮的处理逻辑 17 | * 18 | * Created by 杜乾 on 2022/8/11 - 19:43. 19 | * E-mail: duqian2010@gmail.com 20 | */ 21 | class RoomGiftBtnController(view: View, owner: LifecycleOwner) : BaseController(view, owner) { 22 | 23 | override fun initView(rootView: View) { 24 | rootView.findViewById(R.id.ivRoomGift).setOnClickListener { 25 | 26 | val dialogFragment = RoomGiftDialog.newInstance() 27 | dialogFragment.callback = object : RoomGiftDialog.DialogCallback { 28 | override fun onClicked() { 29 | testGiftEffect() 30 | } 31 | } 32 | FragmentUtils.show(mActivity, dialogFragment) 33 | } 34 | } 35 | 36 | private fun testGiftEffect() { 37 | LiveEventBus.get().with(LiveEventKey.EVENT_KEY_SEND_GIFT_START, Boolean::class.java) 38 | .postValue(true) 39 | FragmentUtils.dismissAllowingStateLoss( 40 | mActivity.supportFragmentManager, 41 | RoomGiftDialog::class.java.simpleName 42 | ) 43 | } 44 | 45 | override fun onResume() { 46 | } 47 | 48 | override fun onDestroy() { 49 | val playerService = AutoServiceHelper.load(ILivePlayerService::class.java) 50 | playerService?.stopPlay() 51 | } 52 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/fragment/PublicChatFragment.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.fragment 2 | 3 | import android.os.Bundle 4 | import com.duqian.app.live.R 5 | import com.duqian.app.live.utils.setViewVisible 6 | import com.duqian.app.live.base_comm.BaseLiveFragment 7 | import com.duqian.app.live.base_comm.BaseRoomParams 8 | import com.duqian.app.live.base_comm.ParamKey 9 | import kotlinx.android.synthetic.main.fragment_room_public_chat.* 10 | 11 | /** 12 | * Description:直播间 公屏fragment 13 | * 14 | * Created by 杜乾 on 2022/8/11 - 19:20. 15 | * E-mail: duqian2010@gmail.com 16 | */ 17 | class PublicChatFragment : BaseLiveFragment() { 18 | 19 | //controller或者子fragment实现 20 | 21 | override fun getLayoutId(): Int { 22 | return R.layout.fragment_room_public_chat 23 | } 24 | 25 | override fun initControllers() { 26 | 27 | } 28 | 29 | override fun initData() { 30 | 31 | } 32 | 33 | override fun initView() { 34 | 35 | } 36 | 37 | override fun observerData() { 38 | super.observerData() 39 | mGlobalViewModel?.isLandscape?.observe(this) { 40 | rootPublicChat.setViewVisible(!it) 41 | } 42 | } 43 | 44 | companion object { 45 | /** 46 | * 传入的参数,后续按需扩展,进入房间的参数放BaseRoomParams,其他额外的参数视情况添加 47 | */ 48 | fun newInstance(data: BaseRoomParams? = null): PublicChatFragment { 49 | val fragment = PublicChatFragment() 50 | val bundle = Bundle() 51 | bundle.putSerializable(ParamKey.PARAM_KEY_ROOM_DATA, data) 52 | fragment.arguments = bundle 53 | return fragment 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/fragment/RoomBottomBtnFragment.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.fragment 2 | 3 | import android.os.Bundle 4 | import com.duqian.app.live.R 5 | import com.duqian.app.live.liveroom.controller.RoomCallBtnController 6 | import com.duqian.app.live.liveroom.controller.RoomCoinsBtnController 7 | import com.duqian.app.live.pushroom.controller.PushMicBtnController 8 | import com.duqian.app.live.base_comm.BaseLiveFragment 9 | import com.duqian.app.live.base_comm.BaseRoomParams 10 | import com.duqian.app.live.base_comm.ParamKey 11 | import com.duqian.app.live.controller.RoomChatBtnController 12 | import com.duqian.app.live.controller.RoomGiftBtnController 13 | 14 | /** 15 | * Description:直播间底部按钮fragment 16 | * 17 | * Created by 杜乾 on 2022/8/11 - 19:20. 18 | * E-mail: duqian2010@gmail.com 19 | */ 20 | class RoomBottomBtnFragment : BaseLiveFragment() { 21 | 22 | //聊天按钮对应的逻辑 23 | private lateinit var mChatBtnController: RoomChatBtnController 24 | //送礼 25 | private lateinit var mGiftBtnController: RoomGiftBtnController 26 | //充值 27 | private lateinit var mCoinsBtnController: RoomCoinsBtnController 28 | //打电话 29 | private lateinit var mCallBtnController: RoomCallBtnController 30 | //麦克风 31 | private lateinit var mMicVoiceBtnController: PushMicBtnController 32 | 33 | override fun getLayoutId(): Int { 34 | if (isPushRoom()) { 35 | return R.layout.fragment_pushroom_bottom_btns 36 | } 37 | return R.layout.fragment_liveroom_bottom_btns 38 | } 39 | 40 | override fun initControllers() { 41 | //通用的按钮 42 | mChatBtnController = RoomChatBtnController(rootView, this) 43 | mGiftBtnController = RoomGiftBtnController(rootView, this) 44 | 45 | if (isPushRoom()) { 46 | //主播端初始化底部按钮 47 | mMicVoiceBtnController = PushMicBtnController(rootView.findViewById(R.id.ivRoomMicVoice), this) 48 | } else { 49 | //用户端才需要初始化的按钮逻辑 50 | mCoinsBtnController = RoomCoinsBtnController(rootView, this) 51 | mCallBtnController = RoomCallBtnController(rootView, this) 52 | } 53 | } 54 | 55 | override fun initData() { 56 | 57 | } 58 | 59 | override fun initView() { 60 | 61 | } 62 | 63 | companion object { 64 | private const val TAG = "LiveRoomBottomFragment" 65 | 66 | /** 67 | * 传入的参数,后续按需扩展,进入房间的参数放BaseRoomParams,其他额外的参数视情况添加 68 | */ 69 | fun newInstance(data: BaseRoomParams? = null): RoomBottomBtnFragment { 70 | val fragment = RoomBottomBtnFragment() 71 | val bundle = Bundle() 72 | bundle.putSerializable(ParamKey.PARAM_KEY_ROOM_DATA, data) 73 | fragment.arguments = bundle 74 | return fragment 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/fragment/RoomEffectFragment.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.fragment 2 | 3 | import android.os.Bundle 4 | import com.duqian.app.live.R 5 | import com.duqian.app.live.base_comm.BaseLiveFragment 6 | import com.duqian.app.live.base_comm.BaseRoomParams 7 | import com.duqian.app.live.base_comm.ParamKey 8 | 9 | /** 10 | * Description:Description:直播间 特效 fragment 11 | * 12 | * Created by 杜乾 on 2022/8/12 - 14:23. 13 | * E-mail: duqian2010@gmail.com 14 | */ 15 | class RoomEffectFragment : BaseLiveFragment() { 16 | 17 | override fun getLayoutId(): Int { 18 | return R.layout.fragment_room_gift_effect 19 | } 20 | 21 | override fun initData() { 22 | 23 | } 24 | 25 | override fun initView() { 26 | 27 | } 28 | 29 | companion object { 30 | /** 31 | * 传入的参数,后续按需扩展,进入房间的参数放BaseRoomParams,其他额外的参数视情况添加 32 | */ 33 | fun newInstance(data: BaseRoomParams? = null): RoomEffectFragment { 34 | val fragment = RoomEffectFragment() 35 | val bundle = Bundle() 36 | bundle.putSerializable(ParamKey.PARAM_KEY_ROOM_DATA, data) 37 | fragment.arguments = bundle 38 | return fragment 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/fragment/RoomMainFragment.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.fragment 2 | 3 | import android.content.res.Configuration 4 | import android.os.Bundle 5 | import android.util.Log 6 | import com.duqian.app.live.R 7 | import com.duqian.app.helper.RegisterEvent 8 | import com.duqian.app.live.controller.AnchorInfoController 9 | import com.duqian.app.live.controller.RoomCommonController 10 | import com.duqian.app.live.utils.setViewVisible 11 | import com.duqian.app.live.base_comm.BaseLiveFragment 12 | import com.duqian.app.live.base_comm.BaseRoomParams 13 | import com.duqian.app.live.base_comm.ParamKey 14 | import com.duqian.app.live.liveroom.data.LiveRoomEvent 15 | import com.duqian.app.live.liveroom.data.LiveRoomEventCode 16 | import kotlinx.android.synthetic.main.fragment_room_main.* 17 | import kotlinx.android.synthetic.main.include_room_top_layout.* 18 | import org.greenrobot.eventbus.Subscribe 19 | import org.greenrobot.eventbus.ThreadMode 20 | 21 | /** 22 | * Description:观众端,直播间,主fragment 23 | * Created by 杜乾 on 2022/8/10 - 16:14. 24 | * E-mail: duqian2010@gmail.com 25 | */ 26 | @RegisterEvent 27 | class RoomMainFragment : BaseLiveFragment() { 28 | 29 | //通用逻辑模块(不方便归类,或者无UI逻辑端,可以暂时放这里) 30 | private lateinit var mRoomCommonController: RoomCommonController 31 | 32 | //主播信息/follow模块 33 | private lateinit var mAnchorInfoController: AnchorInfoController 34 | 35 | override fun getLayoutId(): Int { 36 | return R.layout.fragment_room_main 37 | } 38 | 39 | override fun initControllers() { 40 | //通用模块 41 | mRoomCommonController = RoomCommonController(rootCommonContainer, this) 42 | //主播信息 43 | mAnchorInfoController = AnchorInfoController(rootLiveTopContainer, this) 44 | //公屏 45 | addFragment(R.id.rootPublicChatContainer, PublicChatFragment.newInstance(mRoomParams)) 46 | //礼物特效 47 | addFragment(R.id.rootEffectContainer, RoomEffectFragment.newInstance(mRoomParams)) 48 | //底部按钮 49 | addFragment(R.id.rootBottomContainer, RoomBottomBtnFragment.newInstance(mRoomParams)) 50 | } 51 | 52 | override fun initData() { 53 | 54 | } 55 | 56 | override fun initView() { 57 | //rootRoomContainer.setViewVisible(false) 58 | } 59 | 60 | override fun observerData() { 61 | super.observerData() 62 | mGlobalViewModel?.isRoomLoading?.observe(this) { 63 | rootRoomContainer?.setViewVisible(!it) 64 | //Log.d(TAG,"dq isRoomLoading=$it") 65 | } 66 | } 67 | 68 | @Subscribe(threadMode = ThreadMode.MAIN) 69 | fun onRoomEvent(event: LiveRoomEvent) { 70 | when (event.eventCode) { 71 | LiveRoomEventCode.EVENT_ID_LIVE_ROOM_END -> { 72 | } 73 | } 74 | } 75 | 76 | override fun onConfigurationChanged(newConfig: Configuration) { 77 | super.onConfigurationChanged(newConfig) 78 | 79 | Log.d(TAG, "onConfigurationChanged=$") 80 | } 81 | 82 | companion object { 83 | private const val TAG = "LiveRoomFragment" 84 | 85 | /** 86 | * 传入的参数,后续按需扩展,进入房间的参数放BaseRoomParams,其他额外的参数视情况添加 87 | */ 88 | fun newInstance(data: BaseRoomParams? = null): RoomMainFragment { 89 | val fragment = RoomMainFragment() 90 | val bundle = Bundle() 91 | if (data != null) { 92 | bundle.putSerializable(ParamKey.PARAM_KEY_ROOM_DATA, data) 93 | //bundle.putParcelable(ParamKey.PARAM_KEY_ROOM_DATA, data) 94 | } 95 | fragment.arguments = bundle 96 | return fragment 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/fragment/RoomTestFragment.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.fragment 2 | 3 | import android.os.Bundle 4 | import com.duqian.app.live.R 5 | import com.duqian.app.helper.ToastUtils 6 | import com.duqian.app.live.services.IRoomApi 7 | import com.duqian.app.live.base_comm.BaseLiveFragment 8 | import com.duqian.app.live.base_comm.BaseRoomParams 9 | import com.duqian.app.live.base_comm.ParamKey 10 | import com.duqian.app.live.base_comm.RoomType 11 | import dagger.hilt.android.AndroidEntryPoint 12 | import kotlinx.android.synthetic.main.fragment_room_test_layout.* 13 | import javax.inject.Inject 14 | 15 | /** 16 | * Description:测试直播间内容的fragment 17 | * 18 | * Created by 杜乾 on 2022/8/11 - 19:20. 19 | * E-mail: duqian2010@gmail.com 20 | */ 21 | @AndroidEntryPoint 22 | class RoomTestFragment : BaseLiveFragment() { 23 | 24 | @Inject 25 | lateinit var mRoomApi: IRoomApi 26 | 27 | override fun getLayoutId(): Int { 28 | return R.layout.fragment_room_test_layout 29 | } 30 | 31 | override fun initData() { 32 | ToastUtils.show("mRoomApi: ${mRoomApi.getRoomInfo()}") 33 | } 34 | 35 | override fun initView() { 36 | tvRoomTestTips.setOnClickListener { 37 | mGlobalViewModel?.roomType?.value = RoomType.ROOM_TYPE_SINGLE 38 | } 39 | } 40 | 41 | override fun onConfigurationChanged(isLandscape: Boolean) { 42 | super.onConfigurationChanged(isLandscape) 43 | tvRoomTestTips.text = "isLandscape= $isLandscape" 44 | } 45 | 46 | override fun observerData() { 47 | super.observerData() 48 | } 49 | 50 | companion object { 51 | /** 52 | * 传入的参数,后续按需扩展,进入房间的参数放BaseRoomParams,其他额外的参数视情况添加 53 | */ 54 | fun newInstance(data: BaseRoomParams? = null): RoomTestFragment { 55 | val fragment = RoomTestFragment() 56 | val bundle = Bundle() 57 | bundle.putSerializable(ParamKey.PARAM_KEY_ROOM_DATA, data) 58 | fragment.arguments = bundle 59 | return fragment 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/fragment/dialog/RoomGiftDialog.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.fragment.dialog 2 | 3 | import android.os.Bundle 4 | import android.os.Parcelable 5 | import com.duqian.app.live.R 6 | import com.duqian.app.live.databinding.DialogRoomGiftLayoutBinding 7 | import com.duqian.app.live.base_comm.BaseDataBindingDialog 8 | import com.duqian.app.live.base_comm.ParamKey 9 | 10 | /** 11 | * Description:送礼弹窗 12 | * 13 | * Created by 杜乾 on 2022/8/13 - 22:29. 14 | * E-mail: duqian2010@gmail.com 15 | */ 16 | class RoomGiftDialog : BaseDataBindingDialog() { 17 | 18 | override val layoutId: Int 19 | get() = R.layout.dialog_room_gift_layout 20 | 21 | override fun iniView() { 22 | 23 | } 24 | 25 | override fun iniData() { 26 | } 27 | 28 | var callback: DialogCallback? = null 29 | 30 | interface DialogCallback { 31 | fun onClicked() 32 | } 33 | 34 | override fun iniListener() { 35 | mDataBinding?.ivDialogImg?.setOnClickListener { 36 | callback?.onClicked() 37 | } 38 | } 39 | 40 | companion object { 41 | /** 42 | * 传入的参数,按需扩展, 43 | */ 44 | fun newInstance(data: Parcelable? = null): RoomGiftDialog { 45 | val fragment = RoomGiftDialog() 46 | val bundle = Bundle() 47 | bundle.putParcelable(ParamKey.PARAM_KEY_DIALOG_DATA, data) 48 | fragment.arguments = bundle 49 | return fragment 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/liveroom/LiveRoomActivity.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.liveroom 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.res.Configuration 7 | import android.util.Log 8 | import com.duqian.app.live.R 9 | import com.duqian.app.helper.RegisterEvent 10 | import com.duqian.app.helper.ToastUtils 11 | import com.duqian.app.live.base_comm.* 12 | import com.duqian.app.live.fragment.RoomMainFragment 13 | import com.duqian.app.live.fragment.RoomTestFragment 14 | import com.duqian.app.live.liveroom.controller.RoomLivePlayerController 15 | import com.duqian.app.live.liveroom.data.LiveRoomParams 16 | import com.duqian.app.live.services.IRoomApi 17 | import com.duqian.app.live.utils.FragmentUtils 18 | import com.duqian.app.live.utils.LiveEventBus 19 | import com.duqian.app.live.liveroom.data.LiveRoomEvent 20 | import com.duqian.app.live.liveroom.data.LiveRoomEventCode 21 | import dagger.hilt.android.AndroidEntryPoint 22 | import org.greenrobot.eventbus.Subscribe 23 | import org.greenrobot.eventbus.ThreadMode 24 | import javax.inject.Inject 25 | 26 | /** 27 | * Description:直播间Activity 28 | * 目前没有支持上下滑动,后面直接加入一个触摸事件处理,不使用ViewPager,可以不改动已有的框架 29 | * Created by 杜乾 on 2022/8/10 - 14:05. 30 | * E-mail: duqian2010@gmail.com 31 | */ 32 | @RegisterEvent 33 | @AndroidEntryPoint 34 | class LiveRoomActivity : BaseLiveActivity() { 35 | 36 | private var mRoomLivePlayerController: RoomLivePlayerController? = null 37 | 38 | @Inject 39 | lateinit var mRoomApi: IRoomApi 40 | 41 | override fun getLayoutId(): Int { 42 | return R.layout.activity_live_room 43 | } 44 | 45 | override fun initData() { 46 | super.initData() 47 | val roomInfo = mRoomApi.getRoomInfo() ?: "" 48 | ToastUtils.show(roomInfo) 49 | } 50 | 51 | override fun initView() { 52 | super.initView() 53 | //主fragment,放在roomType改变的时候处理 54 | } 55 | 56 | override fun observerData() { 57 | super.observerData() 58 | mGlobalViewModel.roomType.observe(this) { 59 | initRoomFragmentByRoomType() 60 | } 61 | 62 | // TODO-dq: remove 清屏 63 | LiveEventBus.get().with(LiveEventKey.EVENT_KEY_SEND_GIFT_START, Boolean::class.java) 64 | .observe(this) { 65 | if (it) 66 | FragmentUtils.removeFragmentByTag( 67 | supportFragmentManager, 68 | RoomMainFragment::class.java.simpleName 69 | ) 70 | Log.d(TAG, "dq EVENT_KEY_CLEAR_LIVE_SCREEN $it") 71 | } 72 | } 73 | 74 | override fun initControllers() { 75 | super.initControllers() 76 | mRoomLivePlayerController = RoomLivePlayerController(mRootView, this) 77 | } 78 | 79 | /** 80 | * 初始化房间类型,方便拓展/修改主体内容,根据直播间类型的初始化 81 | */ 82 | private fun initRoomFragmentByRoomType() { 83 | var mainFragment: BaseLiveFragment = RoomMainFragment.newInstance(mRoomParams) 84 | when (mGlobalViewModel.roomType.value) { 85 | RoomType.ROOM_TYPE_SINGLE -> { 86 | mainFragment = RoomMainFragment.newInstance(mRoomParams) 87 | } 88 | RoomType.ROOM_TYPE_DOUBLE -> {//todo-dq test remove 89 | mainFragment = RoomTestFragment.newInstance(mRoomParams) 90 | } 91 | } 92 | FragmentUtils.addFragment( 93 | supportFragmentManager, R.id.rootRoomContainer, mainFragment 94 | ) 95 | } 96 | 97 | @Subscribe(threadMode = ThreadMode.MAIN) 98 | fun onRoomEvent(event: LiveRoomEvent) { 99 | when (event.eventCode) { 100 | LiveRoomEventCode.EVENT_ID_LIVE_ROOM_END -> { 101 | ToastUtils.show("Live End") 102 | } 103 | } 104 | } 105 | 106 | override fun onConfigurationChanged(newConfig: Configuration) { 107 | super.onConfigurationChanged(newConfig) 108 | val isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE 109 | mGlobalViewModel.isLandscape.value = isLandscape 110 | Log.d(TAG, "dq-isLandscape=$isLandscape") 111 | } 112 | 113 | override fun onDestroy() { 114 | super.onDestroy() 115 | //todo-dq 销毁资源 116 | } 117 | 118 | companion object { 119 | private const val TAG = "LiveRoomActivity" 120 | 121 | fun startActivity(context: Context?, roomParams: LiveRoomParams) { 122 | val intent = Intent(context, LiveRoomActivity::class.java) 123 | roomParams.isPushRoom = false 124 | intent.putExtra(ParamKey.PARAM_KEY_ROOM_DATA, roomParams) 125 | if (context !is Activity) { 126 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 127 | } 128 | context?.startActivity(intent) 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/liveroom/controller/RoomCallBtnController.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.liveroom.controller 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import androidx.lifecycle.LifecycleOwner 6 | import com.duqian.app.live.R 7 | import com.duqian.app.helper.ToastUtils 8 | import com.duqian.app.live.base_comm.BaseController 9 | 10 | /** 11 | * Description:直播间底部,打电话按钮的处理逻辑 12 | * 13 | * Created by 杜乾 on 2022/8/11 - 19:43. 14 | * E-mail: duqian2010@gmail.com 15 | */ 16 | class RoomCallBtnController(view: View, owner: LifecycleOwner) : BaseController(view, owner) { 17 | 18 | companion object { 19 | private const val TAG = "RoomCallBtnController" 20 | } 21 | 22 | override fun initView(rootView: View) { 23 | rootView.findViewById(R.id.ivRoomCall).setOnClickListener { 24 | ToastUtils.show("打电话") 25 | } 26 | } 27 | 28 | override fun onResume() { 29 | } 30 | 31 | override fun onDestroy() { 32 | } 33 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/liveroom/controller/RoomCoinsBtnController.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.liveroom.controller 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import androidx.lifecycle.LifecycleOwner 6 | import com.duqian.app.live.R 7 | import com.duqian.app.helper.ToastUtils 8 | import com.duqian.app.live.base_comm.BaseController 9 | /** 10 | * Description:直播间底部,充值按钮的处理逻辑 11 | * 12 | * Created by 杜乾 on 2022/8/11 - 19:43. 13 | * E-mail: duqian2010@gmail.com 14 | */ 15 | class RoomCoinsBtnController(view: View, owner: LifecycleOwner) : BaseController(view, owner) { 16 | 17 | companion object { 18 | private const val TAG = "RoomCoinsBtnController" 19 | } 20 | 21 | override fun initView(rootView: View) { 22 | rootView.findViewById(R.id.ivRoomCoins).setOnClickListener { 23 | ToastUtils.show("弹起充值面板") 24 | } 25 | } 26 | 27 | override fun onResume() { 28 | 29 | } 30 | 31 | override fun onDestroy() { 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/liveroom/data/LiveRoomEvent.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.liveroom.data 2 | 3 | import com.duqian.app.live.base_comm.BaseEvent 4 | 5 | /** 6 | * Description:直播间通用事件 7 | * Created by 杜小菜 on 2022/8/10 - 12:48. 8 | * E-mail: duqian2010@gmail.com 9 | */ 10 | /** 11 | * 观众端,通用事件 12 | */ 13 | class LiveRoomEvent(eventCode: Int, data: Any? = null) : BaseEvent(eventCode, data) { 14 | } 15 | 16 | //Note 尽量通过eventType区分事件类型,而不是重复新建各种event类。如果要新建,后续在这里补充添加其他直播间的事件类型, 17 | 18 | //定义事件常量,新增的事件id,注意自增,不要写重复了,遇到冲突的时候,确保id不一样即可。 19 | object LiveRoomEventCode { 20 | private const val EVENT_ID_LIVE_ROOM_BASE = 0x1000 //直播间模块基准id 21 | const val EVENT_ID_LIVE_ROOM_ENTER = EVENT_ID_LIVE_ROOM_BASE + 1 //进入房间的事件 22 | const val EVENT_ID_LIVE_ROOM_END = EVENT_ID_LIVE_ROOM_BASE + 2 // 退出直播间的事件 23 | } 24 | 25 | -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/liveroom/data/LiveRoomParams.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.liveroom.data 2 | 3 | import com.duqian.app.live.base_comm.BaseRoomParams 4 | 5 | /** 6 | * Description:外部进入直播房间,传参数做下封装 7 | * Created by 杜乾 on 2022/8/10 - 18:23. 8 | * E-mail: duqian2010@gmail.com 9 | */ 10 | class LiveRoomParams(roomId: Int, from: String? = "") : 11 | BaseRoomParams(roomId, from) { 12 | 13 | override fun toString(): String { 14 | return "RoomParams(roomId=$roomId, from=$from, mRoomType=$roomType)" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/pushroom/PushRoomActivity.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.pushroom 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.pm.ActivityInfo 7 | import android.os.Bundle 8 | import com.duqian.app.live.R 9 | import com.duqian.app.helper.RegisterEvent 10 | import com.duqian.app.live.fragment.RoomMainFragment 11 | import com.duqian.app.live.pushroom.controller.PushCameraController 12 | import com.duqian.app.live.pushroom.data.PushRoomEvent 13 | import com.duqian.app.live.pushroom.data.PushRoomEventCode 14 | import com.duqian.app.live.pushroom.data.PushRoomParams 15 | import com.duqian.app.live.pushroom.ui.LivePreviewFragment 16 | import com.duqian.app.live.utils.FragmentUtils 17 | import com.duqian.app.live.base_comm.BaseLiveActivity 18 | import com.duqian.app.live.base_comm.ParamKey 19 | import com.duqian.app.live.liveroom.data.LiveRoomEvent 20 | import com.duqian.app.live.liveroom.data.LiveRoomEventCode 21 | import com.duqian.app.live.pushroom.controller.AgoraPusherController 22 | import org.greenrobot.eventbus.Subscribe 23 | import org.greenrobot.eventbus.ThreadMode 24 | 25 | /** 26 | * Description:主播端,push直播间 27 | * Created by 杜乾 on 2022/8/10 - 14:05. 28 | * E-mail: duqian2010@gmail.com 29 | */ 30 | @RegisterEvent 31 | class PushRoomActivity : BaseLiveActivity() { 32 | 33 | //CameraX预览 34 | private var mPushCameraController: PushCameraController? = null 35 | 36 | //声网推流 37 | private var mAgoraPusherController: AgoraPusherController? = null 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT 42 | } 43 | 44 | override fun getLayoutId(): Int { 45 | return R.layout.activity_live_push_room 46 | } 47 | 48 | override fun initView() { 49 | super.initView() 50 | initFragments() 51 | } 52 | 53 | override fun initControllers() { 54 | //mPushCameraController = PushCameraController(mRootView, this) 55 | mAgoraPusherController = AgoraPusherController(mRootView, this) 56 | } 57 | 58 | private fun initFragments() { 59 | FragmentUtils.addFragment( 60 | supportFragmentManager, R.id.rootRoomContainer, 61 | LivePreviewFragment.newInstance() 62 | ) 63 | } 64 | 65 | @Subscribe(threadMode = ThreadMode.MAIN) 66 | fun onRoomEvent(event: LiveRoomEvent) { 67 | when (event.eventCode) { 68 | LiveRoomEventCode.EVENT_ID_LIVE_ROOM_END -> { 69 | // TODO-dq: 处理下播逻辑 70 | } 71 | } 72 | } 73 | 74 | @Subscribe(threadMode = ThreadMode.MAIN) 75 | fun onRoomEvent(event: PushRoomEvent) { 76 | when (event.eventCode) { 77 | PushRoomEventCode.EVENT_ID_PUSH_ROOM_START_LIVE -> { 78 | FragmentUtils.addFragment( 79 | supportFragmentManager, R.id.rootRoomContainer, 80 | RoomMainFragment.newInstance(mRoomParams) 81 | ) 82 | } 83 | } 84 | } 85 | 86 | companion object { 87 | fun startActivity(context: Context?, roomParams: PushRoomParams) { 88 | val intent = Intent(context, PushRoomActivity::class.java) 89 | roomParams.isPushRoom = true 90 | intent.putExtra(ParamKey.PARAM_KEY_ROOM_DATA, roomParams) 91 | if (context !is Activity) { 92 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 93 | } 94 | context?.startActivity(intent) 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/pushroom/controller/AgoraPusherController.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.pushroom.controller 2 | 3 | import android.util.Log 4 | import android.view.View 5 | import android.widget.FrameLayout 6 | import androidx.lifecycle.LifecycleOwner 7 | import com.duqian.app.base.BaseApplication 8 | import com.duqian.app.helper.RegisterEvent 9 | import com.duqian.app.live.R 10 | import com.duqian.app.live.base_comm.BaseController 11 | import com.duqian.app.live.liveroom.data.LiveRoomEvent 12 | import com.duqian.app.live.liveroom.data.LiveRoomEventCode 13 | import com.duqian.app.live.pushroom.data.PushRoomEvent 14 | import com.duqian.app.live.pushroom.data.PushRoomEventCode 15 | import com.duqian.app.live.services.AutoServiceHelper 16 | import com.duqian.live.pusher.AgoraPusherImpl.Companion.TEST_PUBLISH_URL 17 | import com.duqian.live.pusher.IBasePusher 18 | import com.duqian.live.pusher.ResultCode 19 | import org.greenrobot.eventbus.Subscribe 20 | import org.greenrobot.eventbus.ThreadMode 21 | 22 | /** 23 | * Description:声网sdk推流逻辑 24 | * 25 | * Created by 杜乾 on 2022/8/18 - 21:11. 26 | * E-mail: duqian2010@gmail.com 27 | */ 28 | 29 | @RegisterEvent 30 | class AgoraPusherController(view: View, owner: LifecycleOwner) : BaseController(view, owner) { 31 | 32 | private lateinit var previewView: FrameLayout 33 | private val mAgoraPush: IBasePusher? = AutoServiceHelper.load(IBasePusher::class.java) 34 | 35 | override fun initView(rootView: View) { 36 | previewView = rootView.findViewById(R.id.agoraContainer) 37 | } 38 | 39 | override fun initData() { 40 | super.initData() 41 | mAgoraPush?.init(BaseApplication.instance.baseContext, previewView) 42 | //禁止声音 43 | //mAgoraPush?.enableAudioVideo(false, false) 44 | //mAgoraPush?.muteLocalAudioStream(true) 45 | } 46 | 47 | @Subscribe(threadMode = ThreadMode.MAIN) 48 | fun onRoomEvent(event: LiveRoomEvent) { 49 | when (event.eventCode) { 50 | LiveRoomEventCode.EVENT_ID_LIVE_ROOM_END -> { 51 | //重复调用,会失败 ERR_LEAVE_CHANNEL_REJECTED(18):离开频道失败 52 | } 53 | } 54 | } 55 | 56 | @Subscribe(threadMode = ThreadMode.MAIN) 57 | fun onPushRoomEvent(event: PushRoomEvent) { 58 | when (event.eventCode) { 59 | PushRoomEventCode.EVENT_ID_PUSH_ROOM_START_LIVE -> { 60 | val joinChannel = mAgoraPush?.joinChannel() 61 | if (joinChannel == ResultCode.STATUS_OK) { 62 | val startPushToCDN = mAgoraPush?.startPushToCDN(TEST_PUBLISH_URL) 63 | Log.d(TAG, "joinChannel=$joinChannel,startPushToCDN=$startPushToCDN") 64 | } else { 65 | Log.d(TAG, "joinChannel,error=$joinChannel") 66 | } 67 | } 68 | 69 | PushRoomEventCode.EVENT_PUSH_MIC_BTN_CLICKED -> { 70 | val isMicEnable = event.data as Boolean 71 | mAgoraPush?.enableAudioVideo( 72 | false, 73 | isMicEnable 74 | ) 75 | Log.d(TAG, "isMicEnable=$isMicEnable") 76 | mAgoraPush?.muteLocalAudioStream(!isMicEnable) 77 | /*if (isMicEnable) { 78 | mAgoraPush?.adjustRecordingSignalVolume(100) 79 | } else { 80 | mAgoraPush?.adjustRecordingSignalVolume(0) 81 | }*/ 82 | } 83 | } 84 | } 85 | 86 | override fun onResume() { 87 | // TODO: 声网的这个处理有问题,停止预览,无法恢复,暂时不处理 88 | val startPreview = mAgoraPush?.startPreview() 89 | Log.d(TAG, "startPreview=$startPreview") 90 | //mAgoraPush?.onLifecycleChanged(LifecycleEvent.ON_RESUME) 91 | } 92 | 93 | override fun onPause() { 94 | val stopPreview = mAgoraPush?.stopPreview() 95 | Log.d(TAG, "startPreview=$stopPreview") 96 | //mAgoraPush?.onLifecycleChanged(LifecycleEvent.ON_PAUSE) 97 | } 98 | 99 | override fun onDestroy() { 100 | mAgoraPush?.release() 101 | } 102 | 103 | companion object { 104 | private const val TAG = "dq-agora-pusher" 105 | } 106 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/pushroom/controller/PushCameraController.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.pushroom.controller 2 | 3 | import android.view.View 4 | import androidx.camera.core.CameraSelector 5 | import androidx.camera.core.Preview 6 | import androidx.camera.lifecycle.ProcessCameraProvider 7 | import androidx.camera.view.PreviewView 8 | import androidx.core.content.ContextCompat 9 | import androidx.lifecycle.LifecycleOwner 10 | import com.duqian.app.live.R 11 | import com.duqian.app.live.base_comm.BaseController 12 | import com.google.common.util.concurrent.ListenableFuture 13 | import com.duqian.app.live.utils.setViewVisible 14 | 15 | /** 16 | * Description:开播端,基于CameraX的摄像头预览逻辑 17 | * 18 | * Created by 杜乾 on 2022/8/11 - 19:43. 19 | * E-mail: duqian2010@gmail.com 20 | */ 21 | class PushCameraController(view: View, owner: LifecycleOwner) : BaseController(view, owner) { 22 | 23 | private var cameraProviderFuture: ListenableFuture? = null 24 | 25 | private lateinit var previewView: PreviewView 26 | override fun initView(rootView: View) { 27 | previewView = rootView.findViewById(R.id.previewView) 28 | previewView.setViewVisible(true) 29 | initCamera() 30 | } 31 | 32 | private fun initCamera() { 33 | cameraProviderFuture = ProcessCameraProvider.getInstance(mActivity) 34 | cameraProviderFuture?.addListener({ 35 | val cameraProvider = cameraProviderFuture!!.get() 36 | bindPreview(cameraProvider) 37 | }, ContextCompat.getMainExecutor(mActivity)) 38 | } 39 | 40 | private fun bindPreview(cameraProvider: ProcessCameraProvider) { 41 | val preview = Preview.Builder().build() 42 | 43 | val cameraSelector = CameraSelector.Builder() 44 | .requireLensFacing(CameraSelector.LENS_FACING_BACK) 45 | //.requireLensFacing(CameraSelector.LENS_FACING_FRONT) 46 | .build() 47 | 48 | preview.setSurfaceProvider(previewView.surfaceProvider) 49 | 50 | val camera = cameraProvider.bindToLifecycle(mLifecycleOwner, cameraSelector, preview) 51 | } 52 | 53 | override fun onResume() { 54 | 55 | } 56 | 57 | override fun onDestroy() { 58 | 59 | } 60 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/pushroom/controller/PushMicBtnController.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.pushroom.controller 2 | 3 | import android.Manifest 4 | import android.view.View 5 | import android.widget.ImageView 6 | import androidx.lifecycle.LifecycleOwner 7 | import com.duqian.app.helper.EventBusHelper 8 | import com.duqian.app.helper.ToastUtils 9 | import com.duqian.app.live.R 10 | import com.duqian.app.live.base_comm.BaseController 11 | import com.duqian.app.live.pushroom.data.PushRoomEvent 12 | import com.duqian.app.live.pushroom.data.PushRoomEventCode 13 | 14 | /** 15 | * Description:开播端,底部,禁音 按钮的处理逻辑 16 | * 17 | * Created by 杜乾 on 2022/8/11 - 19:43. 18 | * E-mail: duqian2010@gmail.com 19 | */ 20 | class PushMicBtnController(view: View, owner: LifecycleOwner) : BaseController(view, owner) { 21 | 22 | private lateinit var ivMicBtn: ImageView 23 | private var isMicOn = true 24 | override fun initView(rootView: View) { 25 | ivMicBtn = rootView.findViewById(R.id.ivRoomMicVoice) 26 | ivMicBtn.setOnClickListener { 27 | if (hasMicPermission()) { 28 | isMicOn = !isMicOn 29 | updateMicIcon() 30 | 31 | EventBusHelper.sendEvent( 32 | PushRoomEvent( 33 | PushRoomEventCode.EVENT_PUSH_MIC_BTN_CLICKED, 34 | isMicOn 35 | ) 36 | ) 37 | } else { 38 | ToastUtils.show("No audio permission") 39 | } 40 | } 41 | } 42 | 43 | private fun hasMicPermission(): Boolean { 44 | return true //todo-dq 权限检测不正常, 45 | //return CommonUtils.checkSelfPermission(mActivity, Manifest.permission.RECORD_AUDIO) 46 | } 47 | 48 | private fun updateMicIcon() { 49 | if (isMicOn) { 50 | ivMicBtn.setImageResource(R.mipmap.ic_live_pushroom_btn_mic_on) 51 | } else { 52 | ivMicBtn.setImageResource(R.mipmap.ic_live_pushroom_btn_mic_off) 53 | } 54 | } 55 | 56 | override fun onResume() { 57 | } 58 | 59 | override fun onDestroy() { 60 | } 61 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/pushroom/data/PushRoomEvent.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.pushroom.data 2 | 3 | import com.duqian.app.live.base_comm.BaseEvent 4 | 5 | /** 6 | * 主播端,通用事件,一般很多事件会和直播间一样,这个类只定义主播端不一样的id 7 | * 尽量通过eventType区分事件类型,而不是重复新建各种event类。如果要新建,后续在这里补充添加其他直播间的事件类型, 8 | * 9 | * Created by 杜小菜 on 2022/8/10 - 12:48. 10 | * E-mail: duqian2010@gmail.com 11 | */ 12 | class PushRoomEvent(eventCode: Int, data: Any? = null) : BaseEvent(eventCode, data) { 13 | } 14 | 15 | object PushRoomEventCode { 16 | //定义事件常量,新增的事件id,注意自增,不要写重复了,遇到冲突的时候,确保id不一样即可。 17 | private const val EVENT_ID_PUSH_ROOM_BASE = 2000 //主播端模块基准id 18 | const val EVENT_ID_PUSH_ROOM_START_LIVE = EVENT_ID_PUSH_ROOM_BASE + 1 //开播事件 19 | const val EVENT_ID_PUSH_ROOM_LEAVE = EVENT_ID_PUSH_ROOM_BASE + 2 // 主播主动离开的事件 20 | const val EVENT_PUSH_MIC_BTN_CLICKED = EVENT_ID_PUSH_ROOM_BASE + 3 // 主播禁音/恢复声音 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/pushroom/data/PushRoomParams.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.pushroom.data 2 | 3 | import com.duqian.app.live.base_comm.BaseRoomParams 4 | 5 | /** 6 | * Description:外部进入开播端,传参数做下封装 7 | * Created by 杜乾 on 2022/8/10 - 18:23. 8 | * E-mail: duqian2010@gmail.com 9 | */ 10 | class PushRoomParams(roomId: Int, from: String? = "") : 11 | BaseRoomParams(roomId, from) { 12 | 13 | override fun toString(): String { 14 | return "RoomParams(roomId=$roomId, from=$from, mRoomType=$roomType)" 15 | } 16 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/pushroom/ui/LivePreviewFragment.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.pushroom.ui 2 | 3 | import android.os.Bundle 4 | import com.duqian.app.live.R 5 | import com.duqian.app.helper.EventBusHelper 6 | import com.duqian.app.helper.RegisterEvent 7 | import com.duqian.app.live.utils.setViewVisible 8 | import com.duqian.app.live.base_comm.BaseLiveFragment 9 | import com.duqian.app.live.base_comm.ParamKey 10 | import com.duqian.app.live.pushroom.data.PushRoomEvent 11 | import com.duqian.app.live.pushroom.data.PushRoomEventCode 12 | import kotlinx.android.synthetic.main.fragment_pushroom_preview.* 13 | import org.greenrobot.eventbus.Subscribe 14 | import org.greenrobot.eventbus.ThreadMode 15 | import java.io.Serializable 16 | 17 | /** 18 | * Description:开播预览界面 19 | * Created by 杜乾 on 2022/8/10 - 18:10. 20 | * E-mail: duqian2010@gmail.com 21 | */ 22 | @RegisterEvent 23 | class LivePreviewFragment : BaseLiveFragment() { 24 | override fun getLayoutId(): Int { 25 | return R.layout.fragment_pushroom_preview 26 | } 27 | 28 | override fun initData() { 29 | 30 | } 31 | 32 | override fun initView() { 33 | 34 | } 35 | 36 | override fun initListener() { 37 | tvStartLive.setOnClickListener { 38 | //todo-dq 请求接口,成功后,隐藏当前按钮,切换显示直播间主体内容 39 | tvStartLive.setViewVisible(false) 40 | EventBusHelper.sendEvent(PushRoomEvent(PushRoomEventCode.EVENT_ID_PUSH_ROOM_START_LIVE)) 41 | } 42 | } 43 | 44 | override fun observerData() { 45 | } 46 | 47 | 48 | @Subscribe(threadMode = ThreadMode.MAIN) 49 | fun onRoomEvent(event: PushRoomEvent) { 50 | when (event.eventCode) { 51 | //PushRoomEventCode.EVENT_ID_PUSH_ROOM_START_LIVE -> { } 52 | } 53 | } 54 | 55 | companion object { 56 | private const val TAG = "LivePreviewFragment" 57 | 58 | /** 59 | * 传入的参数,后续按需扩展,进入房间的参数放BaseRoomParams,其他额外的参数视情况添加 60 | */ 61 | fun newInstance(data: Serializable? = null): LivePreviewFragment { 62 | val fragment = LivePreviewFragment() 63 | val bundle = Bundle() 64 | if (data != null) { 65 | bundle.putSerializable(ParamKey.PARAM_KEY_ROOM_DATA, data) 66 | } 67 | fragment.arguments = bundle 68 | return fragment 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/repository/RoomApiImpl.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.repository 2 | 3 | import com.duqian.app.live.services.IRoomApi 4 | import javax.inject.Inject 5 | 6 | /** 7 | * Description:接口实现类 8 | * 9 | * Created by 杜乾 on 2022/8/14 - 12:50. 10 | * E-mail: duqian2010@gmail.com 11 | */ 12 | class RoomApiImpl @Inject constructor() : IRoomApi { 13 | override fun getRoomInfo(): String? { 14 | return "度小菜 roomInfo" 15 | } 16 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/repository/RoomModule.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.repository 2 | 3 | import com.duqian.app.live.services.IRoomApi 4 | import dagger.Binds 5 | import dagger.Module 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.components.ActivityComponent 8 | 9 | /** 10 | * Description:房间Module提供者 11 | * 12 | * Created by 杜乾 on 2022/8/14 - 14:18. 13 | * E-mail: duqian2010@gmail.com 14 | */ 15 | @InstallIn(ActivityComponent::class) 16 | @Module 17 | abstract class RoomModule { 18 | 19 | @Binds 20 | abstract fun bindRoomApi(impl: RoomApiImpl): IRoomApi 21 | } 22 | -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/services/AutoServiceHelper.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.services 2 | 3 | import android.app.Application 4 | import java.util.* 5 | 6 | /** 7 | * Description:AutoService的load工具方法,todo-dq 处理有多种接口实现的get 8 | * 9 | * Created by 杜乾 on 2022/8/14 - 00:28. 10 | * E-mail: duqian2010@gmail.com 11 | */ 12 | object AutoServiceHelper { 13 | 14 | fun load(clazz: Class): S? { 15 | val service = ServiceLoader.load(clazz).iterator() 16 | try { 17 | if (service.hasNext()) { 18 | return service.next() 19 | } 20 | } catch (e: Exception) { 21 | e.printStackTrace() 22 | } 23 | return null 24 | } 25 | 26 | fun load(app: Application, service: Class): ServiceLoader { 27 | return ServiceLoader.load(service, app.classLoader) 28 | } 29 | 30 | fun loadFirst(app: Application, service: Class): S { 31 | return load(app, service).iterator().next() 32 | } 33 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/services/IRoomApi.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.services 2 | 3 | /** 4 | * Description:房间接口API 5 | * 6 | * Created by 杜乾 on 2022/8/14 - 11:54. 7 | * E-mail: duqian2010@gmail.com 8 | */ 9 | 10 | interface IRoomApi { 11 | fun getRoomInfo(): String? 12 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/utils/FragmentUtils.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.utils 2 | 3 | import androidx.fragment.app.FragmentActivity 4 | import android.app.Activity 5 | import android.text.TextUtils 6 | import android.util.Log 7 | import androidx.fragment.app.DialogFragment 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.FragmentManager 10 | 11 | /** 12 | * Description:Fragment工具类 13 | * Created by 杜乾 on 2022/8/10 - 15:26. 14 | * E-mail: duqian2010@gmail.com 15 | */ 16 | object FragmentUtils { 17 | 18 | private const val TAG = "FragmentUtils" 19 | 20 | fun show(activity: FragmentActivity?, dialogFragment: DialogFragment?) { 21 | if (activity != null) { 22 | show(activity, activity.supportFragmentManager, false, dialogFragment) 23 | } 24 | } 25 | 26 | fun show( 27 | activity: Activity?, fragmentMgr: FragmentManager?, ignoreShowOnce: Boolean, 28 | dialogFragment: DialogFragment? 29 | ) { 30 | if (dialogFragment == null) { 31 | return 32 | } 33 | //val tag = dialogFragment.javaClass.simpleName 34 | val tag = dialogFragment::class.java.simpleName 35 | show(activity, fragmentMgr, ignoreShowOnce, dialogFragment, tag) 36 | } 37 | 38 | fun show( 39 | activity: Activity?, 40 | manager: FragmentManager?, 41 | fragment: DialogFragment, 42 | tag: String? 43 | ) { 44 | show(activity, manager, false, fragment, tag) 45 | } 46 | 47 | fun show( 48 | activity: Activity?, 49 | manager: FragmentManager?, 50 | ignoreShowOnce: Boolean, 51 | fragment: DialogFragment, 52 | tag: String? 53 | ) { 54 | //ignoreShowOnce,忽略只能展示一次 55 | if (activity == null || manager == null || 56 | !ignoreShowOnce && (manager.findFragmentByTag(tag) != null || fragment.isAdded) 57 | ) { 58 | return 59 | } 60 | showDialogFragmentByCommitAllowingStateLoss(activity, manager, fragment, tag) 61 | } 62 | 63 | fun dismissAllowingStateLoss(fragmentMgr: FragmentManager?, tag: String?) { 64 | if (fragmentMgr != null) { 65 | val fragment = fragmentMgr.findFragmentByTag(tag) 66 | if (fragment is DialogFragment) { 67 | fragment.dismissAllowingStateLoss() 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * 关闭DialogFragment(全局DialogFragment属性使用时,可以先dismiss再重新show避免出现:“java.lang.IllegalStateException: Fragment already 74 | * added:”) 75 | */ 76 | fun dismissAllowingStateLoss(fragment: DialogFragment?): Boolean { 77 | val needDismiss = fragment != null && fragment.fragmentManager != null 78 | if (needDismiss) { 79 | fragment!!.dismissAllowingStateLoss() 80 | } 81 | return needDismiss 82 | } 83 | 84 | fun findFragmentByTag(fragmentActivity: FragmentActivity, tag: String?): Boolean { 85 | return fragmentActivity.supportFragmentManager.findFragmentByTag(tag) != null 86 | } 87 | 88 | fun findFragmentByTag(fm: FragmentManager, tag: String?): Fragment? { 89 | return fm.findFragmentByTag(tag) 90 | } 91 | 92 | fun findByTag(fm: FragmentManager?, clazz: Class): T? { 93 | val tag = clazz.simpleName 94 | return if (fm == null) { 95 | null 96 | } else fm.findFragmentByTag(tag) as T? 97 | } 98 | 99 | fun hasFragmentByTag(fragmentActivity: FragmentActivity, tag: String): Boolean { 100 | return fragmentActivity.supportFragmentManager.findFragmentByTag(tag) != null 101 | } 102 | 103 | fun isFragmentShowing(fragmentMgr: FragmentManager?, tag: String?): Boolean { 104 | if (fragmentMgr?.findFragmentByTag(tag) != null) { //instanceof DialogFragment 105 | val fragment = fragmentMgr.findFragmentByTag(tag) 106 | return fragment != null && fragment.isAdded 107 | } 108 | return false 109 | } 110 | 111 | fun addFragment(fm: FragmentManager?, layoutId: Int, fragment: Fragment?) { 112 | if (fm == null || fragment == null) { 113 | return 114 | } 115 | fm.beginTransaction().replace(layoutId, fragment, fragment.javaClass.simpleName) 116 | .commitAllowingStateLoss() 117 | } 118 | 119 | fun removeFragment(fm: FragmentManager?, fragment: Fragment?) { 120 | try { 121 | if (fm == null || fragment == null || !fragment.isAdded) { 122 | return 123 | } 124 | fm.beginTransaction().remove(fragment).commitAllowingStateLoss() 125 | } catch (e: Exception) { 126 | Log.e(TAG, "removeFragment error $e") 127 | } 128 | } 129 | 130 | fun removeFragmentByTag(fm: FragmentManager?, fragmentTag: String? = "") { 131 | if (TextUtils.isEmpty(fragmentTag)) return 132 | val fragmentByTag = 133 | fm?.findFragmentByTag(fragmentTag) 134 | removeFragment(fm, fragmentByTag) 135 | } 136 | 137 | /** 138 | * 直接show,可能报java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState 139 | * 关闭DialogFragment,推荐使用commitAllowingStateLoss()方法 140 | */ 141 | private fun showDialogFragmentByCommitAllowingStateLoss( 142 | activity: Activity?, fm: FragmentManager?, 143 | dialogFragment: DialogFragment?, tag: String? 144 | ) { 145 | if (fm != null && dialogFragment != null && 146 | tag != null && !UIUtil.isActivityIllegal(activity) 147 | ) { 148 | val ft = fm.beginTransaction() 149 | ft.add(dialogFragment, tag) 150 | ft.commitAllowingStateLoss() 151 | if (UIUtil.isOnUiThread) { 152 | fm.executePendingTransactions() 153 | } else { 154 | activity?.runOnUiThread { fm.executePendingTransactions() } 155 | } 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/utils/KotlinExtUtils.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.utils 2 | 3 | import android.content.res.Resources 4 | import android.view.View 5 | 6 | /** 7 | * Description:description:Kotlin扩展系统函数 8 | * Created by 杜小菜 on 2022/8/1 - 07:59. 9 | * E-mail: duqian2010@gmail.com 10 | */ 11 | // View Extensions 12 | var View.isVisible 13 | get() = visibility == View.VISIBLE 14 | set(value) { 15 | visibility = if (value) View.VISIBLE else View.GONE 16 | } 17 | 18 | fun View.setViewVisible(isVisible: Boolean) { 19 | visibility = if (isVisible) View.VISIBLE else View.GONE 20 | } 21 | 22 | 23 | fun Float.dp2px(): Float { 24 | return (0.5f + this * Resources.getSystem().displayMetrics.density) 25 | } 26 | 27 | fun Int.dp2px(): Float { 28 | return this * 1.0f.dp2px() 29 | } 30 | 31 | val Float.dp: Float 32 | get() = android.util.TypedValue.applyDimension( 33 | android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics 34 | ) 35 | 36 | val Int.dp: Int 37 | get() = (this * 1.0f.dp).toInt() 38 | 39 | val Int.dpInt: Int 40 | get() = this * 1f.dp.toInt() 41 | 42 | val Int.dpFloat: Float 43 | get() = this * 1f.dp 44 | 45 | 46 | val Float.sp: Float 47 | get() = android.util.TypedValue.applyDimension( 48 | android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics 49 | ) 50 | 51 | val Int.sp: Float 52 | get() = this * 1.0f.sp 53 | -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/utils/LiveEventBus.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.utils 2 | 3 | import androidx.lifecycle.* 4 | 5 | /** 6 | * Description:基于LiveData的事件通信 7 | * Created by 杜乾 on 2022/8/11 - 11:04. 8 | * E-mail: duqian2010@gmail.com 9 | */ 10 | class LiveEventBus private constructor() { 11 | private val mLiveDataMap: MutableMap> 12 | 13 | /** 14 | * 存、取LiveData 15 | */ 16 | @Synchronized 17 | fun with(key: String, clazz: Class?): BusMutableLiveData { 18 | if (!mLiveDataMap.containsKey(key)) { 19 | mLiveDataMap[key] = BusMutableLiveData() 20 | } 21 | return mLiveDataMap[key] as BusMutableLiveData 22 | } 23 | 24 | @Synchronized 25 | fun with(key: String): BusMutableLiveData { 26 | return with(key, Any::class.java) 27 | } 28 | 29 | /** 30 | * 去除粘性事件,BusMutableLiveData isDisableSticky为true的都要清空 31 | */ 32 | fun clear() { 33 | val saveMap: MutableMap> = HashMap() 34 | mLiveDataMap.forEach { 35 | val busMutableLiveData = it.value 36 | if (busMutableLiveData.isDisableSticky) { 37 | //要移除 Log.d(TAG, "removed $busMutableLiveData") 38 | } else { 39 | saveMap[it.key] = busMutableLiveData 40 | } 41 | } 42 | mLiveDataMap.clear() 43 | if (saveMap.isNotEmpty()) { 44 | mLiveDataMap.putAll(saveMap) 45 | } 46 | } 47 | 48 | class BusMutableLiveData : MutableLiveData() { 49 | //是否需要阻止粘性事件,true则不会粘性传递 50 | var isDisableSticky = true 51 | 52 | fun observe( 53 | lifecycleOwner: LifecycleOwner, 54 | isDisableSticky: Boolean, 55 | observer: Observer 56 | ) { 57 | this.isDisableSticky = isDisableSticky 58 | observe(lifecycleOwner, observer) 59 | } 60 | 61 | //重写observe的方法 62 | override fun observe(lifecycleOwner: LifecycleOwner, observer: Observer) { 63 | if (isDisableSticky) {// 反射可能兼容性问题,避免有bug,组件onDestroy处理,保留粘性事件 64 | removeEventWhenDestroy(lifecycleOwner, this, observer) 65 | //改变observer.mLastVersion >= mVersion这个判断 然后拦截onChanged 66 | /*try { 67 | hook(observer as Observer) 68 | } catch (e: Exception) { 69 | Log.d("dq-live", "hook Observer error $e") 70 | super.observe(lifecycleOwner, observer) 71 | }*/ 72 | } else { 73 | super.observe(lifecycleOwner, observer) 74 | } 75 | super.observe(lifecycleOwner, observer) 76 | } 77 | 78 | private fun removeEventWhenDestroy( 79 | lifecycleOwner: LifecycleOwner, 80 | busMutableLiveData: BusMutableLiveData, 81 | observer: Observer 82 | ) { 83 | lifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver { 84 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 85 | if (event == Lifecycle.Event.ON_DESTROY) { 86 | busMutableLiveData.removeObserver(observer) 87 | lifecycleOwner.lifecycle.removeObserver(this) 88 | } 89 | } 90 | }) 91 | } 92 | 93 | /** 94 | * hook LiveData 改变一些参数 95 | */ 96 | @Throws(Exception::class) 97 | private fun hook(observer: Observer) { 98 | val liveDataClass = LiveData::class.java 99 | val mObserversField = liveDataClass.getDeclaredField("mObservers") 100 | mObserversField.isAccessible = true 101 | val mObservers = mObserversField[this] 102 | //获取到mObservers的get方法的反射对象 103 | val get = mObservers.javaClass.getDeclaredMethod("get", Any::class.java) 104 | get.isAccessible = true 105 | val invokeEntry = get.invoke(mObservers, observer) 106 | var observerWrapper: Any? = null 107 | if (invokeEntry != null && invokeEntry is Map.Entry<*, *>) { 108 | observerWrapper = invokeEntry.value 109 | } 110 | if (observerWrapper == null) { 111 | throw NullPointerException("ObserverWrapper不能为空") 112 | } 113 | //获取到ObserverWrapper的类对象 114 | val superclass: Class<*> = observerWrapper.javaClass.superclass 115 | val mLastVersionField = superclass.getDeclaredField("mLastVersion") 116 | mLastVersionField.isAccessible = true 117 | val mVersionField = liveDataClass.getDeclaredField("mVersion") 118 | mVersionField.isAccessible = true 119 | //得到mVersion在当前类中的值 120 | val o = mVersionField[this] 121 | //把它的值给mLastVersion 122 | mLastVersionField[observerWrapper] = o 123 | } 124 | } 125 | 126 | companion object { 127 | private val liveDataBus = LiveEventBus() 128 | fun get(): LiveEventBus { 129 | return liveDataBus 130 | } 131 | } 132 | 133 | init { 134 | mLiveDataMap = HashMap() 135 | } 136 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/utils/UIUtil.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.utils 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.res.Configuration 6 | import android.os.Handler 7 | import android.os.Looper 8 | import android.os.Message 9 | 10 | /** 11 | * Description:主线程UI工具类 12 | * Created by 杜乾 on 2022/8/10 - 15:23. 13 | * E-mail: duqian2010@gmail.com 14 | */ 15 | object UIUtil { 16 | private var sMainHandler: Handler? = Handler(Looper.getMainLooper()) 17 | 18 | val isOnUiThread: Boolean 19 | get() = Looper.myLooper() == Looper.getMainLooper() 20 | 21 | fun runOnUiThread(runnable: Runnable?, token: Any? = null) { 22 | runOnUiThreadDelay(runnable, 0, token) 23 | } 24 | 25 | fun runOnUiThread(runnable: Runnable?, delayMillis: Long) { 26 | runOnUiThreadDelay(runnable, delayMillis, null) 27 | } 28 | 29 | private fun runOnUiThreadDelay(runnable: Runnable?, delayMillis: Long, token: Any?) { 30 | if (sMainHandler == null) { 31 | sMainHandler = Handler(Looper.getMainLooper()) 32 | } 33 | val message = Message.obtain(sMainHandler, runnable) 34 | if (token != null) { 35 | message.what = token.hashCode() 36 | } 37 | sMainHandler?.sendMessageDelayed(message, delayMillis) 38 | } 39 | 40 | fun runOnUiThread(action: Runnable) { 41 | if (isOnUiThread) { 42 | action.run() 43 | } else { 44 | sMainHandler?.post(action) 45 | } 46 | } 47 | 48 | fun runOnUiThreadDelay(action: Runnable, delay: Long) { 49 | sMainHandler?.postDelayed(action, delay) 50 | } 51 | 52 | fun runOnUiThreadAtFront(action: Runnable) { 53 | sMainHandler?.postAtFrontOfQueue(action) 54 | } 55 | 56 | fun removeCallbacks(action: Runnable) { 57 | sMainHandler?.removeCallbacks(action) 58 | } 59 | 60 | @JvmStatic 61 | fun isActivityIllegal(context: Context?): Boolean { 62 | return context !is Activity || context.isFinishing || context.isDestroyed 63 | } 64 | 65 | fun isLandscape(context: Context): Boolean { 66 | var isLand = false 67 | try { 68 | val mConfiguration = context.resources.configuration //获取设置的配置信息 69 | val ori = mConfiguration.orientation //获取屏幕方向 70 | isLand = ori == Configuration.ORIENTATION_LANDSCAPE 71 | } catch (e: Exception) { 72 | e.printStackTrace() 73 | } 74 | return isLand 75 | } 76 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/viewmodel/GlobalViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.viewmodel 2 | 3 | import androidx.lifecycle.AndroidViewModel 4 | import androidx.lifecycle.MutableLiveData 5 | import com.duqian.app.base.BaseApplication 6 | 7 | /** 8 | * Description:App级别的ViewModel 9 | * 直播间数据暴露给直播间外部,使用Application级别的ViewModel 10 | * Created by 杜乾 on 2022/8/11 - 14:02. 11 | * E-mail: duqian2010@gmail.com 12 | */ 13 | class GlobalViewModel : AndroidViewModel(BaseApplication.instance) { 14 | //App级别的数据定义在这里,如: 15 | val isLogin = MutableLiveData() 16 | } 17 | -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/live/viewmodel/RoomGlobalViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.live.viewmodel 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | 6 | /** 7 | * Description:直播间全局的状态管理,使用MutableLiveData,方便监听变化 8 | * LifecycleOwner为activity(如:LiveRoomActivity),子模块内部非直播间全局的ViewModel,请用fragment作为owner 9 | * 如果直播间的数据要暴露给外部,请定义在GlobalViewModel中。 10 | * Created by 杜乾 on 2022/8/11 - 14:02. 11 | * E-mail: duqian2010@gmail.com 12 | */ 13 | class RoomGlobalViewModel : ViewModel() { 14 | 15 | /** 16 | * 房间类型,房间改变,一般都要改变UI 17 | */ 18 | val roomType = MutableLiveData() 19 | val roomId = MutableLiveData() 20 | val isPushRoom = MutableLiveData() 21 | val isLandscape = MutableLiveData() 22 | val isRoomLoading = MutableLiveData() //直播间loading 23 | 24 | //拓展直播间全局 25 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/main/MainFragment.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.main 2 | 3 | import android.Manifest 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.duqian.app.live.R 6 | import com.duqian.app.base.BaseApplication 7 | import com.duqian.app.base.BaseFragment 8 | import com.duqian.app.helper.CommonUtils 9 | import com.duqian.app.live.base_comm.RoomConstants 10 | import com.duqian.app.navigator.AppNavigator 11 | import com.duqian.app.navigator.RoomScreens 12 | import dagger.hilt.android.AndroidEntryPoint 13 | import kotlinx.android.synthetic.main.fragment_main.* 14 | import me.ele.uetool.UETool 15 | import pub.devrel.easypermissions.EasyPermissions 16 | import javax.inject.Inject 17 | 18 | /** 19 | * Description:首页MainFragment 20 | * 21 | * Created by 杜乾 on 2022/8/8 - 17:08. 22 | * E-mail: duqian2010@gmail.com 23 | */ 24 | 25 | @AndroidEntryPoint 26 | class MainFragment : BaseFragment() { 27 | 28 | companion object { 29 | private const val TAG = "MainFragment" 30 | 31 | fun newInstance() = MainFragment() 32 | } 33 | 34 | private lateinit var viewModel: MainViewModel 35 | 36 | @Inject 37 | lateinit var navigator: AppNavigator 38 | 39 | override fun getLayoutId(): Int { 40 | return R.layout.fragment_main 41 | } 42 | 43 | override fun initData() { 44 | 45 | } 46 | 47 | override fun initView() { 48 | mainContainer.setOnLongClickListener { 49 | val enable: Boolean = 50 | BaseApplication.instance.mmkv?.decodeBool("UEToolEnable", false) ?: false 51 | BaseApplication.instance.mmkv?.encode("UEToolEnable", !enable) 52 | if (enable) UETool.dismissUETMenu() else UETool.showUETMenu() 53 | false 54 | } 55 | } 56 | 57 | override fun initListener() { 58 | tv_go_live.setOnClickListener { 59 | requestLivePermission() 60 | } 61 | tv_live_room.setOnClickListener { 62 | navigator.navigateTo(RoomScreens.LIVE_ROOM) 63 | /*val roomParams = LiveRoomParams(12345, RoomFrom.ROOM_FROM_LIVE) 64 | LiveRoomActivity.startActivity(context, roomParams)*/ 65 | } 66 | } 67 | 68 | override fun observerData() { 69 | viewModel = ViewModelProvider(this)[MainViewModel::class.java] 70 | } 71 | 72 | fun startLiveActivity() { 73 | //val roomParams = PushRoomParams(6666, RoomFrom.ROOM_FROM_LIVE) 74 | //PushRoomActivity.startActivity(context, roomParams) 75 | navigator.navigateTo(RoomScreens.PUSH_ROOM) 76 | } 77 | 78 | private fun requestLivePermission() { 79 | if (CommonUtils.checkSelfPermission( 80 | requireActivity(), 81 | Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO 82 | ) 83 | ) { 84 | startLiveActivity() 85 | } else { 86 | //ToastUtils.show("no camera permission") 87 | EasyPermissions.requestPermissions( 88 | requireActivity(), 89 | getString(R.string.text_camera_and_audio_rationale), 90 | RoomConstants.REQUEST_CODE_CAMERA_AND_AUDIO, 91 | Manifest.permission.CAMERA, 92 | Manifest.permission.RECORD_AUDIO 93 | ) 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.main 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | class MainViewModel : ViewModel() { 6 | // TODO: Implement the ViewModel 7 | } -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/navigator/AppNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.navigator 2 | 3 | /** 4 | * 直播间房间类型,screens. 5 | */ 6 | enum class RoomScreens { 7 | LIVE_ROOM, 8 | PUSH_ROOM, 9 | } 10 | 11 | /** 12 | * Description:定义页面导航 13 | * 14 | * Created by 杜乾 on 2022/8/14 - 13:47. 15 | * E-mail: duqian2010@gmail.com 16 | */ 17 | interface AppNavigator { 18 | fun navigateTo(screen: RoomScreens) 19 | } 20 | -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/navigator/AppNavigatorImpl.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.navigator 2 | 3 | import androidx.fragment.app.FragmentActivity 4 | import com.duqian.app.live.base_comm.RoomFrom 5 | import com.duqian.app.live.liveroom.LiveRoomActivity 6 | import com.duqian.app.live.liveroom.data.LiveRoomParams 7 | import com.duqian.app.live.pushroom.PushRoomActivity 8 | import com.duqian.app.live.pushroom.data.PushRoomParams 9 | import javax.inject.Inject 10 | 11 | /** 12 | * Description:实现房间跳转 13 | * 14 | * Created by 杜乾 on 2022/8/14 - 13:50. 15 | * E-mail: duqian2010@gmail.com 16 | */ 17 | class AppNavigatorImpl @Inject constructor(private val activity: FragmentActivity) : AppNavigator { 18 | 19 | override fun navigateTo(screen: RoomScreens) { 20 | when (screen) { 21 | RoomScreens.LIVE_ROOM -> { 22 | //ToastUtils.show("enter live room") 23 | //粘性事件 24 | //LiveEventBus.get().with(LiveEventKey.EVENT_KEY_VIEWER_COUNT, Int::class.java).postValue(68899) 25 | 26 | val roomParams = LiveRoomParams(12345, RoomFrom.ROOM_FROM_LIVE) 27 | LiveRoomActivity.startActivity(activity, roomParams) 28 | } 29 | RoomScreens.PUSH_ROOM -> { 30 | //ToastUtils.show("enter push room") 31 | val roomParams = PushRoomParams(6666, RoomFrom.ROOM_FROM_LIVE) 32 | PushRoomActivity.startActivity(activity, roomParams) 33 | } 34 | } 35 | 36 | /*activity.supportFragmentManager.beginTransaction() 37 | .replace(R.id.main_container, fragment) 38 | .addToBackStack(fragment::class.java.canonicalName) 39 | .commit()*/ 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app_live/src/main/java/com/duqian/app/navigator/NavigationModule.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.app.navigator 2 | 3 | import dagger.Binds 4 | import dagger.Module 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.android.components.ActivityComponent 7 | 8 | @InstallIn(ActivityComponent::class) 9 | @Module 10 | abstract class NavigationModule { 11 | 12 | @Binds 13 | abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator 14 | } 15 | -------------------------------------------------------------------------------- /app_live/src/main/res/anim/loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | -------------------------------------------------------------------------------- /app_live/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app_live/src/main/res/drawable/shape_live_room_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app_live/src/main/res/drawable/shape_live_top_anchor_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app_live/src/main/res/drawable/shape_push_start_live_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/activity_live_push_room.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 22 | 23 | 24 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/activity_live_room.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 22 | 26 | 27 | 28 | 32 | 33 | 34 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/dialog_room_gift_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 16 | 17 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/fragment_liveroom_bottom_btns.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 21 | 22 | 23 | 30 | 31 | 39 | 40 | 41 | 49 | 50 | 51 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/fragment_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 28 | 29 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/fragment_pushroom_bottom_btns.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 21 | 22 | 29 | 30 | 38 | 39 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/fragment_pushroom_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 22 | 23 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/fragment_room_gift_effect.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/fragment_room_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 37 | 38 | 39 | 43 | 44 | 45 | 49 | 50 | 51 | 55 | 56 | 57 | 61 | 62 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/fragment_room_public_chat.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 23 | 24 | 25 | 39 | 40 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/fragment_room_test_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 18 | 19 | 30 | 31 | 42 | 43 | 44 | 48 | 49 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/include_liveroom_player.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 17 | 22 | 23 | 28 | 29 | 30 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/include_pushroom_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 12 | 13 | 18 | 19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/include_room_close_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/include_room_common_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 17 | 18 | 29 | 30 | 31 | 43 | 44 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/include_room_public_chat_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app_live/src/main/res/layout/include_room_top_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 27 | 28 | 37 | 38 | 51 | 52 | 69 | 70 | 71 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/bg_dq_live_splash.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/bg_dq_live_splash.jpeg -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/bg_live_common_btn.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/bg_live_common_btn.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/bg_live_pushroom_go_live.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/bg_live_pushroom_go_live.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/bg_live_room_loading_default.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/bg_live_room_loading_default.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_live_close_room.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_live_close_room.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_live_pushroom_btn_mic_off.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_live_pushroom_btn_mic_off.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_live_pushroom_btn_mic_on.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_live_pushroom_btn_mic_on.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_live_room_anchor_follow.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_live_room_anchor_follow.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_live_room_btn_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_live_room_btn_call.png -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_live_room_btn_chat.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_live_room_btn_chat.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_live_room_btn_coins.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_live_room_btn_coins.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_live_room_btn_coins_activity.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_live_room_btn_coins_activity.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_live_room_btn_gift.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_live_room_btn_gift.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_live_room_hot_num.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_live_room_hot_num.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxhdpi/ic_live_room_loading_circle.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxhdpi/ic_live_room_loading_circle.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxxhdpi/bg_room_gift_shop.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxxhdpi/bg_room_gift_shop.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxxhdpi/ic_live_default_head_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxxhdpi/ic_live_default_head_icon.webp -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxxhdpi/ic_live_default_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxxhdpi/ic_live_default_icon.png -------------------------------------------------------------------------------- /app_live/src/main/res/mipmap-xxxhdpi/ic_live_default_icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/app_live/src/main/res/mipmap-xxxhdpi/ic_live_default_icon2.png -------------------------------------------------------------------------------- /app_live/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | -------------------------------------------------------------------------------- /app_live/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app_live/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @color/common_color_5 5 | #ffffff 6 | @color/common_color_5 7 | @color/colorPrimary 8 | 9 | 10 | #B81BEC 11 | #4A1DE7 12 | #FFE34F 13 | #FF5858 14 | #952CFF 15 | #7FFF4A8B 16 | #262628 17 | #909094 18 | #B6B7BC 19 | 20 | 21 | #FFBB86FC 22 | #FF6200EE 23 | #FF3700B3 24 | #FF03DAC5 25 | #FF018786 26 | #00000000 27 | #FF039BE5 28 | #FF01579B 29 | #FF40C4FF 30 | #FF00B0FF 31 | #66000000 32 | #1D1613 33 | 34 | 35 | #FFFFFFFF 36 | #FF000000 37 | 38 | 39 | #E6000000 40 | #cc000000 41 | #B3000000 42 | #99000000 43 | #80000000 44 | #66000000 45 | #4D000000 46 | #33000000 47 | #1A000000 48 | 49 | 50 | #1Affffff 51 | #33ffffff 52 | #4Dffffff 53 | #66ffffff 54 | #80ffffff 55 | #99ffffff 56 | #B3ffffff 57 | #ccffffff 58 | #E6ffffff 59 | 60 | -------------------------------------------------------------------------------- /app_live/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 35dp 5 | -------------------------------------------------------------------------------- /app_live/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DQLive 3 | LiveRoomActivity 4 | Need camera and audio permissions to start live 5 | Need sdcard permissions to play local video 6 | -------------------------------------------------------------------------------- /app_live/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 11 | 12 | 16 | 17 | 21 | -------------------------------------------------------------------------------- /app_live/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 16 | 17 | 18 | 19 | 32 | 33 | 40 | 41 | 45 | -------------------------------------------------------------------------------- /app_live/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app_live/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app_live/src/main/res/xml/file_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app_live/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | miloji.live 6 | 7 | 8 | 9 | 10 | res.miloji.live 11 | test-api.miloji.live 12 | ws.miloji.live 13 | 14 | 15 | 16 | 17 | 18 | android.bugly.qq.com 19 | api-test.gameschalo.com 20 | log.test.v-mate.mobi 21 | test-api-in.cbrcg.com 22 | test.appnow.store 23 | pay.bmartpay.com 24 | bigfun.xiaoxiangwan.com 25 | res-fq.miloji.live 26 | www.miloji.live 27 | 28 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | dependencies { 4 | classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40' 5 | } 6 | } 7 | 8 | plugins { 9 | id 'com.android.application' version '7.2.1' apply false //4.1.1 -->7.2.1 10 | id 'com.android.library' version '7.2.1' apply false 11 | id 'org.jetbrains.kotlin.android' version '1.6.10' apply false //1.4.31--> 1.6.10 12 | } 13 | 14 | task clean(type: Delete) { 15 | delete rootProject.buildDir 16 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | #androidx 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | # Enables namespacing of each library's R class so that its R class includes only the 23 | # resources declared in the library itself and none from the library's dependencies, 24 | # thereby reducing the size of the R class for that library 25 | android.nonTransitiveRClass=true 26 | 27 | # live as library 28 | isLiveModuleLib = false 29 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Aug 09 16:12:41 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | #distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip 5 | distributionPath=wrapper/dists 6 | zipStorePath=wrapper/dists 7 | zipStoreBase=GRADLE_USER_HOME 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /lib_livepusher/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lib_livepusher/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | } 6 | 7 | android { 8 | compileSdk 32 9 | 10 | defaultConfig { 11 | minSdk 23 12 | targetSdk 31 13 | 14 | consumerProguardFiles "consumer-rules.pro" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_1_8 25 | targetCompatibility JavaVersion.VERSION_1_8 26 | } 27 | kotlinOptions { 28 | jvmTarget = '1.8' 29 | } 30 | } 31 | 32 | dependencies { 33 | 34 | implementation 'androidx.core:core-ktx:1.6.0' 35 | implementation 'androidx.appcompat:appcompat:1.5.0' 36 | 37 | //Arogra SDK 38 | api 'io.agora.rtc:full-sdk:3.5.3' 39 | 40 | //autoService 41 | kapt "com.google.auto.service:auto-service:1.0-rc6" 42 | implementation "com.google.auto.service:auto-service:1.0-rc6" 43 | } -------------------------------------------------------------------------------- /lib_livepusher/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/lib_livepusher/consumer-rules.pro -------------------------------------------------------------------------------- /lib_livepusher/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /lib_livepusher/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /lib_livepusher/src/main/java/com/duqian/live/pusher/IBasePusher.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.live.pusher 2 | 3 | import android.content.Context 4 | import android.view.ViewGroup 5 | import com.duqian.live.pusher.helper.LifecycleEvent 6 | 7 | /** 8 | * Description:推流相关接口定义 9 | * 10 | * Created by 杜乾 on 2022/8/18 - 15:08. 11 | * E-mail: duqian2010@gmail.com 12 | */ 13 | interface IBasePusher { 14 | 15 | /** 16 | * 初始化推流配置 17 | */ 18 | fun init(context: Context, videoContainer: ViewGroup): Int 19 | 20 | /** 21 | * 开始摄像头预览 22 | */ 23 | fun startPreview(): Int 24 | 25 | /** 26 | * 停止摄像头预览 27 | */ 28 | fun stopPreview(): Int 29 | 30 | /** 31 | * 进入房间/频道 32 | */ 33 | fun joinChannel(): Int 34 | 35 | /** 36 | * true则停止发送本地媒体流 37 | */ 38 | fun muteLocalAudioStream(isMute: Boolean): Int 39 | 40 | /** 41 | * 退出房间/频道 42 | */ 43 | fun leaveChannel(): Int 44 | 45 | /** 46 | * 销毁资源 47 | * mRtcEngine.leaveChannel();mRtcEngine.destroy(); 48 | */ 49 | fun release() 50 | 51 | /** 52 | * 检测声音 53 | */ 54 | fun startEchoTest(isTest: Boolean): Int 55 | 56 | /** 57 | * 禁止视频,声音功能 58 | */ 59 | fun enableAudioVideo(isVideo: Boolean = false, isEnable: Boolean = true): Int 60 | 61 | /** 62 | * 开启CDN推流 63 | */ 64 | fun startPushToCDN(url: String?): Int 65 | 66 | /** 67 | * onResume 68 | */ 69 | fun onLifecycleChanged(@LifecycleEvent event: Int) 70 | 71 | /** 72 | * 推流声音 73 | */ 74 | fun adjustRecordingSignalVolume(volume: Int): Int 75 | 76 | /** 77 | * 外部设置推流整个过程中的状态,action值越大,流程越后面 78 | */ 79 | fun setCallBack(callback: ILiveCallback?) 80 | } -------------------------------------------------------------------------------- /lib_livepusher/src/main/java/com/duqian/live/pusher/ILiveCallback.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.live.pusher 2 | 3 | /** 4 | * Description:推流相关 回调 定义 5 | * 6 | * Created by 杜乾 on 2022/8/18 - 15:08. 7 | * E-mail: duqian@flatincbr.com 8 | */ 9 | interface ILiveCallback { 10 | 11 | /** 12 | * action值越大,流程越后面 13 | */ 14 | fun onSuccess(action: Int, uid: Int = 0) 15 | 16 | fun onFailed(errorCode: Int? = 0, errorMsg: String? = "") 17 | 18 | fun onConnectionStateChanged(state: Int, reason: Int) {} 19 | } 20 | 21 | object ResultCode { 22 | const val STATUS_OK = 0 //成功 23 | const val STATUS_FAILED = -1 //失败 24 | } 25 | -------------------------------------------------------------------------------- /lib_livepusher/src/main/java/com/duqian/live/pusher/helper/CommonState.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.live.pusher.helper 2 | 3 | import androidx.annotation.IntDef 4 | 5 | class CommonState { 6 | } 7 | 8 | /** 9 | * 当前的页面状态 10 | */ 11 | @IntDef(value = [LifecycleEvent.ON_RESUME, LifecycleEvent.ON_PAUSE, LifecycleEvent.ON_DESTROY]) 12 | @Retention(AnnotationRetention.SOURCE) 13 | annotation class LifecycleEvent { 14 | companion object { 15 | const val ON_RESUME = 0 16 | const val ON_PAUSE = 1 17 | const val ON_DESTROY = 2 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib_livepusher/src/main/java/com/duqian/live/pusher/helper/PusherConfigHelper.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.live.pusher.helper 2 | 3 | import io.agora.rtc.video.VideoEncoderConfiguration 4 | 5 | /** 6 | * Description:配置参数转换为声网的配置,先跟目前的1v1通话的配置AvConfig类似 7 | * 8 | * Created by 杜乾 on 2022/8/18 - 22:00. 9 | * E-mail: duqian2010@gmail.com 10 | */ 11 | object PusherConfigHelper { 12 | 13 | /** 14 | * 根绝视频分辨率和帧率档位,初始化视频配置 15 | */ 16 | fun initVideoEncoderConfiguration( 17 | videoDimensionsLevel: Int, 18 | fpsLevel: Int 19 | ): VideoEncoderConfiguration { 20 | // 档位 1:160x120 2:320x240 3:480x360 4:640x480 5:960x720 6:1280x720 21 | val videoDimensions: VideoEncoderConfiguration.VideoDimensions = 22 | when (videoDimensionsLevel) { 23 | 1 -> VideoEncoderConfiguration.VD_160x120 24 | 2 -> VideoEncoderConfiguration.VD_320x240 25 | 3 -> VideoEncoderConfiguration.VD_480x360 26 | 4 -> VideoEncoderConfiguration.VD_640x480 27 | 5 -> VideoEncoderConfiguration.VD_960x720 28 | 6 -> VideoEncoderConfiguration.VD_1280x720 29 | else -> VideoEncoderConfiguration.VD_640x480 30 | } 31 | 32 | // 档位 1:7帧 2:10帧 3:15帧 4:24帧 5:30帧 33 | val frameFPS: VideoEncoderConfiguration.FRAME_RATE = when (fpsLevel) { 34 | 1 -> VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_7 35 | 2 -> VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_10 36 | 3 -> VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15 37 | 4 -> VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_24 38 | 5 -> VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_30 39 | else -> VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15 40 | } 41 | 42 | return VideoEncoderConfiguration( 43 | videoDimensions, 44 | frameFPS, 45 | VideoEncoderConfiguration.STANDARD_BITRATE, 46 | VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /lib_mediaplayer/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lib_mediaplayer/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-android' 5 | id 'kotlin-android-extensions' 6 | id 'kotlin-kapt' 7 | } 8 | 9 | android { 10 | compileSdk 32 11 | 12 | defaultConfig { 13 | minSdk 23 14 | targetSdk 31 15 | 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | } 33 | 34 | def exoVersion = "2.18.1"//"2.14.2" 35 | 36 | dependencies { 37 | 38 | implementation 'androidx.core:core-ktx:1.6.0' 39 | implementation 'androidx.appcompat:appcompat:1.5.0' 40 | 41 | //exo 42 | //api "com.google.android.exoplayer:exoplayer:$exoVersion" 43 | api "com.google.android.exoplayer:exoplayer-core:$exoVersion" 44 | api "com.google.android.exoplayer:exoplayer-common:$exoVersion" 45 | api "com.google.android.exoplayer:exoplayer-ui:$exoVersion" 46 | api "com.google.android.exoplayer:extension-rtmp:$exoVersion" 47 | 48 | //autoService 49 | kapt 'com.google.auto.service:auto-service:1.0.1' 50 | implementation 'com.google.auto.service:auto-service:1.0.1' 51 | } -------------------------------------------------------------------------------- /lib_mediaplayer/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/lib_mediaplayer/consumer-rules.pro -------------------------------------------------------------------------------- /lib_mediaplayer/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /lib_mediaplayer/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /lib_mediaplayer/src/main/java/com/duqian/live/mediaplayer/ILivePlayerService.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.live.mediaplayer 2 | 3 | import android.content.Context 4 | import com.google.android.exoplayer2.ui.PlayerView 5 | 6 | /** 7 | * Description:播放器接口 8 | * 9 | * Created by 杜乾 on 2022/8/12 - 17:15. 10 | * E-mail: duqian@flatincbr.com 11 | */ 12 | interface ILivePlayerService { 13 | 14 | fun initPlayer(context: Context, playerView: PlayerView) 15 | 16 | fun startPlay(url: String) 17 | 18 | fun isPlaying(): Boolean 19 | 20 | fun getPlayState(): String 21 | 22 | fun resumePlay() 23 | 24 | fun pausePlay() 25 | 26 | fun stopPlay() 27 | 28 | fun setVolume(audioVolume: Float) 29 | 30 | fun addCallback(callback: PlayerCallback?) 31 | 32 | fun onDestroy() 33 | 34 | } 35 | 36 | /** 37 | * Description:播放器监听 38 | * 39 | * Created by 杜乾 on 2022/8/15 - 07:37. 40 | * E-mail: duqian@flatincbr.com 41 | */ 42 | interface PlayerCallback { 43 | 44 | fun onLoadingChanged(isLoading: Boolean) 45 | 46 | fun onPlayerStateChanged(@PlayState playbackState: String) 47 | 48 | /** 49 | * 首帧回调 50 | */ 51 | fun onRenderedFirstFrame() {} 52 | 53 | fun onPlayerError(e: Exception?) 54 | 55 | fun onIsPlayingChanged(isPlaying: Boolean) {} 56 | } 57 | 58 | -------------------------------------------------------------------------------- /lib_mediaplayer/src/main/java/com/duqian/live/mediaplayer/PlayState.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.live.mediaplayer 2 | 3 | import androidx.annotation.StringDef 4 | 5 | /** 6 | * 直播互动的角色类型 7 | */ 8 | @StringDef( 9 | value = [PlayState.STATE_IDLE, PlayState.STATE_BUFFERING, PlayState.STATE_READY, PlayState.STATE_ENDED, PlayState.STATE_ERROR] 10 | ) 11 | @Retention(AnnotationRetention.SOURCE) 12 | annotation class PlayState { 13 | companion object { 14 | /** The player does not have any media to play. */ 15 | const val STATE_IDLE = "STATE_IDLE" 16 | 17 | /** 18 | * The player is not able to immediately play from its current position. This state typically 19 | * occurs when more data needs to be loaded. 20 | */ 21 | const val STATE_BUFFERING = "STATE_BUFFERING" 22 | 23 | /** 24 | * The player is able to immediately play from its current position. The player will be playing if 25 | * [.getPlayWhenReady] is true, and paused otherwise. 26 | */ 27 | const val STATE_READY = "STATE_READY" 28 | 29 | /** The player has finished playing the media. */ 30 | const val STATE_ENDED = "STATE_ENDED" 31 | 32 | /** 33 | * 自定义的错误状态 34 | */ 35 | const val STATE_ERROR = "STATE_ERROR" 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /lib_mediaplayer/src/main/java/com/duqian/live/mediaplayer/RtmpLivePlayer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | package com.duqian.live.mediaplayer 3 | 4 | import android.content.Context 5 | import android.net.Uri 6 | import android.util.Log 7 | import android.view.View 8 | import com.google.android.exoplayer2.ExoPlaybackException 9 | import com.google.android.exoplayer2.ExoPlayerFactory 10 | import com.google.android.exoplayer2.Player 11 | import com.google.android.exoplayer2.SimpleExoPlayer 12 | import com.google.android.exoplayer2.ext.rtmp.RtmpDataSourceFactory 13 | import com.google.android.exoplayer2.source.ExtractorMediaSource 14 | import com.google.android.exoplayer2.source.MediaSource 15 | import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection 16 | import com.google.android.exoplayer2.trackselection.DefaultTrackSelector 17 | import com.google.android.exoplayer2.trackselection.TrackSelection 18 | import com.google.android.exoplayer2.trackselection.TrackSelector 19 | import com.google.android.exoplayer2.ui.AspectRatioFrameLayout 20 | import com.google.android.exoplayer2.ui.PlayerView 21 | import com.google.android.exoplayer2.upstream.DataSource 22 | import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory 23 | import com.google.android.exoplayer2.util.Util 24 | import com.google.auto.service.AutoService 25 | import java.util.* 26 | 27 | */ 28 | /** 29 | * Description:简单的拉流播放器,基于ExoPlayer 2.9.6 30 | * 31 | * Created by 杜乾 on 2022/8/12 - 13:38. 32 | * E-mail: duqian2010@gmail.com 33 | *//* 34 | 35 | @AutoService(value = [ILivePlayerService::class]) 36 | class RtmpLivePlayer : ILivePlayerService { 37 | 38 | private var player: SimpleExoPlayer? = null//lateinit property player has not been initialized 39 | private lateinit var playerView: PlayerView 40 | 41 | override fun initPlayer(context: Context, playerView: PlayerView, loadingView: View?) { 42 | this.playerView = playerView 43 | 44 | initPlayer(context, playerView) 45 | 46 | player?.also { 47 | it.addListener(object : Player.EventListener { 48 | override fun onLoadingChanged(isLoading: Boolean) { 49 | super.onLoadingChanged(isLoading) 50 | Log.d("dq-player", "isLoading=$isLoading") 51 | //loadingView?.visibility = if (isLoading) View.VISIBLE else View.GONE 52 | mCallback?.onLoadingChanged(isLoading) 53 | } 54 | 55 | override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { 56 | super.onPlayerStateChanged(playWhenReady, playbackState) 57 | //playWhenReady=true 58 | //playbackState=1,failed 59 | //playbackState=2,停止播放 60 | //playbackState=3,拉流成功,播放 61 | */ 62 | /* if (playbackState == Player.STATE_READY) { 63 | loadingView?.visibility = View.GONE 64 | }*//* 65 | 66 | Log.d( 67 | "dq-player", 68 | "onPlayerStateChanged,playWhenReady=$playWhenReady,playbackState=$playbackState" 69 | ) 70 | mCallback?.onPlayerStateChanged(playbackState) 71 | } 72 | 73 | override fun onPlayerError(error: ExoPlaybackException?) { 74 | super.onPlayerError(error) 75 | Log.d("dq-player", "error=$error") 76 | mCallback?.onPlayerError(error) 77 | } 78 | }) 79 | } 80 | 81 | } 82 | 83 | private fun initPlayer(context: Context, playerView: PlayerView) { 84 | if (player == null) { 85 | //val bandwidthMeter: BandwidthMeter = DefaultBandwidthMeter() 86 | val videoTrackSelectionFactory: TrackSelection.Factory = 87 | AdaptiveTrackSelection.Factory() 88 | val trackSelector: TrackSelector = DefaultTrackSelector(videoTrackSelectionFactory) 89 | 90 | player = ExoPlayerFactory.newSimpleInstance(context, trackSelector) 91 | 92 | playerView.player = player 93 | 94 | playerView.useController = false 95 | playerView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL 96 | } 97 | } 98 | 99 | */ 100 | /** 101 | * 播放的方法,待完善 102 | *//* 103 | 104 | override fun startPlay(url: String) { 105 | stopPlay() 106 | //val url = "rtmp://examplepull.agoramdn.com/live/duqian" 107 | //val url = "https://examplepull.agoramdn.com/live/duqian.flv" 108 | val videoSource = createMediaSourceByUrl(url) 109 | player?.also { 110 | it.prepare(videoSource) 111 | it.playWhenReady = true 112 | if (url.endsWith("mp4")) { 113 | //it.repeatMode = Player.REPEAT_MODE_ONE //REPEAT_MODE_ALL闪烁 114 | } else { 115 | it.repeatMode = Player.REPEAT_MODE_OFF 116 | } 117 | } 118 | } 119 | 120 | override fun onResume() { 121 | 122 | } 123 | 124 | private fun createMediaSourceByUrl(url: String): MediaSource { 125 | val isRtmpStream = url.lowercase(Locale.getDefault()).startsWith("rtmp://") 126 | val dataSourceFactory: DataSource.Factory = 127 | if (isRtmpStream) { 128 | RtmpDataSourceFactory() 129 | } else { 130 | DefaultDataSourceFactory( 131 | playerView.context, 132 | Util.getUserAgent(playerView.context, "DQLive") 133 | ) 134 | } 135 | return ExtractorMediaSource.Factory(dataSourceFactory) 136 | .createMediaSource(Uri.parse(url)) 137 | } 138 | 139 | override fun stopPlay() { 140 | player?.stop() 141 | } 142 | 143 | private var mCallback: PlayerCallback? = null 144 | 145 | override fun addCallback(callback: PlayerCallback?) { 146 | this.mCallback = callback 147 | } 148 | 149 | override fun onDestroy() { 150 | player?.stop() 151 | player?.release() 152 | } 153 | }*/ 154 | -------------------------------------------------------------------------------- /nativelib/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /nativelib/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | compileSdk 32 8 | android.ndkVersion = "21.4.7075529" 9 | 10 | defaultConfig { 11 | minSdk 23 12 | targetSdk 32 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | externalNativeBuild { 17 | cmake { 18 | cppFlags "" 19 | //ndk需要编译的目标架构 20 | abiFilters 'armeabi-v7a', 'arm64-v8a' 21 | } 22 | } 23 | } 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | externalNativeBuild { 32 | cmake { 33 | path "src/main/cpp/CMakeLists.txt" 34 | version "3.18.1" 35 | } 36 | } 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_1_8 39 | targetCompatibility JavaVersion.VERSION_1_8 40 | } 41 | kotlinOptions { 42 | jvmTarget = '1.8' 43 | } 44 | } 45 | 46 | dependencies { 47 | 48 | /*implementation 'androidx.core:core-ktx:1.6.0' 49 | implementation 'androidx.appcompat:appcompat:1.5.0' 50 | implementation 'com.google.android.material:material:1.12.0' 51 | testImplementation 'junit:junit:4.13.2' 52 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 53 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'*/ 54 | } -------------------------------------------------------------------------------- /nativelib/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/nativelib/consumer-rules.pro -------------------------------------------------------------------------------- /nativelib/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /nativelib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /nativelib/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # For more information about using CMake with Android Studio, read the 2 | # documentation: https://d.android.com/studio/projects/add-native-code.html 3 | 4 | # Sets the minimum version of CMake required to build the native library. 5 | 6 | cmake_minimum_required(VERSION 3.18.1) 7 | 8 | # Declares and names the project. 9 | 10 | project("nativelib") 11 | 12 | # Creates and names a library, sets it as either STATIC 13 | # or SHARED, and provides the relative paths to its source code. 14 | # You can define multiple libraries, and CMake builds them for you. 15 | # Gradle automatically packages shared libraries with your APK. 16 | 17 | add_library( # Sets the name of the library. 18 | nativelib 19 | 20 | # Sets the library as a shared library. 21 | SHARED 22 | 23 | # Provides a relative path to your source file(s). 24 | nativelib.cpp ) 25 | 26 | # Searches for a specified prebuilt library and stores the path as a 27 | # variable. Because CMake includes system libraries in the search path by 28 | # default, you only need to specify the name of the public NDK library 29 | # you want to add. CMake verifies that the library exists before 30 | # completing its build. 31 | 32 | find_library( # Sets the name of the path variable. 33 | log-lib 34 | 35 | # Specifies the name of the NDK library that 36 | # you want CMake to locate. 37 | log ) 38 | 39 | # Specifies libraries CMake should link to your target library. You 40 | # can link multiple libraries, such as libraries you define in this 41 | # build script, prebuilt third-party libraries, or system libraries. 42 | 43 | target_link_libraries( # Specifies the target library. 44 | nativelib 45 | 46 | # Links the target library to the log library 47 | # included in the NDK. 48 | ${log-lib} ) -------------------------------------------------------------------------------- /nativelib/src/main/cpp/nativelib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | extern "C" JNIEXPORT jstring JNICALL 5 | Java_com_duqian_nativelib_NativeLib_stringFromJNI( 6 | JNIEnv* env, 7 | jobject /* this */) { 8 | std::string hello = "Hello from C++,DQ"; 9 | return env->NewStringUTF(hello.c_str()); 10 | } -------------------------------------------------------------------------------- /nativelib/src/main/java/com/duqian/nativelib/NativeLib.kt: -------------------------------------------------------------------------------- 1 | package com.duqian.nativelib 2 | 3 | class NativeLib { 4 | 5 | /** 6 | * A native method that is implemented by the 'nativelib' native library, 7 | * which is packaged with this application. 8 | */ 9 | external fun stringFromJNI(): String 10 | 11 | companion object { 12 | // Used to load the 'nativelib' library on application startup. 13 | init { 14 | System.loadLibrary("nativelib") 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /screenshot/DQLive.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/screenshot/DQLive.mp4 -------------------------------------------------------------------------------- /screenshot/DQLive_Anchor.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/screenshot/DQLive_Anchor.jpeg -------------------------------------------------------------------------------- /screenshot/DQLive_Landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/screenshot/DQLive_Landscape.png -------------------------------------------------------------------------------- /screenshot/DQLive_Main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/screenshot/DQLive_Main.png -------------------------------------------------------------------------------- /screenshot/DQLive_Room.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/screenshot/DQLive_Room.jpeg -------------------------------------------------------------------------------- /screenshot/DQLive_Splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duqian291902259/DQLive/89a38a5854b58489246d663892669f359445b27b/screenshot/DQLive_Splash.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | maven { url "https://maven.aliyun.com/repository/jcenter" } 7 | maven { url "https://maven.aliyun.com/repository/public" } 8 | } 9 | } 10 | dependencyResolutionManagement { 11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 12 | repositories { 13 | google() 14 | mavenCentral() 15 | maven { url "https://maven.aliyun.com/repository/jcenter" } 16 | maven { url "https://maven.aliyun.com/repository/public" } 17 | maven { url "https://maven.aliyun.com/repository/google" } 18 | maven { url "https://jitpack.io" } 19 | } 20 | } 21 | rootProject.name = "DQLive" 22 | include ':app_live' 23 | include ':lib_mediaplayer' 24 | include ':lib_livepusher' 25 | include ':nativelib' 26 | --------------------------------------------------------------------------------