├── .gitignore ├── README.md ├── README_v1.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── com │ │ └── android │ │ └── cast │ │ └── dlna │ │ └── demo │ │ ├── CastApplication.kt │ │ ├── DetailFragment.kt │ │ ├── MainActivity.kt │ │ └── fragment │ │ ├── ContentFragment.kt │ │ ├── DeviceInfoFragment.kt │ │ ├── DeviceListFragment.kt │ │ ├── DeviceServiceActionFragment.kt │ │ ├── UrlDialogFragment.kt │ │ └── VideoViewFragment.kt │ └── res │ ├── drawable │ ├── cast.xml │ ├── cast_add.xml │ ├── cast_connected.xml │ ├── cast_fast.xml │ ├── cast_mute.xml │ ├── cast_next.xml │ ├── cast_pause.xml │ ├── cast_phone.xml │ ├── cast_play.xml │ ├── cast_previous.xml │ ├── cast_slow.xml │ ├── cast_stop.xml │ ├── cast_tv.xml │ ├── cast_video.xml │ ├── common_button_bg.xml │ ├── common_button_bg_night.xml │ ├── ic_launcher_foreground.xml │ ├── local_http.xml │ └── search.xml │ ├── layout │ ├── activity_main.xml │ ├── fragment_cast.xml │ ├── fragment_content.xml │ ├── fragment_detail.xml │ ├── fragment_device_list.xml │ ├── fragment_device_service_action.xml │ ├── fragment_info.xml │ ├── fragment_video_view.xml │ ├── item_browse_button.xml │ ├── item_content.xml │ ├── item_device.xml │ └── item_video_url.xml │ ├── menu │ └── main_options.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── dlna-core ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro ├── publish.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── android │ └── cast │ └── dlna │ └── core │ ├── Logger.kt │ ├── Utils.kt │ └── http │ ├── ContentResourceServlet.kt │ ├── HttpLocalServer.kt │ └── HttpServer.kt ├── dlna-dmc ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro ├── publish.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── android │ └── cast │ └── dlna │ └── dmc │ ├── DLNACallback.kt │ ├── DLNACastManager.kt │ ├── DLNACastService.kt │ ├── DeviceRegistryImpl.kt │ └── control │ ├── CastControlImpl.kt │ ├── CastInterface.kt │ ├── CastSubscriptionCallback.kt │ ├── ServiceAction.kt │ ├── ServiceExecutor.kt │ ├── Utils.kt │ └── action │ └── SetNextAVTransportURI.kt ├── dlna-dmr ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro ├── publish.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── android │ └── cast │ └── dlna │ └── dmr │ ├── BaseRendererActivity.kt │ ├── DLNARendererService.kt │ ├── RenderControl.kt │ └── service │ ├── AVTransportController.kt │ ├── AVTransportServiceImpl.kt │ ├── AudioRenderController.kt │ ├── AudioRenderServiceImpl.kt │ └── RendererInterface.kt ├── dlna-dms ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro ├── publish.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── android │ └── cast │ └── dlna │ └── dms │ ├── DLNAContentService.kt │ └── service │ ├── ContentDirectoryServiceController.kt │ ├── ContentDirectoryServiceImpl.kt │ └── ContentInterface.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libs ├── core-2.0.0-SNAPSHOT.aar ├── dmc-2.0.0-SNAPSHOT.aar ├── dmr-2.0.0-SNAPSHOT.aar └── dms-2.0.0-SNAPSHOT.aar ├── screen ├── Screenshot_20230801_173015.png ├── Screenshot_20230801_173051.png ├── Screenshot_20230801_173059.png └── Screenshot_20230801_173117.png ├── server ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── android │ │ └── cast │ │ └── dlna │ │ └── demo │ │ └── server │ │ ├── MainActivity.kt │ │ └── ServerApplication.kt │ └── res │ ├── drawable │ └── ic_launcher_foreground.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-night │ └── themes.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── settings.gradle └── tv ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── android │ └── cast │ └── dlna │ └── demo │ └── renderer │ ├── MainActivity.kt │ ├── RendererApplication.kt │ └── VideoViewRendererActivity.kt └── res ├── drawable ├── ic_launcher_background.xml └── ic_launcher_foreground.xml ├── layout ├── activity_main.xml └── activity_videoview_renderer.xml ├── mipmap-anydpi-v26 ├── ic_launcher.xml └── ic_launcher_round.xml ├── mipmap-hdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-mdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxxhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── values-night └── themes.xml ├── values ├── colors.xml ├── strings.xml └── themes.xml └── xml └── network_security_config.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | 3 | .idea/ 4 | 5 | local.properties 6 | 7 | *.iml 8 | 9 | captures/ 10 | 11 | build/ 12 | 13 | cling-core/ 14 | 15 | cling-support/ 16 | 17 | backup/ 18 | 19 | *.sh 20 | /TODO.txt 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DLNA-Cast 2 | 3 | | Author: LIUWEI | 4 | |-------------------------------| 5 | | Email: liuwei10074180@163.com | 6 | 7 | [![Download](https://jitpack.io/v/devin1014/DLNA-Cast.svg)](https://jitpack.io/#devin1014/DLNA-Cast) 8 | 9 | 整理重构中.... 10 | 整理重构中.... 11 | 整理重构中.... 12 | 13 | 有时间会更新这个库,好多人在问Dmr的问题,我这个只是最简单的一个VideoView示例,具体还是需要自己集成播放器,实现各种格式的流。 14 | 另外我也在做DLNA的Flutter库,差不多了也会开源出来。 15 | 16 | 投屏 爱奇艺、优酷、腾讯 TV端的时候,m3u8格式会失败,url需要带参数,带上之后就可以了,这个有时间再研究下(自己模拟了下,可以正常投屏了,但是应该有有效期,暂不清楚具体的机制) 17 | 国内Tv App很多不支持多码率的流,用单码率就就可以播放,但是爱奇艺不行,app应该有特殊的限制。 18 | 19 | # 功能 20 | 21 | 基于Cling库封装的DLNA投屏库 22 | * 支持移动端设备发现控制投射功能(DMC) 23 | * 支持电视端设备播放器功能(DMR) 24 | * 支持服务端设备共享内容(DMS) 25 | 26 | Cling库(v2.1.1) 27 | 28 | [Cling Core](http://4thline.org/projects/cling/core/manual/cling-core-manual.xhtml) 29 | [Cling Support](http://4thline.org/projects/cling/support/manual/cling-support-manual.xhtml) 30 | 31 | 32 | #App示例 33 | 34 | ![AppScreenshot](https://raw.githubusercontent.com/devin1014/DLNA-Cast/master/screen/Screenshot_20230801_173015.png) 35 | ![AppScreenshot](https://raw.githubusercontent.com/devin1014/DLNA-Cast/master/screen/Screenshot_20230801_173051.png) 36 | ![AppScreenshot](https://raw.githubusercontent.com/devin1014/DLNA-Cast/master/screen/Screenshot_20230801_173059.png) 37 | ![AppScreenshot](https://raw.githubusercontent.com/devin1014/DLNA-Cast/master/screen/Screenshot_20230801_173117.png) 38 | 39 | ## 使用说明 40 | ### 引用地址 41 | 在项目根gradle中引入 42 | ``` 43 | allprojects { 44 | repositories { 45 | ... 46 | maven { url 'http://4thline.org/m2' } 47 | maven { url 'https://jitpack.io' } 48 | } 49 | } 50 | ``` 51 | 在项目模块gradle中引入 52 | 53 | ``` 54 | api 'com.github.devin1014.DLNA-Cast:dlna-dmc:vx.x.x' 55 | api 'com.github.devin1014.DLNA-Cast:dlna-dmr:vx.x.x' 56 | api 'com.github.devin1014.DLNA-Cast:dlna-dms:vx.x.x' 57 | 58 | 直接引用aar亦可 59 | ``` 60 | 61 | ### 权限申明 62 | 在AndroidManifest.xml中需要添加如下 63 | 64 | ``` 65 | 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | ### 服务申明 73 | 在AndroidManifest.xml中需要添加如下 74 | 75 | ``` 76 | 77 | 78 | 79 | ``` 80 | 81 | ### 注册服务 82 | 在Activity或者Fragment中绑定/解绑 83 | ``` 84 | @Override 85 | protected void onStart() { 86 | DLNACastManager.getInstance().bindCastService(this); 87 | } 88 | 89 | @Override 90 | protected void onStop() { 91 | DLNACastManager.getInstance().unbindCastService(this); 92 | } 93 | ``` 94 | 95 | 当绑定服务后,会自动搜索设备,也可以手动搜索。 96 | ``` 97 | DLNACastManager.getInstance().search(); 98 | ``` 99 | 100 | ### 监听设备 101 | ``` 102 | DLNACastManager.getInstance().registerDeviceListener(listener); 103 | DLNACastManager.getInstance().unregisterListener(listener); 104 | ``` 105 | 当发现新设备时需要添加到设备列表中用于显示。 106 | * OnDeviceRegistryListener 该接口回调始终在**主线程**线程被调用 107 | 108 | ### 连接设备 109 | ``` 110 | deviceControl: DeviceControl = DLNACastManager.connectDevice(device, callback) 111 | 112 | DeviceControl接口如下: 113 | DeviceControl { 114 | // 投射当前视频 115 | fun setAVTransportURI(uri: String, title: String, callback: ServiceActionCallback?) {} 116 | // 投射下一个视频(不是每个播放器都支持这个功能,当前播放结束自动播放下一个) 117 | fun setNextAVTransportURI(uri: String, title: String, callback: ServiceActionCallback?) {} 118 | // 播放 119 | fun play(speed: String, callback: ServiceActionCallback?) {} 120 | // 暂停 121 | fun pause(callback: ServiceActionCallback?) {} 122 | // 停止 123 | fun stop(callback: ServiceActionCallback?) {} 124 | // 快进/快退 125 | fun seek(millSeconds: Long, callback: ServiceActionCallback?) {} 126 | // 播放下一个视频 127 | fun next(callback: ServiceActionCallback?) {} 128 | // 播放上一个视频 129 | fun previous(callback: ServiceActionCallback?) {} 130 | // 获取当前投射视频的播放信息,当前时间/总时间 131 | fun getPositionInfo(callback: ServiceActionCallback?) {} 132 | // 获取当前视频信息 133 | fun getMediaInfo(callback: ServiceActionCallback?) {} 134 | // 获取当前播放状态等 135 | fun getTransportInfo(callback: ServiceActionCallback?) {} 136 | // 设置音量 137 | fun setVolume(volume: Int, callback: ServiceActionCallback?) {} 138 | // 获取音量 139 | fun getVolume(callback: ServiceActionCallback?) {} 140 | // 设置静音 141 | fun setMute(mute: Boolean, callback: ServiceActionCallback?) {} 142 | // 获取是否静音 143 | fun getMute(callback: ServiceActionCallback?) {} 144 | // 查询objectId的信息(‘0’默认值即所有信息) 145 | fun browse(objectId: String, flag: BrowseFlag, filter: String, firstResult: Int, maxResults: Int, callback: ServiceActionCallback?) {} 146 | // 查找objectId的信息 147 | fun search(containerId: String, searchCriteria: String, filter: String, firstResult: Int, maxResults: Int, callback: ServiceActionCallback?) {} 148 | } 149 | 150 | 每个操作都有相应的参数和事件回调接口,监听操作是否成功 151 | ``` 152 | -------------------------------------------------------------------------------- /README_v1.md: -------------------------------------------------------------------------------- 1 | # DLNA-Cast 2 | 3 | | Author: LIUWEI | 4 | |-------------------------------| 5 | | Email: liuwei10074180@163.com | 6 | 7 | [![Download](https://jitpack.io/v/devin1014/DLNA-Cast.svg)](https://jitpack.io/#devin1014/DLNA-Cast) 8 | 9 | 有时间会更新这个库,好多人在问Dmr的问题,我这个只是最简单的一个VideoView示例,具体还是需要自己集成播放器,实现各种格式的流。 10 | 另外我也在做DLNA的Flutter库,差不多了也会开源出来。 11 | 12 | # 功能 13 | 14 | 基于Cling库封装的DLNA投屏库 15 | * 支持移动端设备控制(DMC)功能 16 | * 支持投射本地视频(DMS)或者网络视频 17 | * 支持电视端设备播放器功能(DMR) 18 | 19 | Cling库(v2.1.1) 20 | 21 | [Cling Core](http://4thline.org/projects/cling/core/manual/cling-core-manual.xhtml) 22 | [Cling Support](http://4thline.org/projects/cling/support/manual/cling-support-manual.xhtml) 23 | 24 | 25 | #App示例 26 | 27 | 28 | ![AppScreenshot](https://raw.githubusercontent.com/devin1014/DLNA-Cast/master/screen/device-2021-05-13-155608.png) 29 | 30 | ## 使用说明 31 | ### 引用地址 32 | 在项目根gradle中引入 33 | ``` 34 | allprojects { 35 | repositories { 36 | ... 37 | maven { url 'http://4thline.org/m2' } 38 | maven { url 'https://jitpack.io' } 39 | } 40 | } 41 | ``` 42 | 在项目模块gradle中引入 43 | 44 | ``` 45 | api 'com.github.devin1014.DLNA-Cast:dlna-dmc:V1.0.0' 46 | ``` 47 | 48 | ### 权限申明 49 | 在AndroidManifest.xml中需要添加如下 50 | 51 | ``` 52 | 53 | 54 | 55 | 56 | 57 | ``` 58 | 59 | ### 服务申明 60 | 在AndroidManifest.xml中需要添加如下 61 | 62 | ``` 63 | 64 | ``` 65 | 66 | ### 注册服务 67 | 在Activity或者Fragment中绑定/解绑 68 | ``` 69 | @Override 70 | protected void onStart() { 71 | DLNACastManager.getInstance().bindCastService(this); 72 | } 73 | 74 | @Override 75 | protected void onStop() { 76 | DLNACastManager.getInstance().unbindCastService(this); 77 | } 78 | ``` 79 | 80 | 当绑定服务后,会自动搜索设备,也可以手动搜索。 81 | ``` 82 | DLNACastManager.getInstance().search(null, 60); 83 | ``` 84 | * type:需要搜索设备的类型,null表示不限制类型 85 | * maxSeconds:搜索最大搜索时长(单位:秒) 86 | 87 | ### 监听设备 88 | ``` 89 | DLNACastManager.getInstance().registerDeviceListener(listener); 90 | DLNACastManager.getInstance().unregisterListener(listener); 91 | ``` 92 | 当发现新设备时需要添加到设备列表中用于显示。 93 | * OnDeviceRegistryListener 该接口回调始终在**主线程**线程被调用 94 | 95 | ### 投屏 96 | 97 | ``` 98 | DLNACastManager.getInstance().cast(device, castObject) 99 | ``` 100 | 101 | * device:已发现的设备(这个设备需要是Renderer类型,支持播放器才能投屏) 102 | * castObject:实现了ICast接口的实现类(主要参数是投屏的url) 103 | 104 | ### 事件监听 105 | 控制器事件监听 106 | 107 | ``` 108 | DLNACastManager.getInstance().registerActionCallbacks(callbacks); 109 | ``` 110 | * callbacks: 操作事件回调接口,主要有如下事件接口(*投屏、播放、暂停、停止、快进*) 111 | **CastEventListener**、**PlayEventListener**、**PauseEventListener**、**StopEventListener**、**SeekToEventListener** 112 | 以上接口都继承自*IServiceAction.IServiceActionCallback* 113 | 114 | ``` 115 | interface IServiceActionCallback { 116 | void onSuccess(T result); //成功 117 | void onFailed(String errMsg); //失败 118 | } 119 | ``` 120 | 121 | 播放器事件监听 122 | `DLNACastManager.getInstance().registerSubscriptionListener(listener)` 123 | * listener:监听播放器端状态改变(根据测试每个电视端实现不同效果也不同,有的发有的不发!) 124 | 125 | ``` 126 | interface ISubscriptionListener { 127 | void onSubscriptionTransportStateChanged(TransportState event); 128 | } 129 | ``` 130 | ### 控制 131 | 目前支持的控制事件如下 132 | 133 | ``` 134 | interface IControl { 135 | void cast(Device device, ICast object); 136 | boolean isCasting(Device device); 137 | boolean isCasting(Device device, @Nullable String uri); 138 | void stop(); 139 | void play(); 140 | void pause(); 141 | void seekTo(long position); 142 | void setVolume(int percent); 143 | void setMute(boolean mute); 144 | void setBrightness(int percent); 145 | } 146 | ``` 147 | ### 查询 148 | 149 | ``` 150 | interface IGetInfo { 151 | void getMediaInfo(Device device, ICastInterface.GetInfoListener listener); 152 | void getPositionInfo(Device device, ICastInterface.GetInfoListener listener); 153 | void getTransportInfo(Device device, ICastInterface.GetInfoListener listener); 154 | void getVolumeInfo(Device device, ICastInterface.GetInfoListener listener); 155 | void getContent(Device device, ContentType contentType, ICastInterface.GetInfoListener listener); 156 | } 157 | ``` 158 | * getMediaInfo:获取媒体信息 159 | * getPositionInfo:获取视频播放位置 160 | * getTransportInfo:获取当前设备播放状态 161 | * getVolumeInfo:获取当前设备音量信息 162 | * getContent:获取当前设备提供的目录信息(当前设备是DMS) 163 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | 3 | build/ 4 | 5 | local.properties 6 | 7 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | namespace 'com.android.cast.dlna.demo' 8 | compileSdk 32 9 | defaultConfig { 10 | applicationId "com.android.cast.dlna.demo" 11 | minSdk 24 12 | targetSdk 32 13 | versionCode 1 14 | versionName "1.0" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | packagingOptions { 23 | exclude 'META-INF/beans.xml' 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | } 30 | 31 | dependencies { 32 | api project(':dlna-dmc') 33 | api project(':dlna-dms') 34 | implementation 'com.guolindev.permissionx:permissionx:1.7.1' 35 | 36 | //noinspection GradleDependency 37 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 38 | implementation 'androidx.appcompat:appcompat:1.4.2' 39 | implementation 'com.google.android.material:material:1.6.1' 40 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 41 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 42 | } 43 | -------------------------------------------------------------------------------- /app/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 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/android/cast/dlna/demo/CastApplication.kt: -------------------------------------------------------------------------------- 1 | package com.android.cast.dlna.demo 2 | 3 | import android.app.Application 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.fragment.app.Fragment 6 | import java.util.logging.Level 7 | 8 | class CastApplication : Application() { 9 | override fun onCreate() { 10 | super.onCreate() 11 | java.util.logging.Logger.getLogger("org.fourthline.cling").level = Level.CONFIG 12 | com.android.cast.dlna.core.Logger.printThread = true 13 | com.android.cast.dlna.core.Logger.enabled = true 14 | com.android.cast.dlna.core.Logger.level = com.android.cast.dlna.core.Level.D 15 | com.android.cast.dlna.core.Logger.create("CastApplication").i("Application onCreate.") 16 | } 17 | } 18 | 19 | // --------------------------------------------- 20 | // ---- URL 21 | // --------------------------------------------- 22 | const val castVideoMp4Url_20s = "https://video.699pic.com/videos/39/09/13/sUXhxQmpaNf91534390913.mp4" 23 | const val castVideoMp4Url_1min = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4" 24 | const val castVideoMp4Url_10min = "http://mirror.aarnet.edu.au/pub/TED-talks/911Mothers_2010W-480p.mp4" 25 | const val castVideoM3u8Url = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8" 26 | 27 | //const val castVideoM3u8Url_480x270 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/v2/prog_index.m3u8" 28 | const val castVideoM3u8Url_960x540 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/v5/prog_index.m3u8" 29 | const val castVideoM3u8Url_1920x1080 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/v9/prog_index.m3u8" 30 | 31 | data class VideoUrl(val url: String, val title: String) 32 | 33 | val videoUrlList = mutableListOf( 34 | VideoUrl(castVideoMp4Url_20s, "20秒的短视频(mp4)"), 35 | VideoUrl(castVideoMp4Url_1min, "1分钟的视频(mp4)"), 36 | VideoUrl(castVideoMp4Url_10min, "10分钟的视频(mp4)"), 37 | VideoUrl(castVideoM3u8Url, "标准苹果测试流(m3u8)-多码率[部分Tv端国内App不支持]"), 38 | VideoUrl(castVideoM3u8Url_960x540, "标准苹果测试流(m3u8)-960x540"), 39 | VideoUrl(castVideoM3u8Url_1920x1080, "标准苹果测试流(m3u8)-1920x1080"), 40 | ) 41 | 42 | internal fun AppCompatActivity.replace(id: Int, fragment: Fragment) { 43 | supportFragmentManager.beginTransaction().replace(id, fragment).commit() 44 | } 45 | 46 | internal fun Fragment.replace(id: Int, fragment: Fragment) { 47 | childFragmentManager.beginTransaction().replace(id, fragment).commit() 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/cast/dlna/demo/DetailFragment.kt: -------------------------------------------------------------------------------- 1 | package com.android.cast.dlna.demo 2 | 3 | import android.os.Bundle 4 | import android.view.KeyEvent 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import com.android.cast.dlna.demo.fragment.ContentFragment 10 | import com.android.cast.dlna.demo.fragment.DeviceInfoFragment 11 | import com.android.cast.dlna.demo.fragment.DeviceServiceActionFragment 12 | import com.android.cast.dlna.demo.fragment.VideoViewFragment 13 | import com.android.cast.dlna.dmc.DLNACastManager 14 | import org.fourthline.cling.model.meta.Device 15 | 16 | interface DetailContainer { 17 | fun getDevice(): Device<*, *, *> 18 | } 19 | 20 | interface OnKeyEventHandler { 21 | fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean = false 22 | } 23 | 24 | class DetailFragment : Fragment(), DetailContainer, OnKeyEventHandler { 25 | companion object { 26 | fun create(device: Device<*, *, *>): Fragment = DetailFragment().apply { 27 | this.device = device 28 | } 29 | } 30 | 31 | private lateinit var device: Device<*, *, *> 32 | override fun getDevice(): Device<*, *, *> = device 33 | 34 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 35 | return inflater.inflate(R.layout.fragment_detail, container, false) 36 | } 37 | 38 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 39 | super.onViewCreated(view, savedInstanceState) 40 | if (this::device.isInitialized) { 41 | if (device.type == DLNACastManager.DEVICE_TYPE_MEDIA_RENDERER) { 42 | replace(R.id.detail_video_container, VideoViewFragment()) 43 | replace(R.id.detail_service_container, DeviceServiceActionFragment()) 44 | replace(R.id.detail_info_container, DeviceInfoFragment()) 45 | } else if (device.type == DLNACastManager.DEVICE_TYPE_MEDIA_SERVER) { 46 | replace(R.id.detail_content_container, ContentFragment()) 47 | } 48 | } 49 | } 50 | 51 | override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { 52 | childFragmentManager.fragments.forEach { 53 | (it as? OnKeyEventHandler)?.onKeyDown(keyCode, event) 54 | } 55 | return false 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/cast/dlna/demo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.android.cast.dlna.demo 2 | 3 | import android.Manifest.permission 4 | import android.annotation.SuppressLint 5 | import android.os.Bundle 6 | import android.view.KeyEvent 7 | import android.view.Menu 8 | import android.view.MenuItem 9 | import android.widget.Toast 10 | import androidx.appcompat.app.AppCompatActivity 11 | import com.android.cast.dlna.core.Utils 12 | import com.android.cast.dlna.demo.fragment.OnItemClickListener 13 | import com.android.cast.dlna.dmc.DLNACastManager 14 | import com.permissionx.guolindev.PermissionX 15 | import org.fourthline.cling.model.meta.Device 16 | 17 | class MainActivity : AppCompatActivity(), OnItemClickListener { 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContentView(R.layout.activity_main) 22 | setSupportActionBar(findViewById(R.id.toolbar)) 23 | PermissionX.init(this) 24 | .permissions(permission.READ_EXTERNAL_STORAGE, permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION) 25 | .request { _: Boolean, _: List?, _: List? -> resetToolbar() } 26 | } 27 | 28 | private fun resetToolbar() { 29 | supportActionBar?.title = "DLNA Cast" 30 | supportActionBar?.subtitle = "${Utils.getWiFiName(this)} - ${Utils.getWiFiIpAddress(this)}" 31 | } 32 | 33 | override fun onStart() { 34 | super.onStart() 35 | resetToolbar() 36 | DLNACastManager.bindCastService(this) 37 | } 38 | 39 | override fun onBackPressed() { 40 | val detailFragment = supportFragmentManager.findFragmentById(R.id.detail_container) 41 | if (detailFragment != null) { 42 | supportFragmentManager.beginTransaction().remove(detailFragment).commit() 43 | return 44 | } 45 | super.onBackPressed() 46 | } 47 | 48 | override fun onDestroy() { 49 | DLNACastManager.unbindCastService(this) 50 | super.onDestroy() 51 | } 52 | 53 | override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { 54 | val result = super.onKeyDown(keyCode, event) 55 | (supportFragmentManager.findFragmentById(R.id.detail_container) as? OnKeyEventHandler)?.onKeyDown(keyCode, event) 56 | return result 57 | } 58 | 59 | override fun onItemClick(device: Device<*, *, *>) { 60 | replace(R.id.detail_container, DetailFragment.create(device)) 61 | } 62 | 63 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 64 | menuInflater.inflate(R.menu.main_options, menu) 65 | return super.onCreateOptionsMenu(menu) 66 | } 67 | 68 | @SuppressLint("NonConstantResourceId") 69 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 70 | if (item.itemId == R.id.menu_search) { 71 | Toast.makeText(this, "开始搜索...", Toast.LENGTH_SHORT).show() 72 | // DLNACastManager.search(DLNACastManager.DEVICE_TYPE_MEDIA_RENDERER, 60) 73 | DLNACastManager.search() 74 | } 75 | 76 | return super.onOptionsItemSelected(item) 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/cast/dlna/demo/fragment/DeviceInfoFragment.kt: -------------------------------------------------------------------------------- 1 | package com.android.cast.dlna.demo.fragment 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.TextView 9 | import androidx.fragment.app.Fragment 10 | import com.android.cast.dlna.demo.DetailContainer 11 | import com.android.cast.dlna.demo.R 12 | import com.android.cast.dlna.demo.R.layout 13 | import com.android.cast.dlna.dmc.DLNACastManager 14 | import com.android.cast.dlna.dmc.control.DeviceControl 15 | import com.android.cast.dlna.dmc.control.OnDeviceControlListener 16 | import com.android.cast.dlna.dmc.control.ServiceActionCallback 17 | import org.fourthline.cling.model.meta.Device 18 | import org.fourthline.cling.support.model.MediaInfo 19 | import org.fourthline.cling.support.model.TransportInfo 20 | 21 | class DeviceInfoFragment : Fragment() { 22 | 23 | private val device: Device<*, *, *> by lazy { (requireParentFragment() as DetailContainer).getDevice() } 24 | private val mediaInfo: TextView by lazy { requireView().findViewById(R.id.info_device_media) } 25 | private val transportInfo: TextView by lazy { requireView().findViewById(R.id.info_device_transport) } 26 | private val volume: TextView by lazy { requireView().findViewById(R.id.info_device_volume) } 27 | private val deviceControl: DeviceControl by lazy { DLNACastManager.connectDevice(device, object : OnDeviceControlListener {}) } 28 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 29 | return inflater.inflate(layout.fragment_info, container, false) 30 | } 31 | 32 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 33 | super.onViewCreated(view, savedInstanceState) 34 | view.findViewById(R.id.info_get_volume).setOnClickListener { 35 | deviceControl.getVolume(object : ServiceActionCallback { 36 | @SuppressLint("SetTextI18n") 37 | override fun onSuccess(result: Int) { 38 | volume.text = "音量: $result\n" 39 | } 40 | 41 | override fun onFailure(msg: String) { 42 | volume.text = msg 43 | } 44 | }) 45 | } 46 | view.findViewById(R.id.info_get_media).setOnClickListener { 47 | deviceControl.getMediaInfo(object : ServiceActionCallback { 48 | @SuppressLint("SetTextI18n") 49 | override fun onSuccess(result: MediaInfo) { 50 | mediaInfo.text = "currentURI:\n ${result.currentURI}\n\n" + 51 | "nextURI:\n ${result.nextURI}" 52 | } 53 | 54 | override fun onFailure(msg: String) { 55 | mediaInfo.text = msg 56 | } 57 | }) 58 | } 59 | view.findViewById(R.id.info_get_transport).setOnClickListener { 60 | deviceControl.getTransportInfo(object : ServiceActionCallback { 61 | @SuppressLint("SetTextI18n") 62 | override fun onSuccess(result: TransportInfo) { 63 | transportInfo.text = "currentTransportState: ${result.currentTransportState}\n" + 64 | "currentTransportStatus: ${result.currentTransportStatus}\n" + 65 | "currentSpeed: ${result.currentSpeed}\n" 66 | } 67 | 68 | override fun onFailure(msg: String) { 69 | transportInfo.text = msg 70 | } 71 | }) 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/cast/dlna/demo/fragment/DeviceListFragment.kt: -------------------------------------------------------------------------------- 1 | package com.android.cast.dlna.demo.fragment 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.View.OnClickListener 9 | import android.view.ViewGroup 10 | import android.widget.TextView 11 | import androidx.fragment.app.Fragment 12 | import androidx.recyclerview.widget.DividerItemDecoration 13 | import androidx.recyclerview.widget.LinearLayoutManager 14 | import androidx.recyclerview.widget.RecyclerView 15 | import androidx.recyclerview.widget.RecyclerView.Adapter 16 | import androidx.recyclerview.widget.RecyclerView.ViewHolder 17 | import com.android.cast.dlna.demo.R 18 | import com.android.cast.dlna.dmc.DLNACastManager 19 | import com.android.cast.dlna.dmc.OnDeviceRegistryListener 20 | import org.fourthline.cling.model.meta.Device 21 | 22 | class DeviceListFragment : Fragment() { 23 | private lateinit var adapter: DeviceAdapter 24 | 25 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 26 | return inflater.inflate(R.layout.fragment_device_list, container, false) 27 | } 28 | 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | 32 | val recyclerView = view.findViewById(R.id.recycler_view) 33 | recyclerView.isNestedScrollingEnabled = false 34 | recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) 35 | recyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) 36 | recyclerView.adapter = DeviceAdapter(requireContext(), requireActivity() as OnItemClickListener).also { adapter = it } 37 | 38 | DLNACastManager.registerDeviceListener(adapter) 39 | } 40 | 41 | override fun onDestroyView() { 42 | DLNACastManager.unregisterListener(adapter) 43 | super.onDestroyView() 44 | } 45 | } 46 | 47 | interface OnItemClickListener { 48 | fun onItemClick(device: Device<*, *, *>) 49 | } 50 | 51 | @SuppressLint("NotifyDataSetChanged") 52 | private class DeviceAdapter( 53 | context: Context, 54 | private val listener: OnItemClickListener, 55 | ) : Adapter(), OnDeviceRegistryListener { 56 | 57 | private val layoutInflater: LayoutInflater = LayoutInflater.from(context) 58 | private val deviceList: MutableList> = mutableListOf() 59 | 60 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceHolder { 61 | return DeviceHolder(layoutInflater.inflate(R.layout.item_device, parent, false), listener) 62 | } 63 | 64 | override fun onBindViewHolder(holder: DeviceHolder, position: Int) { 65 | holder.setData(getItem(position)) 66 | } 67 | 68 | override fun getItemCount(): Int = deviceList.size 69 | 70 | private fun getItem(position: Int): Device<*, *, *>? { 71 | return if (position < 0 || position >= itemCount) { 72 | null 73 | } else deviceList[position] 74 | } 75 | 76 | override fun onDeviceAdded(device: Device<*, *, *>) { 77 | if (!deviceList.contains(device)) { 78 | deviceList.add(device) 79 | notifyDataSetChanged() 80 | } 81 | } 82 | 83 | override fun onDeviceRemoved(device: Device<*, *, *>) { 84 | if (deviceList.contains(device)) { 85 | deviceList.remove(device) 86 | notifyDataSetChanged() 87 | } 88 | } 89 | } 90 | 91 | private class DeviceHolder( 92 | itemView: View, 93 | private val itemSelectedListener: OnItemClickListener, 94 | ) : ViewHolder(itemView), OnClickListener { 95 | 96 | private val deviceName: TextView = itemView.findViewById(R.id.device_name) 97 | private val deviceDescription: TextView = itemView.findViewById(R.id.device_description) 98 | private val deviceType: TextView = itemView.findViewById(R.id.device_type) 99 | private val deviceId: TextView = itemView.findViewById(R.id.device_id) 100 | 101 | init { 102 | itemView.setOnClickListener(this) 103 | } 104 | 105 | fun setData(device: Device<*, *, *>?) { 106 | itemView.tag = device 107 | device?.apply { 108 | deviceName.text = details.friendlyName 109 | deviceDescription.text = details.manufacturerDetails.manufacturer 110 | deviceType.text = type.type 111 | deviceId.text = identity.udn.identifierString 112 | } 113 | } 114 | 115 | override fun onClick(v: View) { 116 | (v.tag as? Device<*, *, *>)?.also { device -> 117 | itemSelectedListener.onItemClick(device) 118 | } 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/cast/dlna/demo/fragment/DeviceServiceActionFragment.kt: -------------------------------------------------------------------------------- 1 | package com.android.cast.dlna.demo.fragment 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import androidx.fragment.app.Fragment 9 | import com.android.cast.dlna.demo.DetailContainer 10 | import com.android.cast.dlna.demo.R 11 | import com.android.cast.dlna.demo.R.layout 12 | import org.fourthline.cling.model.meta.Action 13 | import org.fourthline.cling.model.meta.Device 14 | import org.fourthline.cling.model.meta.Service 15 | 16 | class DeviceServiceActionFragment : Fragment() { 17 | private val device: Device<*, *, *> by lazy { (requireParentFragment() as DetailContainer).getDevice() } 18 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 19 | return inflater.inflate(layout.fragment_device_service_action, container, false) 20 | } 21 | 22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 23 | super.onViewCreated(view, savedInstanceState) 24 | view.findViewById(R.id.info_device_name)?.text = device.details?.friendlyName 25 | view.findViewById(R.id.info_device_status)?.text = buildDeviceInfo(device) 26 | } 27 | 28 | private fun buildDeviceInfo(device: Device<*, *, *>): String { 29 | val builder = StringBuilder() 30 | device.details.baseURL?.let { url -> builder.append("URL: $url\n") } 31 | builder.append("DeviceType: ${device.type.type}\n") 32 | (device.services as? Array>)?.forEach { service -> 33 | builder.append("\n") 34 | builder.append("ServiceType: ").append(service.serviceType.type).append("\n") 35 | val list = mutableListOf(*service.actions) 36 | list.sortWith { o1: Action<*>, o2: Action<*> -> o1.name.compareTo(o2.name) } 37 | builder.append("Action: ") 38 | for (action in list) { 39 | builder.append(action.name).append(", ") 40 | } 41 | builder.append("\n") 42 | } 43 | return builder.toString() 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/cast/dlna/demo/fragment/UrlDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.android.cast.dlna.demo.fragment 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.View.OnClickListener 8 | import android.view.ViewGroup 9 | import android.widget.TextView 10 | import androidx.fragment.app.DialogFragment 11 | import androidx.fragment.app.FragmentManager 12 | import androidx.recyclerview.widget.LinearLayoutManager 13 | import androidx.recyclerview.widget.RecyclerView 14 | import androidx.recyclerview.widget.RecyclerView.Adapter 15 | import androidx.recyclerview.widget.RecyclerView.ViewHolder 16 | import com.android.cast.dlna.demo.R 17 | import com.android.cast.dlna.demo.VideoUrl 18 | import com.android.cast.dlna.demo.videoUrlList 19 | 20 | interface OnUrlSelectListener { 21 | fun onUrlSelected(video: VideoUrl) 22 | } 23 | 24 | class CastUrlDialogFragment : DialogFragment() { 25 | 26 | companion object { 27 | fun show(fragmentManager: FragmentManager, listener: OnUrlSelectListener? = null) { 28 | CastUrlDialogFragment().apply { 29 | this.onUrlSelectListener = listener 30 | }.show(fragmentManager, "CastFragment") 31 | } 32 | } 33 | 34 | var onUrlSelectListener: OnUrlSelectListener? = null 35 | 36 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 37 | setStyle(STYLE_NO_TITLE, theme) 38 | return super.onCreateDialog(savedInstanceState) 39 | } 40 | 41 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 42 | return inflater.inflate(R.layout.fragment_cast, container, false) 43 | } 44 | 45 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 46 | super.onViewCreated(view, savedInstanceState) 47 | view.findViewById(R.id.recycler_view).apply { 48 | layoutManager = LinearLayoutManager(requireContext()) 49 | adapter = ListAdapter() 50 | } 51 | } 52 | 53 | private inner class ListAdapter : Adapter() { 54 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder { 55 | return ListViewHolder(LayoutInflater.from(requireContext()).inflate(R.layout.item_video_url, parent, false)) 56 | } 57 | 58 | override fun onBindViewHolder(holder: ListViewHolder, position: Int) { 59 | holder.setData(videoUrlList[position]) 60 | } 61 | 62 | override fun getItemCount(): Int = videoUrlList.size 63 | } 64 | 65 | private inner class ListViewHolder(itemView: View) : ViewHolder(itemView), OnClickListener { 66 | private val url: TextView = itemView.findViewById(R.id.video_url) 67 | private val title: TextView = itemView.findViewById(R.id.video_title) 68 | 69 | init { 70 | itemView.setOnClickListener(this) 71 | } 72 | 73 | fun setData(data: VideoUrl) { 74 | itemView.tag = data 75 | url.text = data.url 76 | title.text = data.title 77 | } 78 | 79 | override fun onClick(v: View) { 80 | (onUrlSelectListener 81 | ?: parentFragment as? OnUrlSelectListener 82 | ?: activity as? OnUrlSelectListener)?.onUrlSelected(v.tag as VideoUrl) 83 | dismiss() 84 | } 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_connected.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_fast.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_mute.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_next.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_pause.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_phone.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_play.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_previous.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_slow.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_stop.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_tv.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cast_video.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/common_button_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/common_button_bg_night.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 14 | 17 | 20 | 23 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/local_http.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/search.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 23 | 24 | 25 | 26 | 29 | 30 | 35 | 36 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_cast.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 22 | 23 | 31 | 32 | 33 | 34 | 46 | 47 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 23 | 24 | 28 | 29 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_device_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_device_service_action.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 27 | 28 | 37 | 38 | 39 | 40 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 |