├── .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 | [](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 | 
35 | 
36 | 
37 | 
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 | [](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 | 
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 |
23 |
24 |
31 |
32 |
36 |
37 |
45 |
46 |
53 |
54 |
58 |
59 |
67 |
68 |
75 |
76 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_browse_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
20 |
21 |
30 |
31 |
41 |
42 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_device.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
27 |
28 |
38 |
39 |
51 |
52 |
63 |
64 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_video_url.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
25 |
26 |
41 |
42 |
46 |
47 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/main_options.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 | #63A7DC
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DLNA 投屏
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext {
4 | kotlin_version = '1.6.21'
5 | }
6 | repositories {
7 | google()
8 | mavenCentral()
9 | mavenLocal()
10 | maven {
11 | url 'http://4thline.org/m2'
12 | allowInsecureProtocol = true
13 | }
14 | }
15 |
16 | dependencies {
17 | classpath 'com.android.tools.build:gradle:7.4.2'
18 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
19 | }
20 | }
21 |
22 | allprojects {
23 | repositories {
24 | google()
25 | mavenCentral()
26 | mavenLocal()
27 | maven {
28 | url 'http://4thline.org/m2'
29 | allowInsecureProtocol = true
30 | }
31 | }
32 | }
33 |
34 | tasks.register('clean', Delete) {
35 | delete rootProject.buildDir
36 | }
37 |
--------------------------------------------------------------------------------
/dlna-core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/dlna-core/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'maven-publish'
5 | }
6 |
7 | android {
8 | namespace 'com.android.cast.dlna.core'
9 | compileSdk 32
10 | defaultConfig {
11 | minSdk 24
12 | targetSdk 32
13 | consumerProguardFiles "consumer-rules.pro"
14 | }
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 | compileOptions {
22 | sourceCompatibility JavaVersion.VERSION_1_8
23 | targetCompatibility JavaVersion.VERSION_1_8
24 | }
25 | }
26 |
27 | dependencies {
28 | // Cling library required
29 | api 'org.fourthline.cling:cling-core:2.1.1'
30 | api 'org.fourthline.cling:cling-support:2.1.1'
31 | // Servlet
32 | api 'javax.servlet:javax.servlet-api:3.1.0'
33 | // Jetty
34 | api 'org.eclipse.jetty:jetty-server:8.1.21.v20160908'
35 | api 'org.eclipse.jetty:jetty-servlet:8.1.21.v20160908'
36 | api 'org.eclipse.jetty:jetty-client:8.1.21.v20160908'
37 | // Nano http
38 | api 'org.nanohttpd:nanohttpd:2.3.1'
39 | // Kotlin
40 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
41 | }
42 | repositories {
43 | mavenCentral()
44 | }
45 | apply from: './publish.gradle'
--------------------------------------------------------------------------------
/dlna-core/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/dlna-core/consumer-rules.pro
--------------------------------------------------------------------------------
/dlna-core/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
--------------------------------------------------------------------------------
/dlna-core/publish.gradle:
--------------------------------------------------------------------------------
1 | // Because the components are created only during the afterEvaluate phase, you must
2 | // configure your publications using the afterEvaluate() lifecycle method.
3 | afterEvaluate {
4 | publishing {
5 | publications {
6 | // Creates a Maven publication called "release".
7 | release(MavenPublication) {
8 | // Applies the component for the release build variant.
9 | from components.release
10 | // You can then customize attributes of the publication as shown below.
11 | groupId = GROUP
12 | artifactId = 'core'
13 | version = VERSION_RELEASE
14 | }
15 | // Creates a Maven publication called “debug”.
16 | debug(MavenPublication) {
17 | // Applies the component for the debug build variant.
18 | from components.debug
19 | groupId = GROUP
20 | artifactId = 'core'
21 | version = VERSION_DEBUG
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/dlna-core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/dlna-core/src/main/java/com/android/cast/dlna/core/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.core
2 |
3 | import android.util.Log
4 | import com.android.cast.dlna.core.Logger.Printer
5 |
6 | fun getLogger(tag: String): Logger = Logger.create(tag)
7 |
8 | object Level {
9 | const val V = 10
10 | const val D = 20
11 | const val I = 30
12 | const val W = 40
13 | const val E = 50
14 | }
15 |
16 | class Logger(private val tag: String) {
17 | companion object {
18 | var prefixTag: String = "DLNA_"
19 | var enabled: Boolean = true
20 | var level: Int = Level.I
21 | var printThread: Boolean = false
22 |
23 | var printer: Printer = Printer { level, tag, message, throwable ->
24 | if (throwable != null) {
25 | when (level) {
26 | Level.V -> Log.v(tag, message.toString(), throwable)
27 | Level.D -> Log.d(tag, message.toString(), throwable)
28 | Level.I -> Log.i(tag, message.toString(), throwable)
29 | Level.W -> Log.w(tag, message.toString(), throwable)
30 | Level.E -> Log.e(tag, message.toString(), throwable)
31 | }
32 | } else {
33 | when (level) {
34 | Level.V -> Log.v(tag, message.toString())
35 | Level.D -> Log.d(tag, message.toString())
36 | Level.I -> Log.i(tag, message.toString())
37 | Level.W -> Log.w(tag, message.toString())
38 | Level.E -> Log.e(tag, message.toString())
39 | }
40 | }
41 | }
42 |
43 | fun create(tag: String) = Logger(tag)
44 | }
45 |
46 | fun v(message: CharSequence, throwable: Throwable? = null) {
47 | if (enabled && Level.V >= level) {
48 | printer.print(Level.V, getTag(), message, throwable)
49 | }
50 | }
51 |
52 | fun v(throwable: Throwable?, function: () -> CharSequence) {
53 | if (enabled && Level.V >= level) {
54 | printer.print(Level.V, getTag(), function(), throwable)
55 | }
56 | }
57 |
58 | fun d(message: CharSequence, throwable: Throwable? = null) {
59 | if (enabled && Level.D >= level) {
60 | printer.print(Level.D, getTag(), message, throwable)
61 | }
62 | }
63 |
64 | fun d(throwable: Throwable?, function: () -> CharSequence) {
65 | if (enabled && Level.D >= level) {
66 | printer.print(Level.D, getTag(), function(), throwable)
67 | }
68 | }
69 |
70 | fun i(message: CharSequence, throwable: Throwable? = null) {
71 | if (enabled && Level.I >= level) {
72 | printer.print(Level.I, getTag(), message, throwable)
73 | }
74 | }
75 |
76 | fun i(throwable: Throwable?, function: () -> CharSequence) {
77 | if (enabled && Level.I >= level) {
78 | printer.print(Level.I, getTag(), function(), throwable)
79 | }
80 | }
81 |
82 | fun w(message: CharSequence, throwable: Throwable? = null) {
83 | if (enabled && Level.W >= level) {
84 | printer.print(Level.W, getTag(), message, throwable)
85 | }
86 | }
87 |
88 | fun w(throwable: Throwable?, function: () -> CharSequence) {
89 | if (enabled && Level.W >= level) {
90 | printer.print(Level.W, getTag(), function(), throwable)
91 | }
92 | }
93 |
94 | fun e(message: CharSequence, throwable: Throwable? = null) {
95 | if (enabled && Level.E >= level) {
96 | printer.print(Level.E, getTag(), message, throwable)
97 | }
98 | }
99 |
100 | fun e(throwable: Throwable?, function: () -> CharSequence) {
101 | if (enabled && Level.E >= level) {
102 | printer.print(Level.E, getTag(), function(), throwable)
103 | }
104 | }
105 |
106 | private fun getTag(): String {
107 | return prefixTag + tag + (if (printThread) "[${Thread.currentThread().name}]" else "")
108 | }
109 |
110 | fun interface Printer {
111 | fun print(level: Int, tag: String, message: CharSequence, throwable: Throwable?)
112 | }
113 | }
--------------------------------------------------------------------------------
/dlna-core/src/main/java/com/android/cast/dlna/core/http/ContentResourceServlet.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 Kevin Shen
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.android.cast.dlna.core.http
17 |
18 | import org.eclipse.jetty.servlet.DefaultServlet
19 | import org.eclipse.jetty.util.resource.FileResource
20 | import org.eclipse.jetty.util.resource.Resource
21 | import java.io.File
22 |
23 | internal open class ContentResourceServlet : DefaultServlet() {
24 | override fun getResource(pathInContext: String): Resource? {
25 | // String id = Utils.parseResourceId(pathInContext);
26 | // content://media/external/video/media/1611127029319529
27 | // Uri uri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, Long.parseLong(id));
28 | return try {
29 | File(pathInContext).takeIf { it.exists() }?.let { FileResource.newResource(it) }
30 | } catch (e: Exception) {
31 | e.printStackTrace()
32 | null
33 | }
34 | }
35 |
36 | class VideoResourceServlet : ContentResourceServlet()
37 | class AudioResourceServlet : ContentResourceServlet()
38 | }
--------------------------------------------------------------------------------
/dlna-core/src/main/java/com/android/cast/dlna/core/http/HttpLocalServer.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.core.http
2 |
3 | import android.content.Context
4 | import com.android.cast.dlna.core.Utils
5 |
6 | interface HttpServer {
7 | fun startServer()
8 | fun stopServer()
9 | fun isRunning(): Boolean
10 | }
11 |
12 | class LocalServer(
13 | context: Context,
14 | private val port: Int = 8192,
15 | jetty: Boolean = true,
16 | httpServer: HttpServer = if (jetty) JettyHttpServer(port) else NanoHttpServer(port),
17 | ) : HttpServer by httpServer {
18 | val ip: String = Utils.getWiFiIpAddress(context)
19 | val baseUrl: String = "http://$ip:$port"
20 | }
--------------------------------------------------------------------------------
/dlna-core/src/main/java/com/android/cast/dlna/core/http/HttpServer.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.core.http
2 |
3 | import android.text.TextUtils
4 | import com.android.cast.dlna.core.Logger
5 | import fi.iki.elonen.NanoHTTPD
6 | import fi.iki.elonen.NanoHTTPD.Response.Status.BAD_REQUEST
7 | import fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND
8 | import fi.iki.elonen.NanoHTTPD.Response.Status.OK
9 | import fi.iki.elonen.NanoHTTPD.Response.Status.SERVICE_UNAVAILABLE
10 | import org.eclipse.jetty.server.Server
11 | import org.eclipse.jetty.servlet.ServletContextHandler
12 | import java.io.File
13 | import java.io.FileInputStream
14 | import java.io.FileNotFoundException
15 | import java.io.IOException
16 |
17 | // ------------------------------------------------
18 | // ---- Jetty Http
19 | // ------------------------------------------------
20 | internal class JettyHttpServer(port: Int) : HttpServer {
21 | private val logger = Logger.create("JettyHttpServer")
22 | private val server: Server = Server(port) // Has its own QueuedThreadPool
23 |
24 | init {
25 | server.gracefulShutdown = 1000 // Let's wait a second for ongoing transfers to complete
26 | }
27 |
28 | @Synchronized
29 | override fun startServer() {
30 | if (!server.isStarted && !server.isStarting) {
31 | Thread {
32 | val context = ServletContextHandler()
33 | context.contextPath = "/"
34 | context.setInitParameter("org.eclipse.jetty.servlet.Default.gzip", "false")
35 | // context.addServlet(ContentResourceServlet.AudioResourceServlet.class, "/audio/*");
36 | // context.addServlet(ContentResourceServlet.VideoResourceServlet.class, "/video/*");
37 | context.addServlet(ContentResourceServlet::class.java, "/")
38 | server.handler = context
39 | logger.i("JettyServer start.")
40 | try {
41 | server.start()
42 | server.join()
43 | } catch (ex: Exception) {
44 | ex.printStackTrace()
45 | logger.e("Error", ex)
46 | } finally {
47 | logger.i("JettyServer complete.")
48 | }
49 | }.start()
50 | }
51 | }
52 |
53 | @Synchronized
54 | override fun stopServer() {
55 | if (!server.isStopped && !server.isStopping) {
56 | try {
57 | server.stop()
58 | } catch (ex: Exception) {
59 | logger.e("Error", ex)
60 | ex.printStackTrace()
61 | } finally {
62 | logger.i("JettyServer stop.")
63 | }
64 | }
65 | }
66 |
67 | override fun isRunning(): Boolean = server.isRunning
68 | }
69 |
70 | // ------------------------------------------------
71 | // ---- Nano Http
72 | // ------------------------------------------------
73 | internal class NanoHttpServer(port: Int) : NanoHTTPD(port), HttpServer {
74 |
75 | private val mimeType = mutableMapOf(
76 | "jpg" to "image/*",
77 | "jpeg" to "image/*",
78 | "png" to "image/*",
79 | "mp3" to "audio/*",
80 | "mp4" to "video/*",
81 | "wav" to "video/*",
82 | )
83 | private val textPlain = "text/plain"
84 |
85 | override fun serve(session: IHTTPSession): Response {
86 | println("uri: " + session.uri)
87 | println("header: " + session.headers.toString())
88 | println("params: " + session.parms.toString())
89 | val uri = session.uri
90 | if (TextUtils.isEmpty(uri) || !uri.startsWith("/")) {
91 | return newChunkedResponse(BAD_REQUEST, textPlain, null)
92 | }
93 | val file = File(uri)
94 | if (!file.exists() || file.isDirectory) {
95 | return newChunkedResponse(NOT_FOUND, textPlain, null)
96 | }
97 | val type = uri.substring(uri.length.coerceAtMost(uri.lastIndexOf(".") + 1)).lowercase()
98 | var mimeType = mimeType[type]
99 | if (TextUtils.isEmpty(mimeType)) {
100 | mimeType = textPlain
101 | }
102 | return try {
103 | newChunkedResponse(OK, mimeType, FileInputStream(file))
104 | } catch (e: FileNotFoundException) {
105 | e.printStackTrace()
106 | newChunkedResponse(SERVICE_UNAVAILABLE, mimeType, null)
107 | }
108 | }
109 |
110 | override fun startServer() {
111 | try {
112 | if (!wasStarted()) start()
113 | } catch (e: IOException) {
114 | e.printStackTrace()
115 | }
116 | }
117 |
118 | override fun stopServer() {
119 | stop()
120 | }
121 |
122 | override fun isRunning(): Boolean = wasStarted()
123 | }
--------------------------------------------------------------------------------
/dlna-dmc/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/dlna-dmc/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'maven-publish'
5 | }
6 |
7 | android {
8 | namespace 'com.android.cast.dlna.dmc'
9 | compileSdk 32
10 | defaultConfig {
11 | minSdk 24
12 | targetSdk 32
13 | consumerProguardFiles "consumer-rules.pro"
14 | }
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 | compileOptions {
22 | sourceCompatibility JavaVersion.VERSION_1_8
23 | targetCompatibility JavaVersion.VERSION_1_8
24 | }
25 | kotlinOptions {
26 | jvmTarget = '1.8'
27 | }
28 | }
29 |
30 | dependencies {
31 | implementation fileTree(dir: 'libs', include: ['*.jar'])
32 |
33 | api(project(':dlna-core'))
34 |
35 | //noinspection GradleDependency
36 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
37 | }
38 | apply from: './publish.gradle'
--------------------------------------------------------------------------------
/dlna-dmc/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/dlna-dmc/consumer-rules.pro
--------------------------------------------------------------------------------
/dlna-dmc/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
--------------------------------------------------------------------------------
/dlna-dmc/publish.gradle:
--------------------------------------------------------------------------------
1 | // Because the components are created only during the afterEvaluate phase, you must
2 | // configure your publications using the afterEvaluate() lifecycle method.
3 | afterEvaluate {
4 | publishing {
5 | publications {
6 | // Creates a Maven publication called "release".
7 | release(MavenPublication) {
8 | // Applies the component for the release build variant.
9 | from components.release
10 | // You can then customize attributes of the publication as shown below.
11 | groupId = GROUP
12 | artifactId = 'dmc'
13 | version = VERSION_RELEASE
14 | }
15 | // Creates a Maven publication called “debug”.
16 | debug(MavenPublication) {
17 | // Applies the component for the debug build variant.
18 | from components.debug
19 | groupId = GROUP
20 | artifactId = 'dmc'
21 | version = VERSION_DEBUG
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/dlna-dmc/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dlna-dmc/src/main/java/com/android/cast/dlna/dmc/DLNACallback.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmc
2 |
3 | import org.fourthline.cling.model.meta.Device
4 |
5 | /**
6 | * this listener call in UI thread.
7 | */
8 | interface OnDeviceRegistryListener {
9 | fun onDeviceAdded(device: Device<*, *, *>) {}
10 | fun onDeviceRemoved(device: Device<*, *, *>) {}
11 | }
12 |
--------------------------------------------------------------------------------
/dlna-dmc/src/main/java/com/android/cast/dlna/dmc/DLNACastService.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmc
2 |
3 | import android.content.Intent
4 | import com.android.cast.dlna.core.Logger
5 | import org.fourthline.cling.UpnpServiceConfiguration
6 | import org.fourthline.cling.android.AndroidUpnpServiceConfiguration
7 | import org.fourthline.cling.android.AndroidUpnpServiceImpl
8 | import org.fourthline.cling.model.types.ServiceType
9 |
10 | class DLNACastService : AndroidUpnpServiceImpl() {
11 | private val logger = Logger.create("CastService")
12 | override fun onCreate() {
13 | logger.i("DLNACastService onCreate")
14 | // LoggingUtil.resetRootHandler(FixedAndroidLogHandler())
15 | super.onCreate()
16 | }
17 |
18 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
19 | logger.i("DLNACastService onStartCommand: $flags, $startId, $intent")
20 | return super.onStartCommand(intent, flags, startId)
21 | }
22 |
23 | override fun onDestroy() {
24 | logger.w("DLNACastService onDestroy")
25 | super.onDestroy()
26 | }
27 |
28 | override fun createConfiguration(): UpnpServiceConfiguration = object : AndroidUpnpServiceConfiguration() {
29 | override fun getExclusiveServiceTypes(): Array = arrayOf(
30 | DLNACastManager.SERVICE_TYPE_AV_TRANSPORT,
31 | DLNACastManager.SERVICE_TYPE_RENDERING_CONTROL,
32 | DLNACastManager.SERVICE_TYPE_CONTENT_DIRECTORY,
33 | DLNACastManager.SERVICE_CONNECTION_MANAGER
34 | )
35 | }
36 | }
--------------------------------------------------------------------------------
/dlna-dmc/src/main/java/com/android/cast/dlna/dmc/DeviceRegistryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmc
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import com.android.cast.dlna.core.Logger
6 | import org.fourthline.cling.model.meta.Device
7 | import org.fourthline.cling.registry.DefaultRegistryListener
8 | import org.fourthline.cling.registry.Registry
9 |
10 | /**
11 | *
12 | */
13 | internal class DeviceRegistryImpl(private val deviceRegistryListener: OnDeviceRegistryListener) : DefaultRegistryListener() {
14 |
15 | private val logger = Logger.create("DeviceRegistry")
16 | private val handler = Handler(Looper.getMainLooper())
17 |
18 | override fun deviceAdded(registry: Registry, device: Device<*, *, *>) {
19 | logger.i("deviceAdded: " + parseDeviceInfo(device))
20 | // logger.i(parseDeviceService(device))
21 | handler.post { deviceRegistryListener.onDeviceAdded(device) }
22 | }
23 |
24 | override fun deviceRemoved(registry: Registry, device: Device<*, *, *>) {
25 | logger.w("deviceRemoved: " + parseDeviceInfo(device))
26 | handler.post { deviceRegistryListener.onDeviceRemoved(device) }
27 | }
28 |
29 | private fun parseDeviceInfo(device: Device<*, *, *>): String = "[${device.type.type}]" +
30 | "[${device.details.friendlyName}]" +
31 | "[${device.identity.udn.identifierString.split("-").last()}]"
32 | }
--------------------------------------------------------------------------------
/dlna-dmc/src/main/java/com/android/cast/dlna/dmc/control/CastControlImpl.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmc.control
2 |
3 | import com.android.cast.dlna.dmc.DLNACastManager
4 | import com.android.cast.dlna.dmc.control.BaseServiceExecutor.AVServiceExecutorImpl
5 | import com.android.cast.dlna.dmc.control.BaseServiceExecutor.ContentServiceExecutorImpl
6 | import com.android.cast.dlna.dmc.control.BaseServiceExecutor.RendererServiceExecutorImpl
7 | import org.fourthline.cling.controlpoint.ControlPoint
8 | import org.fourthline.cling.model.meta.Device
9 | import org.fourthline.cling.support.avtransport.lastchange.AVTransportLastChangeParser
10 | import org.fourthline.cling.support.lastchange.EventedValue
11 | import org.fourthline.cling.support.model.BrowseFlag
12 | import org.fourthline.cling.support.model.DIDLContent
13 | import org.fourthline.cling.support.model.MediaInfo
14 | import org.fourthline.cling.support.model.PositionInfo
15 | import org.fourthline.cling.support.model.TransportInfo
16 | import org.fourthline.cling.support.renderingcontrol.lastchange.RenderingControlLastChangeParser
17 |
18 | class CastControlImpl(
19 | controlPoint: ControlPoint,
20 | device: Device<*, *, *>,
21 | listener: OnDeviceControlListener,
22 | ) : DeviceControl {
23 |
24 | private val avTransportService: AVServiceExecutorImpl
25 | private val renderService: RendererServiceExecutorImpl
26 | private val contentService: ContentServiceExecutorImpl
27 | var released = false
28 |
29 | init {
30 | avTransportService = AVServiceExecutorImpl(controlPoint, device.findService(DLNACastManager.SERVICE_TYPE_AV_TRANSPORT))
31 | avTransportService.subscribe(object : SubscriptionListener {
32 | override fun failed(subscriptionId: String?) {
33 | if (!released) listener.onDisconnected(device)
34 | }
35 |
36 | override fun established(subscriptionId: String?) {
37 | if (!released) listener.onConnected(device)
38 | }
39 |
40 | override fun ended(subscriptionId: String?) {
41 | if (!released) listener.onDisconnected(device)
42 | }
43 |
44 | override fun onReceived(subscriptionId: String?, event: EventedValue<*>) {
45 | if (!released) listener.onEventChanged(event)
46 | }
47 | }, AVTransportLastChangeParser())
48 | renderService = RendererServiceExecutorImpl(controlPoint, device.findService(DLNACastManager.SERVICE_TYPE_RENDERING_CONTROL))
49 | renderService.subscribe(object : SubscriptionListener {}, RenderingControlLastChangeParser())
50 | contentService = ContentServiceExecutorImpl(controlPoint, device.findService(DLNACastManager.SERVICE_TYPE_CONTENT_DIRECTORY))
51 | //TODO: check the parser
52 | contentService.subscribe(object : SubscriptionListener {}, AVTransportLastChangeParser())
53 | }
54 |
55 | // --------------------------------------------------------
56 | // ---- AvTransport ---------------------------------------
57 | // --------------------------------------------------------
58 | override fun setAVTransportURI(uri: String, title: String, callback: ServiceActionCallback?) {
59 | avTransportService.setAVTransportURI(uri, title, callback)
60 | }
61 |
62 | override fun setNextAVTransportURI(uri: String, title: String, callback: ServiceActionCallback?) {
63 | avTransportService.setNextAVTransportURI(uri, title, callback)
64 | }
65 |
66 | override fun play(speed: String, callback: ServiceActionCallback?) {
67 | avTransportService.play(speed, callback)
68 | }
69 |
70 | override fun pause(callback: ServiceActionCallback?) {
71 | avTransportService.pause(callback)
72 | }
73 |
74 | override fun seek(millSeconds: Long, callback: ServiceActionCallback?) {
75 | avTransportService.seek(millSeconds, callback)
76 | }
77 |
78 | override fun stop(callback: ServiceActionCallback?) {
79 | avTransportService.stop(callback)
80 | }
81 |
82 | override fun next(callback: ServiceActionCallback?) {
83 | avTransportService.next(callback)
84 | }
85 |
86 | override fun canNext(callback: ServiceActionCallback?) {
87 | avTransportService.canNext(callback)
88 | }
89 |
90 | override fun previous(callback: ServiceActionCallback?) {
91 | avTransportService.previous(callback)
92 | }
93 |
94 | override fun canPrevious(callback: ServiceActionCallback?) {
95 | avTransportService.canPrevious(callback)
96 | }
97 |
98 | override fun getMediaInfo(callback: ServiceActionCallback?) {
99 | avTransportService.getMediaInfo(callback)
100 | }
101 |
102 | override fun getPositionInfo(callback: ServiceActionCallback?) {
103 | avTransportService.getPositionInfo(callback)
104 | }
105 |
106 | override fun getTransportInfo(callback: ServiceActionCallback?) {
107 | avTransportService.getTransportInfo(callback)
108 | }
109 |
110 | // --------------------------------------------------------
111 | // ---- Renderer ------------------------------------------
112 | // --------------------------------------------------------
113 | override fun setVolume(volume: Int, callback: ServiceActionCallback?) {
114 | renderService.setVolume(volume, callback)
115 | }
116 |
117 | override fun getVolume(callback: ServiceActionCallback?) {
118 | renderService.getVolume(callback)
119 | }
120 |
121 | override fun setMute(mute: Boolean, callback: ServiceActionCallback?) {
122 | renderService.setMute(mute, callback)
123 | }
124 |
125 | override fun getMute(callback: ServiceActionCallback?) {
126 | renderService.getMute(callback)
127 | }
128 |
129 | // --------------------------------------------------------
130 | // ---- Content -------------------------------------------
131 | // --------------------------------------------------------
132 | override fun browse(objectId: String, flag: BrowseFlag, filter: String, firstResult: Int, maxResults: Int, callback: ServiceActionCallback?) {
133 | contentService.browse(objectId, flag, filter, firstResult, maxResults, callback)
134 | }
135 |
136 | override fun search(containerId: String, searchCriteria: String, filter: String, firstResult: Int, maxResults: Int, callback: ServiceActionCallback?) {
137 | contentService.search(containerId, searchCriteria, filter, firstResult, maxResults, callback)
138 | }
139 | }
--------------------------------------------------------------------------------
/dlna-dmc/src/main/java/com/android/cast/dlna/dmc/control/CastInterface.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmc.control
2 |
3 | import org.fourthline.cling.model.meta.Device
4 | import org.fourthline.cling.support.avtransport.lastchange.AVTransportVariable.TransportState
5 | import org.fourthline.cling.support.lastchange.EventedValue
6 | import org.fourthline.cling.support.model.BrowseFlag
7 | import org.fourthline.cling.support.model.DIDLContent
8 | import org.fourthline.cling.support.model.MediaInfo
9 | import org.fourthline.cling.support.model.PositionInfo
10 | import org.fourthline.cling.support.model.TransportInfo
11 | import org.fourthline.cling.support.renderingcontrol.lastchange.EventedValueChannelMute
12 | import org.fourthline.cling.support.renderingcontrol.lastchange.EventedValueChannelVolume
13 |
14 | interface DeviceControl : AvTransportServiceAction, RendererServiceAction, ContentServiceAction
15 |
16 | object EmptyDeviceControl : DeviceControl {
17 | override fun setAVTransportURI(uri: String, title: String, callback: ServiceActionCallback?) {}
18 | override fun setNextAVTransportURI(uri: String, title: String, callback: ServiceActionCallback?) {}
19 | override fun play(speed: String, callback: ServiceActionCallback?) {}
20 | override fun pause(callback: ServiceActionCallback?) {}
21 | override fun stop(callback: ServiceActionCallback?) {}
22 | override fun seek(millSeconds: Long, callback: ServiceActionCallback?) {}
23 | override fun next(callback: ServiceActionCallback?) {}
24 | override fun previous(callback: ServiceActionCallback?) {}
25 | override fun getPositionInfo(callback: ServiceActionCallback?) {}
26 | override fun getMediaInfo(callback: ServiceActionCallback?) {}
27 | override fun getTransportInfo(callback: ServiceActionCallback?) {}
28 | override fun setVolume(volume: Int, callback: ServiceActionCallback?) {}
29 | override fun getVolume(callback: ServiceActionCallback?) {}
30 | override fun setMute(mute: Boolean, callback: ServiceActionCallback?) {}
31 | override fun getMute(callback: ServiceActionCallback?) {}
32 | override fun browse(objectId: String, flag: BrowseFlag, filter: String, firstResult: Int, maxResults: Int, callback: ServiceActionCallback?) {}
33 | override fun search(containerId: String, searchCriteria: String, filter: String, firstResult: Int, maxResults: Int, callback: ServiceActionCallback?) {}
34 | }
35 |
36 | interface OnDeviceControlListener {
37 | fun onConnected(device: Device<*, *, *>) {}
38 | fun onDisconnected(device: Device<*, *, *>) {}
39 | fun onEventChanged(event: EventedValue<*>) {
40 | when (event) {
41 | is TransportState -> onAvTransportStateChanged(event.value)
42 | is EventedValueChannelVolume -> onRendererVolumeChanged(event.value.volume)
43 | is EventedValueChannelMute -> onRendererVolumeMuteChanged(event.value.mute)
44 | }
45 | }
46 |
47 | fun onAvTransportStateChanged(state: org.fourthline.cling.support.model.TransportState) {}
48 | fun onRendererVolumeChanged(volume: Int) {}
49 | fun onRendererVolumeMuteChanged(mute: Boolean) {}
50 | }
51 |
52 | internal interface SubscriptionListener {
53 | fun failed(subscriptionId: String?) {}
54 | fun established(subscriptionId: String?) {}
55 | fun ended(subscriptionId: String?) {}
56 | fun onReceived(subscriptionId: String?, event: EventedValue<*>) {}
57 | }
58 |
--------------------------------------------------------------------------------
/dlna-dmc/src/main/java/com/android/cast/dlna/dmc/control/CastSubscriptionCallback.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmc.control
2 |
3 | import com.android.cast.dlna.core.Logger
4 | import org.fourthline.cling.controlpoint.SubscriptionCallback
5 | import org.fourthline.cling.model.gena.CancelReason
6 | import org.fourthline.cling.model.gena.GENASubscription
7 | import org.fourthline.cling.model.message.UpnpResponse
8 | import org.fourthline.cling.model.meta.Service
9 | import org.fourthline.cling.support.lastchange.LastChangeParser
10 |
11 | /**
12 | *
13 | */
14 | internal class CastSubscriptionCallback(
15 | service: Service<*, *>?,
16 | requestedDurationSeconds: Int = 1800, // Cling default 1800
17 | private val lastChangeParser: LastChangeParser,
18 | private val callback: SubscriptionListener,
19 | ) : SubscriptionCallback(service, requestedDurationSeconds) {
20 |
21 | private val logger = Logger.create("SubscriptionCallback")
22 |
23 | override fun failed(subscription: GENASubscription<*>, responseStatus: UpnpResponse?, exception: Exception?, defaultMsg: String?) {
24 | logger.e("${getTag(subscription)} failed:${responseStatus}, $exception, $defaultMsg")
25 | executeInMainThread { callback.failed(subscription.subscriptionId) }
26 | }
27 |
28 | override fun established(subscription: GENASubscription<*>) {
29 | logger.i("${getTag(subscription)} established")
30 | executeInMainThread { callback.established(subscription.subscriptionId) }
31 | }
32 |
33 | override fun ended(subscription: GENASubscription<*>, reason: CancelReason?, responseStatus: UpnpResponse?) {
34 | logger.w("${getTag(subscription)} ended: $reason, $responseStatus")
35 | executeInMainThread { callback.ended(subscription.subscriptionId) }
36 | }
37 |
38 | override fun eventsMissed(subscription: GENASubscription<*>, numberOfMissedEvents: Int) {
39 | logger.w("${getTag(subscription)} eventsMissed: $numberOfMissedEvents")
40 | }
41 |
42 | override fun eventReceived(subscription: GENASubscription<*>) {
43 | val lastChangeEventValue = subscription.currentValues["LastChange"]?.value?.toString()
44 | if (lastChangeEventValue.isNullOrBlank()) return
45 | logger.i("${getTag(subscription)} eventReceived: ${subscription.currentValues.keys}")
46 | try {
47 | val events = lastChangeParser.parse(lastChangeEventValue)?.instanceIDs?.firstOrNull()?.values
48 | events?.forEach { value ->
49 | logger.i(" value: [${value.javaClass.simpleName}] $value")
50 | executeInMainThread { callback.onReceived(subscription.subscriptionId, value) }
51 | }
52 | } catch (e: Exception) {
53 | logger.w("${getTag(subscription)} currentValues: ${subscription.currentValues}")
54 | e.printStackTrace()
55 | }
56 | }
57 |
58 | private fun getTag(subscription: GENASubscription<*>) = "[${subscription.service.serviceType.type}](${subscription.subscriptionId?.split("-")?.last()})"
59 | }
--------------------------------------------------------------------------------
/dlna-dmc/src/main/java/com/android/cast/dlna/dmc/control/ServiceAction.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmc.control
2 |
3 | import org.fourthline.cling.support.model.BrowseFlag
4 | import org.fourthline.cling.support.model.DIDLContent
5 | import org.fourthline.cling.support.model.MediaInfo
6 | import org.fourthline.cling.support.model.PositionInfo
7 | import org.fourthline.cling.support.model.TransportInfo
8 |
9 | interface ServiceActionCallback {
10 | fun onSuccess(result: T)
11 | fun onFailure(msg: String)
12 | }
13 |
14 | // --------------------------------------------------------------------------------
15 | // ---- AvService
16 | // --------------------------------------------------------------------------------
17 | interface AvTransportServiceAction {
18 | fun setAVTransportURI(uri: String, title: String, callback: ServiceActionCallback? = null)
19 | fun setNextAVTransportURI(uri: String, title: String, callback: ServiceActionCallback? = null)
20 | fun play(speed: String = "1", callback: ServiceActionCallback? = null)
21 | fun pause(callback: ServiceActionCallback? = null)
22 | fun stop(callback: ServiceActionCallback? = null)
23 | fun seek(millSeconds: Long, callback: ServiceActionCallback? = null)
24 | fun next(callback: ServiceActionCallback? = null)
25 | fun canNext(callback: ServiceActionCallback? = null) {
26 | callback?.onSuccess(false)
27 | }
28 |
29 | fun previous(callback: ServiceActionCallback? = null)
30 | fun canPrevious(callback: ServiceActionCallback? = null) {
31 | callback?.onSuccess(false)
32 | }
33 |
34 | fun getPositionInfo(callback: ServiceActionCallback?)
35 | fun getMediaInfo(callback: ServiceActionCallback?)
36 | fun getTransportInfo(callback: ServiceActionCallback?)
37 | }
38 |
39 | // --------------------------------------------------------------------------------
40 | // ---- RendererService
41 | // --------------------------------------------------------------------------------
42 | interface RendererServiceAction {
43 | fun setVolume(volume: Int, callback: ServiceActionCallback? = null)
44 | fun getVolume(callback: ServiceActionCallback?)
45 | fun setMute(mute: Boolean, callback: ServiceActionCallback? = null)
46 | fun getMute(callback: ServiceActionCallback?)
47 | }
48 |
49 | // --------------------------------------------------------------------------------
50 | // ---- ContentService
51 | // --------------------------------------------------------------------------------
52 | interface ContentServiceAction {
53 | fun browse(objectId: String = "0", flag: BrowseFlag = BrowseFlag.DIRECT_CHILDREN, filter: String = "*", firstResult: Int = 0, maxResults: Int = Int.MAX_VALUE, callback: ServiceActionCallback?)
54 | fun search(containerId: String = "0", searchCriteria: String = "", filter: String = "*", firstResult: Int = 0, maxResults: Int = Int.MAX_VALUE, callback: ServiceActionCallback?)
55 | }
--------------------------------------------------------------------------------
/dlna-dmc/src/main/java/com/android/cast/dlna/dmc/control/Utils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 Kevin Shen
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.android.cast.dlna.dmc.control
17 |
18 | import android.os.Handler
19 | import android.os.Looper
20 | import org.fourthline.cling.support.model.item.VideoItem
21 |
22 | internal object MetadataUtils {
23 | private const val DIDL_LITE_XML =
24 | """%s"""
25 |
26 | fun create(url: String, title: String) = DIDL_LITE_XML.format(buildItemXml(url, title))
27 |
28 | private fun buildItemXml(url: String, title: String): String {
29 | val item = VideoItem(title, "-1", title, null)
30 | val builder = StringBuilder()
31 | builder.append("- ")
32 | builder.append("$title")
33 | builder.append("${item.clazz.value}")
34 | builder.append("$url")
35 | builder.append("
")
36 | return builder.toString()
37 | }
38 | }
39 |
40 | private val mainHandler = Handler(Looper.getMainLooper())
41 |
42 | fun executeInMainThread(runnable: Runnable) {
43 | if (Thread.currentThread() == Looper.getMainLooper().thread) {
44 | runnable.run()
45 | } else {
46 | mainHandler.post(runnable)
47 | }
48 | }
--------------------------------------------------------------------------------
/dlna-dmc/src/main/java/com/android/cast/dlna/dmc/control/action/SetNextAVTransportURI.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmc.control.action
2 |
3 | import org.fourthline.cling.controlpoint.ActionCallback
4 | import org.fourthline.cling.model.action.ActionInvocation
5 | import org.fourthline.cling.model.meta.Service
6 | import org.fourthline.cling.model.types.UnsignedIntegerFourBytes
7 | import java.util.logging.Logger
8 |
9 | @Suppress("LeakingThis")
10 | abstract class SetNextAVTransportURI @JvmOverloads constructor(
11 | service: Service<*, *>?,
12 | uri: String,
13 | metadata: String? = null,
14 | ) : ActionCallback(ActionInvocation(service?.getAction("SetNextAVTransportURI"))) {
15 | companion object {
16 | private val log = Logger.getLogger(SetNextAVTransportURI::class.java.name)
17 | }
18 |
19 | init {
20 | log.fine("Creating SetNextAVTransportURI action for URI: $uri")
21 | getActionInvocation().setInput("InstanceID", UnsignedIntegerFourBytes(0))
22 | getActionInvocation().setInput("NextURI", uri)
23 | getActionInvocation().setInput("NextURIMetaData", metadata)
24 | }
25 |
26 | override fun success(invocation: ActionInvocation<*>?) {
27 | log.fine("Execution successful")
28 | }
29 | }
--------------------------------------------------------------------------------
/dlna-dmr/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /backup
--------------------------------------------------------------------------------
/dlna-dmr/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'maven-publish'
5 | }
6 |
7 | android {
8 | namespace 'com.android.cast.dlna.dmr'
9 | compileSdk 32
10 | defaultConfig {
11 | minSdk 24
12 | targetSdk 32
13 | consumerProguardFiles "consumer-rules.pro"
14 | }
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 | compileOptions {
22 | sourceCompatibility JavaVersion.VERSION_1_8
23 | targetCompatibility JavaVersion.VERSION_1_8
24 | }
25 | }
26 |
27 | dependencies {
28 | api(project(':dlna-core'))
29 |
30 | implementation 'androidx.appcompat:appcompat:1.4.2'
31 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
32 | }
33 | repositories {
34 | mavenCentral()
35 | }
36 | apply from: './publish.gradle'
--------------------------------------------------------------------------------
/dlna-dmr/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/dlna-dmr/consumer-rules.pro
--------------------------------------------------------------------------------
/dlna-dmr/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
--------------------------------------------------------------------------------
/dlna-dmr/publish.gradle:
--------------------------------------------------------------------------------
1 | // Because the components are created only during the afterEvaluate phase, you must
2 | // configure your publications using the afterEvaluate() lifecycle method.
3 | afterEvaluate {
4 | publishing {
5 | publications {
6 | // Creates a Maven publication called "release".
7 | release(MavenPublication) {
8 | // Applies the component for the release build variant.
9 | from components.release
10 | // You can then customize attributes of the publication as shown below.
11 | groupId = GROUP
12 | artifactId = 'dmr'
13 | version = VERSION_RELEASE
14 | }
15 | // Creates a Maven publication called “debug”.
16 | debug(MavenPublication) {
17 | // Applies the component for the debug build variant.
18 | from components.debug
19 | groupId = GROUP
20 | artifactId = 'dmr'
21 | version = VERSION_DEBUG
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/dlna-dmr/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/dlna-dmr/src/main/java/com/android/cast/dlna/dmr/BaseRendererActivity.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmr
2 |
3 | import android.content.ComponentName
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.ServiceConnection
7 | import android.os.Bundle
8 | import android.os.IBinder
9 | import androidx.appcompat.app.AppCompatActivity
10 | import com.android.cast.dlna.dmr.service.keyExtraCastAction
11 |
12 | abstract class BaseRendererActivity : AppCompatActivity() {
13 | protected var rendererService: DLNARendererService? = null
14 | private set
15 |
16 | private val serviceConnection: ServiceConnection = object : ServiceConnection {
17 | override fun onServiceConnected(name: ComponentName, service: IBinder) {
18 | rendererService = (service as RendererServiceBinder).service
19 | onServiceConnected()
20 | }
21 |
22 | override fun onServiceDisconnected(name: ComponentName) {
23 | rendererService = null
24 | }
25 | }
26 |
27 | open fun onServiceConnected() {}
28 |
29 | protected val castAction: CastAction?
30 | get() = intent.getParcelableExtra(keyExtraCastAction)
31 |
32 | override fun onCreate(savedInstanceState: Bundle?) {
33 | super.onCreate(savedInstanceState)
34 | if (!castAction?.stop.isNullOrBlank()) {
35 | finish()
36 | return
37 | }
38 | bindService(Intent(this, DLNARendererService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
39 | }
40 |
41 | override fun onNewIntent(newIntent: Intent) {
42 | super.onNewIntent(newIntent)
43 | intent = newIntent
44 | if (!castAction?.stop.isNullOrBlank()) {
45 | finish()
46 | }
47 | }
48 |
49 | override fun onDestroy() {
50 | unbindService(serviceConnection)
51 | rendererService?.bindRealPlayer(null)
52 | super.onDestroy()
53 | }
54 | }
--------------------------------------------------------------------------------
/dlna-dmr/src/main/java/com/android/cast/dlna/dmr/RenderControl.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmr
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import android.os.Parcelable.Creator
6 | import org.fourthline.cling.support.model.TransportState
7 |
8 | /**
9 | *
10 | */
11 | interface RenderControl {
12 | val currentPosition: Long
13 | val duration: Long
14 |
15 | fun play(speed: Double? = 1.0)
16 | fun pause()
17 | fun seek(millSeconds: Long)
18 | fun stop()
19 | fun getState(): RenderState
20 | }
21 |
22 | enum class RenderState {
23 | IDLE, PREPARING, PLAYING, PAUSED, STOPPED, ERROR;
24 |
25 | fun toTransportState(): TransportState {
26 | return when (this) {
27 | PLAYING, PREPARING -> TransportState.PLAYING
28 | PAUSED -> TransportState.PAUSED_PLAYBACK
29 | STOPPED, ERROR -> TransportState.STOPPED
30 | else -> TransportState.NO_MEDIA_PRESENT
31 | }
32 | }
33 | }
34 |
35 | class CastAction(
36 | var currentURI: String? = null,
37 | var currentURIMetaData: String? = null,
38 | var nextURI: String? = null,
39 | var nextURIMetaData: String? = null,
40 | var stop: String? = null,
41 | ) : Parcelable {
42 | constructor(parcel: Parcel) : this(
43 | parcel.readString(),
44 | parcel.readString(),
45 | parcel.readString(),
46 | parcel.readString()
47 | )
48 |
49 | override fun writeToParcel(parcel: Parcel, flags: Int) {
50 | parcel.writeString(currentURI)
51 | parcel.writeString(currentURIMetaData)
52 | parcel.writeString(nextURI)
53 | parcel.writeString(nextURIMetaData)
54 | }
55 |
56 | override fun describeContents(): Int = 0
57 |
58 | companion object CREATOR : Creator {
59 | override fun createFromParcel(parcel: Parcel): CastAction {
60 | return CastAction(parcel)
61 | }
62 |
63 | override fun newArray(size: Int): Array {
64 | return arrayOfNulls(size)
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/dlna-dmr/src/main/java/com/android/cast/dlna/dmr/service/AVTransportController.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmr.service
2 |
3 | import android.content.Context
4 | import com.android.cast.dlna.core.Logger
5 | import com.android.cast.dlna.dmr.RenderControl
6 | import org.fourthline.cling.model.ModelUtil
7 | import org.fourthline.cling.support.avtransport.AVTransportException
8 | import org.fourthline.cling.support.model.*
9 | import org.fourthline.cling.support.model.TransportAction.*
10 |
11 | class AVTransportController(override val applicationContext: Context) : AvTransportControl {
12 | companion object {
13 | private val TRANSPORT_ACTION_STOPPED = arrayOf(Play)
14 | private val TRANSPORT_ACTION_PLAYING = arrayOf(Stop, Pause, Seek)
15 | private val TRANSPORT_ACTION_PAUSE_PLAYBACK = arrayOf(Play, Seek, Stop)
16 | }
17 |
18 | var mediaControl: RenderControl? = null
19 | set(value) {
20 | if (value != null) {
21 | _mediaInfo = MediaInfo(currentURI, currentURIMetaData)
22 | _positionInfo = PositionInfo(0, currentURIMetaData, currentURI)
23 | } else {
24 | mediaControl?.stop()
25 | _mediaInfo = MediaInfo()
26 | _positionInfo = PositionInfo()
27 | }
28 | field = value
29 | }
30 | private var _positionInfo = PositionInfo()
31 | private var _mediaInfo = MediaInfo()
32 |
33 | override val logger = Logger.create("AVTransportController")
34 | override val transportSettings = TransportSettings()
35 | override val deviceCapabilities: DeviceCapabilities = DeviceCapabilities(arrayOf(StorageMedium.UNKNOWN))
36 | override val transportInfo
37 | get() = mediaControl?.let { ctrl ->
38 | TransportInfo(ctrl.getState().toTransportState(), TransportStatus.OK, "1")
39 | } ?: TransportInfo()
40 | override val mediaInfo
41 | get() = _mediaInfo
42 | override val positionInfo: PositionInfo
43 | get() = mediaControl?.let { ctrl ->
44 | val duration = ModelUtil.toTimeString(ctrl.duration / 1000)
45 | val realTime = ModelUtil.toTimeString(ctrl.currentPosition / 1000)
46 | PositionInfo(0, duration, currentURI, realTime, realTime)
47 | } ?: PositionInfo()
48 | override val currentTransportActions: Array
49 | get() = when (transportInfo.currentTransportState) {
50 | TransportState.PLAYING -> TRANSPORT_ACTION_PLAYING
51 | TransportState.PAUSED_PLAYBACK -> TRANSPORT_ACTION_PAUSE_PLAYBACK
52 | else -> TRANSPORT_ACTION_STOPPED
53 | }
54 |
55 | private var currentURI: String? = null
56 | private var currentURIMetaData: String? = null
57 |
58 | @Throws(AVTransportException::class)
59 | override fun setAVTransportURI(currentURI: String, currentURIMetaData: String?) {
60 | super.setAVTransportURI(currentURI, currentURIMetaData)
61 | this.currentURI = currentURI
62 | this.currentURIMetaData = currentURIMetaData
63 | }
64 |
65 | private var nextURI: String? = null
66 | private var nextURIMetaData: String? = null
67 |
68 | override fun setNextAVTransportURI(nextURI: String, nextURIMetaData: String?) {
69 | super.setNextAVTransportURI(nextURI, nextURIMetaData)
70 | this.nextURI = nextURI
71 | this.nextURIMetaData = nextURIMetaData
72 | }
73 |
74 | override fun play(speed: String?) {
75 | super.play(speed)
76 | mediaControl?.play()
77 | }
78 |
79 | override fun pause() {
80 | super.pause()
81 | mediaControl?.pause()
82 | }
83 |
84 | override fun seek(unit: String?, target: String?) {
85 | super.seek(unit, target)
86 | try {
87 | mediaControl?.seek(ModelUtil.fromTimeString(target) * 1000)
88 | } catch (e: Exception) {
89 | logger.w("seek failed: $e")
90 | }
91 | }
92 |
93 | override fun next() {
94 | super.next()
95 | if (nextURI != null && nextURIMetaData != null) {
96 | previousURI = currentURI
97 | previousURIMetaData = currentURIMetaData
98 | setAVTransportURI(nextURI!!, nextURIMetaData)
99 | }
100 | nextURI = null
101 | nextURIMetaData = null
102 | }
103 |
104 | private var previousURI: String? = null
105 | private var previousURIMetaData: String? = null
106 | override fun previous() {
107 | super.previous()
108 | if (previousURI != null && previousURIMetaData != null) {
109 | nextURI = currentURI
110 | nextURIMetaData = currentURIMetaData
111 | setAVTransportURI(previousURI!!, previousURIMetaData)
112 | }
113 | previousURI = null
114 | previousURIMetaData = null
115 | }
116 |
117 | override fun stop() {
118 | super.stop()
119 | mediaControl?.stop()
120 | _mediaInfo = MediaInfo()
121 | _positionInfo = PositionInfo()
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/dlna-dmr/src/main/java/com/android/cast/dlna/dmr/service/AVTransportServiceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmr.service
2 |
3 | import org.fourthline.cling.model.types.UnsignedIntegerFourBytes
4 | import org.fourthline.cling.support.avtransport.AbstractAVTransportService
5 | import org.fourthline.cling.support.model.DeviceCapabilities
6 | import org.fourthline.cling.support.model.MediaInfo
7 | import org.fourthline.cling.support.model.PositionInfo
8 | import org.fourthline.cling.support.model.TransportAction
9 | import org.fourthline.cling.support.model.TransportInfo
10 | import org.fourthline.cling.support.model.TransportSettings
11 |
12 | class AVTransportServiceImpl(private val avTransportControl: AvTransportControl) : AbstractAVTransportService() {
13 | override fun getCurrentInstanceIds(): Array = arrayOf(UnsignedIntegerFourBytes(0))
14 | override fun getCurrentTransportActions(instanceId: UnsignedIntegerFourBytes): Array = avTransportControl.currentTransportActions
15 | override fun getDeviceCapabilities(instanceId: UnsignedIntegerFourBytes): DeviceCapabilities = avTransportControl.deviceCapabilities
16 | override fun getMediaInfo(instanceId: UnsignedIntegerFourBytes): MediaInfo = avTransportControl.mediaInfo
17 | override fun getPositionInfo(instanceId: UnsignedIntegerFourBytes): PositionInfo = avTransportControl.positionInfo
18 | override fun getTransportInfo(instanceId: UnsignedIntegerFourBytes): TransportInfo = avTransportControl.transportInfo
19 | override fun getTransportSettings(instanceId: UnsignedIntegerFourBytes): TransportSettings = avTransportControl.transportSettings
20 | override fun next(instanceId: UnsignedIntegerFourBytes) = avTransportControl.next()
21 | override fun pause(instanceId: UnsignedIntegerFourBytes) = avTransportControl.pause()
22 | override fun play(instanceId: UnsignedIntegerFourBytes, speed: String?) = avTransportControl.play(speed)
23 | override fun previous(instanceId: UnsignedIntegerFourBytes) = avTransportControl.previous()
24 | override fun seek(instanceId: UnsignedIntegerFourBytes, unit: String?, target: String?) = avTransportControl.seek(unit, target)
25 | override fun setAVTransportURI(instanceId: UnsignedIntegerFourBytes, currentURI: String, currentURIMetaData: String?) =
26 | avTransportControl.setAVTransportURI(currentURI, currentURIMetaData)
27 | override fun setNextAVTransportURI(instanceId: UnsignedIntegerFourBytes, nextURI: String, nextURIMetaData: String?) =
28 | avTransportControl.setNextAVTransportURI(nextURI, nextURIMetaData)
29 | override fun setPlayMode(instanceId: UnsignedIntegerFourBytes, newPlayMode: String) = avTransportControl.setPlayMode(newPlayMode)
30 | override fun stop(instanceId: UnsignedIntegerFourBytes) = avTransportControl.stop()
31 | override fun record(instanceId: UnsignedIntegerFourBytes) {} // ignore
32 | override fun setRecordQualityMode(instanceId: UnsignedIntegerFourBytes, newRecordQualityMode: String) {} // ignore
33 | }
34 |
--------------------------------------------------------------------------------
/dlna-dmr/src/main/java/com/android/cast/dlna/dmr/service/AudioRenderController.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmr.service
2 |
3 | import android.content.Context
4 | import android.media.AudioManager
5 | import com.android.cast.dlna.core.Logger
6 | import org.fourthline.cling.model.types.UnsignedIntegerTwoBytes
7 |
8 | /**
9 | *
10 | */
11 | class AudioRenderController constructor(context: Context) : AudioControl {
12 |
13 | private val muteVolume = UnsignedIntegerTwoBytes(0)
14 | private val audioManager: AudioManager
15 | private var currentVolume: UnsignedIntegerTwoBytes
16 |
17 | init {
18 | audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
19 | val maxMusicVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
20 | val volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
21 | currentVolume = UnsignedIntegerTwoBytes(volume * 100L / maxMusicVolume)
22 | }
23 |
24 | override val logger: Logger = Logger.create("AudioRenderController")
25 |
26 | override fun setMute(channelName: String, desiredMute: Boolean) {
27 | super.setMute(channelName, desiredMute)
28 | setVolume(channelName, if (desiredMute) muteVolume else currentVolume)
29 | }
30 |
31 | override fun getMute(channelName: String): Boolean = getVolume(channelName).value == 0L
32 |
33 | override fun setVolume(channelName: String, desiredVolume: UnsignedIntegerTwoBytes) {
34 | super.setVolume(channelName, desiredVolume)
35 | currentVolume = desiredVolume
36 | val volume = desiredVolume.value.toInt()
37 | val adjustVolume = volume * audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) / 100
38 | audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, adjustVolume, AudioManager.FLAG_PLAY_SOUND or AudioManager.FLAG_SHOW_UI)
39 | }
40 |
41 | override fun getVolume(channelName: String): UnsignedIntegerTwoBytes =
42 | UnsignedIntegerTwoBytes(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) * 100L / audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
43 | }
44 |
--------------------------------------------------------------------------------
/dlna-dmr/src/main/java/com/android/cast/dlna/dmr/service/AudioRenderServiceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmr.service
2 |
3 | import org.fourthline.cling.model.types.UnsignedIntegerFourBytes
4 | import org.fourthline.cling.model.types.UnsignedIntegerTwoBytes
5 | import org.fourthline.cling.support.model.Channel
6 | import org.fourthline.cling.support.renderingcontrol.AbstractAudioRenderingControl
7 |
8 | class AudioRenderServiceImpl(private val audioControl: AudioControl) : AbstractAudioRenderingControl() {
9 | override fun setMute(instanceId: UnsignedIntegerFourBytes, channelName: String, desiredMute: Boolean) = audioControl.setMute(channelName, desiredMute)
10 | override fun getMute(instanceId: UnsignedIntegerFourBytes, channelName: String): Boolean = audioControl.getMute(channelName)
11 | override fun setVolume(instanceId: UnsignedIntegerFourBytes, channelName: String, desiredVolume: UnsignedIntegerTwoBytes) =
12 | audioControl.setVolume(channelName, desiredVolume)
13 | override fun getVolume(instanceId: UnsignedIntegerFourBytes, channelName: String): UnsignedIntegerTwoBytes = audioControl.getVolume(channelName)
14 | override fun getCurrentChannels(): Array = arrayOf(Channel.Master)
15 | override fun getCurrentInstanceIds(): Array = arrayOf(UnsignedIntegerFourBytes(0))
16 | }
--------------------------------------------------------------------------------
/dlna-dmr/src/main/java/com/android/cast/dlna/dmr/service/RendererInterface.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dmr.service
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import com.android.cast.dlna.core.Logger
6 | import com.android.cast.dlna.dmr.CastAction
7 | import org.fourthline.cling.model.types.ErrorCode.INVALID_ARGS
8 | import org.fourthline.cling.model.types.UnsignedIntegerTwoBytes
9 | import org.fourthline.cling.support.avtransport.AVTransportException
10 | import org.fourthline.cling.support.model.DeviceCapabilities
11 | import org.fourthline.cling.support.model.MediaInfo
12 | import org.fourthline.cling.support.model.PositionInfo
13 | import org.fourthline.cling.support.model.TransportAction
14 | import org.fourthline.cling.support.model.TransportInfo
15 | import org.fourthline.cling.support.model.TransportSettings
16 | import java.net.URI
17 |
18 | const val actionSetAvTransport = "com.dlna.action.SetAvTransport"
19 | const val keyExtraCastAction = "extra.castAction"
20 |
21 | interface RendererControl
22 |
23 | // -------------------------------------------------------------------------------------------
24 | // - AvTransport
25 | // -------------------------------------------------------------------------------------------
26 | interface AvTransportControl : RendererControl {
27 | val logger: Logger
28 | val applicationContext: Context
29 | fun setAVTransportURI(currentURI: String, currentURIMetaData: String?) {
30 | logger.i("setAVTransportURI: currentURI=$currentURI")
31 | currentURIMetaData?.let { logger.i("setAVTransportURI: currentURIMetaData=$it") }
32 | try {
33 | URI(currentURI)
34 | } catch (ex: Exception) {
35 | throw AVTransportException(INVALID_ARGS, "CurrentURI can not be null or malformed")
36 | }
37 |
38 | startCastActivity {
39 | this.currentURI = currentURI
40 | this.currentURIMetaData = currentURIMetaData
41 | }
42 | }
43 |
44 | fun setNextAVTransportURI(nextURI: String, nextURIMetaData: String?) {
45 | logger.i("setNextAVTransportURI: nextURI=$nextURI")
46 | nextURIMetaData?.let { logger.i("setNextAVTransportURI: nextURIMetaData=$it") }
47 |
48 | startCastActivity {
49 | this.nextURI = nextURI
50 | this.nextURIMetaData = nextURIMetaData
51 | }
52 | }
53 |
54 | private fun startCastActivity(content: CastAction.() -> Unit) {
55 | applicationContext.startActivity(Intent(actionSetAvTransport).apply {
56 | val castAction = CastAction()
57 | content(castAction)
58 | this.putExtra(keyExtraCastAction, castAction)
59 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) // start from service content,should add 'FLAG_ACTIVITY_NEW_TASK' flag.
60 | })
61 | }
62 |
63 | fun setPlayMode(newPlayMode: String?) {
64 | logger.i("setPlayMode: newPlayMode=$newPlayMode")
65 | }
66 |
67 | fun play(speed: String?) {
68 | logger.i("play: speed=$speed")
69 | }
70 |
71 | fun pause() {
72 | logger.i("pause")
73 | }
74 |
75 | fun seek(unit: String?, target: String?) {
76 | logger.i("seek: unit=$unit, target=$target")
77 | }
78 |
79 | fun previous() {
80 | logger.i("previous")
81 | }
82 |
83 | fun next() {
84 | logger.i("next")
85 | }
86 |
87 | fun stop() {
88 | logger.i("stop")
89 | // startCastActivity {
90 | // this.stop = "stop"
91 | // }
92 | }
93 |
94 | val currentTransportActions: Array
95 | val deviceCapabilities: DeviceCapabilities
96 | val mediaInfo: MediaInfo
97 | val positionInfo: PositionInfo
98 | val transportInfo: TransportInfo
99 | val transportSettings: TransportSettings
100 | }
101 |
102 | // -------------------------------------------------------------------------------------------
103 | // - Audio
104 | // -------------------------------------------------------------------------------------------
105 | interface AudioControl : RendererControl {
106 | val logger: Logger
107 | fun setMute(channelName: String, desiredMute: Boolean) {
108 | logger.i("setMute: $desiredMute")
109 | }
110 |
111 | fun getMute(channelName: String): Boolean
112 | fun setVolume(channelName: String, desiredVolume: UnsignedIntegerTwoBytes) {
113 | logger.i("setVolume: ${desiredVolume.value}")
114 | }
115 |
116 | fun getVolume(channelName: String): UnsignedIntegerTwoBytes
117 | }
--------------------------------------------------------------------------------
/dlna-dms/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/dlna-dms/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'maven-publish'
5 | }
6 |
7 | android {
8 | namespace 'com.android.cast.dlna.dms'
9 | compileSdk 32
10 | defaultConfig {
11 | minSdk 24
12 | targetSdk 32
13 | consumerProguardFiles "consumer-rules.pro"
14 | }
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 | compileOptions {
22 | sourceCompatibility JavaVersion.VERSION_1_8
23 | targetCompatibility JavaVersion.VERSION_1_8
24 | }
25 | }
26 |
27 | dependencies {
28 | implementation fileTree(dir: 'libs', include: ['*.jar'])
29 | api(project(':dlna-core'))
30 | //noinspection GradleDependency
31 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
32 | }
33 | apply from: './publish.gradle'
--------------------------------------------------------------------------------
/dlna-dms/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/dlna-dms/consumer-rules.pro
--------------------------------------------------------------------------------
/dlna-dms/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
--------------------------------------------------------------------------------
/dlna-dms/publish.gradle:
--------------------------------------------------------------------------------
1 | // Because the components are created only during the afterEvaluate phase, you must
2 | // configure your publications using the afterEvaluate() lifecycle method.
3 | afterEvaluate {
4 | publishing {
5 | publications {
6 | // Creates a Maven publication called "release".
7 | release(MavenPublication) {
8 | // Applies the component for the release build variant.
9 | from components.release
10 | // You can then customize attributes of the publication as shown below.
11 | groupId = GROUP
12 | artifactId = 'dms'
13 | version = VERSION_RELEASE
14 | }
15 | // Creates a Maven publication called “debug”.
16 | debug(MavenPublication) {
17 | // Applies the component for the debug build variant.
18 | from components.debug
19 | groupId = GROUP
20 | artifactId = 'dms'
21 | version = VERSION_DEBUG
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/dlna-dms/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/dlna-dms/src/main/java/com/android/cast/dlna/dms/DLNAContentService.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dms
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Build
6 | import android.os.IBinder
7 | import com.android.cast.dlna.core.Logger
8 | import com.android.cast.dlna.core.Utils
9 | import com.android.cast.dlna.dms.service.ContentControl
10 | import com.android.cast.dlna.dms.service.ContentDirectoryServiceController
11 | import com.android.cast.dlna.dms.service.ContentDirectoryServiceImpl
12 | import org.fourthline.cling.UpnpServiceConfiguration
13 | import org.fourthline.cling.android.AndroidUpnpServiceConfiguration
14 | import org.fourthline.cling.android.AndroidUpnpServiceImpl
15 | import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder
16 | import org.fourthline.cling.model.DefaultServiceManager
17 | import org.fourthline.cling.model.meta.*
18 | import org.fourthline.cling.model.types.ServiceType
19 | import org.fourthline.cling.model.types.UDADeviceType
20 | import org.fourthline.cling.model.types.UDN
21 | import org.fourthline.cling.support.contentdirectory.AbstractContentDirectoryService
22 | import java.util.*
23 |
24 | open class DLNAContentService : AndroidUpnpServiceImpl() {
25 | companion object {
26 | fun startService(context: Context) {
27 | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
28 | // context.applicationContext.startForegroundService(Intent(context, DLNAContentService::class.java))
29 | // } else {
30 | context.applicationContext.startService(Intent(context, DLNAContentService::class.java))
31 | // }
32 | }
33 | }
34 |
35 | protected inner class RendererServiceBinderWrapper : AndroidUpnpServiceImpl.Binder(), ContentServiceBinder {
36 | override val service: DLNAContentService
37 | get() = this@DLNAContentService
38 | }
39 |
40 | private val logger = Logger.create("LocalContentService")
41 | private val serviceBinder = RendererServiceBinderWrapper()
42 | private var localDevice: LocalDevice? = null
43 | private lateinit var contentControl: ContentControl
44 |
45 | override fun onCreate() {
46 | logger.i("DLNAContentService create.")
47 | super.onCreate()
48 | contentControl = ContentDirectoryServiceController(this)
49 | val baseUrl = Utils.getHttpBaseUrl(this)
50 | try {
51 | localDevice = createContentServiceDevice(baseUrl = baseUrl)
52 | upnpService.registry.addDevice(localDevice)
53 | } catch (e: Exception) {
54 | e.printStackTrace()
55 | stopSelf()
56 | }
57 | }
58 |
59 | protected open fun createContentServiceDevice(baseUrl: String): LocalDevice {
60 | val info = "DLNA_ContentService-$baseUrl-${Build.MODEL}-${Build.MANUFACTURER}"
61 | val udn = try {
62 | UDN(UUID.nameUUIDFromBytes(info.toByteArray()))
63 | } catch (ex: Exception) {
64 | UDN(UUID.randomUUID())
65 | }
66 | logger.i("create local device: [MediaServer][$udn]($baseUrl)")
67 | return LocalDevice(
68 | DeviceIdentity(udn),
69 | UDADeviceType("MediaServer", 1),
70 | DeviceDetails(
71 | "DMS (${Build.MODEL})",
72 | ManufacturerDetails(Build.MANUFACTURER),
73 | ModelDetails(Build.MODEL, "MSI MediaServer", "v1", baseUrl)
74 | ),
75 | emptyArray(),
76 | generateLocalServices()
77 | )
78 | }
79 |
80 | @Suppress("UNCHECKED_CAST")
81 | protected open fun generateLocalServices(): Array> {
82 | val serviceBinder = AnnotationLocalServiceBinder()
83 | // content directory service
84 | val contentDirectoryService = serviceBinder.read(AbstractContentDirectoryService::class.java) as LocalService
85 | contentDirectoryService.manager = object : DefaultServiceManager(contentDirectoryService) {
86 | override fun createServiceInstance(): AbstractContentDirectoryService {
87 | return ContentDirectoryServiceImpl(contentControl)
88 | }
89 | }
90 | return arrayOf(contentDirectoryService)
91 | }
92 |
93 | override fun onBind(intent: Intent?): IBinder? = serviceBinder
94 |
95 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY
96 |
97 | override fun onDestroy() {
98 | logger.w("DLNAContentService destroy.")
99 | localDevice?.also { device ->
100 | upnpService.registry.removeDevice(device)
101 | }
102 | super.onDestroy()
103 | }
104 |
105 | override fun createConfiguration(): UpnpServiceConfiguration = object : AndroidUpnpServiceConfiguration() {
106 | // content service never need find other device
107 | override fun getExclusiveServiceTypes(): Array? = null
108 | }
109 | }
110 |
111 | interface ContentServiceBinder {
112 | val service: DLNAContentService
113 | }
--------------------------------------------------------------------------------
/dlna-dms/src/main/java/com/android/cast/dlna/dms/service/ContentDirectoryServiceController.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dms.service
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.net.Uri
6 | import android.provider.MediaStore.Audio
7 | import android.provider.MediaStore.Images
8 | import android.provider.MediaStore.MediaColumns
9 | import android.provider.MediaStore.Video
10 | import com.android.cast.dlna.core.Logger
11 | import org.fourthline.cling.support.contentdirectory.DIDLParser
12 | import org.fourthline.cling.support.model.BrowseFlag
13 | import org.fourthline.cling.support.model.BrowseResult
14 | import org.fourthline.cling.support.model.DIDLContent
15 | import org.fourthline.cling.support.model.Res
16 | import org.fourthline.cling.support.model.item.ImageItem
17 | import org.fourthline.cling.support.model.item.Item
18 | import kotlin.math.max
19 |
20 | class ContentDirectoryServiceController(context: Context) : ContentControl {
21 | companion object {
22 | private val columns = arrayOf(MediaColumns._ID, MediaColumns.TITLE, MediaColumns.DATA, MediaColumns.MIME_TYPE, MediaColumns.SIZE)
23 | }
24 |
25 | private val logger = Logger.create("ContentDirectoryService")
26 | private val applicationContext: Context = context.applicationContext
27 | override fun browse(objectID: String, browseFlag: BrowseFlag, filter: String?, firstResult: Long, maxResults: Long): BrowseResult {
28 | logger.i("browse: $objectID, $browseFlag, $filter, $firstResult, $maxResults")
29 | return get(objectID, filter, firstResult, maxResults)
30 | }
31 |
32 | override fun search(containerId: String, searchCriteria: String, filter: String?, firstResult: Long, maxResults: Long): BrowseResult {
33 | logger.i("search: $containerId, $searchCriteria, $filter, $firstResult, $maxResults")
34 | return get(containerId, filter, firstResult, maxResults)
35 | }
36 |
37 | private fun get(objectID: String, @Suppress("UNUSED_PARAMETER") filter: String?, firstResult: Long, maxResults: Long): BrowseResult {
38 | var maxCount = maxResults
39 | val list = mutableListOf- ()
40 | val didlContent = DIDLContent()
41 | if (objectID.isBlank() || objectID == "*" || objectID == "0" || objectID.contains("video")) {
42 | list.addAll(getItems(applicationContext, Video.Media.EXTERNAL_CONTENT_URI, firstResult, maxCount))
43 | maxCount -= list.size
44 | }
45 | if (objectID.isBlank() || objectID == "*" || objectID == "0" || objectID.contains("audio")) {
46 | list.addAll(getItems(applicationContext, Audio.Media.EXTERNAL_CONTENT_URI, firstResult, maxCount))
47 | maxCount -= list.size
48 | }
49 | if (objectID.isBlank() || objectID == "*" || objectID == "0" || objectID.contains("image")) {
50 | list.addAll(getItems(applicationContext, Images.Media.EXTERNAL_CONTENT_URI, firstResult, maxCount))
51 | maxCount -= list.size
52 | }
53 | didlContent.items = list
54 | return try {
55 | val result = DIDLParser().generate(didlContent, false)
56 | BrowseResult(result, didlContent.items.size.toLong(), didlContent.items.size.toLong())
57 | } catch (e: Exception) {
58 | e.printStackTrace()
59 | BrowseResult("", 0, 0) //TODO: check
60 | }
61 | }
62 |
63 | @SuppressLint("Range")
64 | private fun getItems(context: Context, uri: Uri, firstResult: Long, maxResults: Long): List
- {
65 | val first = max(firstResult, 0).toInt()
66 | val max = max(maxResults, 0)
67 | val items: MutableList
- = ArrayList()
68 | context.contentResolver.query(uri, columns, null, null, null)
69 | .use { cursor ->
70 | if (cursor == null) return items
71 | cursor.moveToPosition(first)
72 | while (cursor.moveToNext() && items.size < max) {
73 | val id = cursor.getInt(cursor.getColumnIndex(columns[0])).toString()
74 | val title = cursor.getString(cursor.getColumnIndex(columns[1]))
75 | val data = cursor.getString(cursor.getColumnIndex(columns[2]))
76 | val mimeType = cursor.getString(cursor.getColumnIndex(columns[3]))
77 | val size = cursor.getLong(cursor.getColumnIndex(columns[4]))
78 | items.add(ImageItem(id, "-1", title, "", Res(mimeType, size, "", null, data)))
79 | }
80 | }
81 | return items
82 | }
83 | }
--------------------------------------------------------------------------------
/dlna-dms/src/main/java/com/android/cast/dlna/dms/service/ContentDirectoryServiceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dms.service
2 |
3 | import org.fourthline.cling.support.contentdirectory.AbstractContentDirectoryService
4 | import org.fourthline.cling.support.model.BrowseFlag
5 | import org.fourthline.cling.support.model.BrowseResult
6 | import org.fourthline.cling.support.model.SortCriterion
7 |
8 | internal class ContentDirectoryServiceImpl(private val control: ContentControl) : AbstractContentDirectoryService() {
9 | override fun browse(objectID: String, browseFlag: BrowseFlag, filter: String, firstResult: Long, maxResults: Long, orderBy: Array): BrowseResult =
10 | control.browse(objectID, browseFlag, filter, firstResult, maxResults)
11 | override fun search(containerId: String, searchCriteria: String, filter: String, firstResult: Long, maxResults: Long, orderBy: Array): BrowseResult =
12 | control.search(containerId, searchCriteria, filter, firstResult, maxResults)
13 | }
--------------------------------------------------------------------------------
/dlna-dms/src/main/java/com/android/cast/dlna/dms/service/ContentInterface.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.dms.service
2 |
3 | import org.fourthline.cling.support.model.BrowseFlag
4 | import org.fourthline.cling.support.model.BrowseResult
5 |
6 | interface ContentControl {
7 | fun browse(objectID: String, browseFlag: BrowseFlag, filter: String? = null, firstResult: Long = 0, maxResults: Long = 9999): BrowseResult
8 | fun search(containerId: String, searchCriteria: String, filter: String? = null, firstResult: Long = 0, maxResults: Long = 9999): BrowseResult
9 | }
--------------------------------------------------------------------------------
/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 | android.enableJetifier=true
10 | android.useAndroidX=true
11 | org.gradle.jvmargs=-Xmx1536m
12 | # When configured, Gradle will run in incubating parallel mode.
13 | # This option should only be used with decoupled projects. More details, visit
14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
15 | # org.gradle.parallel=true
16 | GROUP=com.devin1014.dlna_cast
17 | VERSION_DEBUG=2.0.0-SNAPSHOT
18 | VERSION_RELEASE=2.0.0-SNAPSHOT
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/libs/core-2.0.0-SNAPSHOT.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/libs/core-2.0.0-SNAPSHOT.aar
--------------------------------------------------------------------------------
/libs/dmc-2.0.0-SNAPSHOT.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/libs/dmc-2.0.0-SNAPSHOT.aar
--------------------------------------------------------------------------------
/libs/dmr-2.0.0-SNAPSHOT.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/libs/dmr-2.0.0-SNAPSHOT.aar
--------------------------------------------------------------------------------
/libs/dms-2.0.0-SNAPSHOT.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/libs/dms-2.0.0-SNAPSHOT.aar
--------------------------------------------------------------------------------
/screen/Screenshot_20230801_173015.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/screen/Screenshot_20230801_173015.png
--------------------------------------------------------------------------------
/screen/Screenshot_20230801_173051.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/screen/Screenshot_20230801_173051.png
--------------------------------------------------------------------------------
/screen/Screenshot_20230801_173059.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/screen/Screenshot_20230801_173059.png
--------------------------------------------------------------------------------
/screen/Screenshot_20230801_173117.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/screen/Screenshot_20230801_173117.png
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/server/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | }
5 |
6 | android {
7 | namespace 'com.android.cast.dlna.demo.server'
8 | compileSdk 32
9 | defaultConfig {
10 | applicationId "com.android.cast.dlna.demo.server"
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-optimize.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 | kotlinOptions {
30 | jvmTarget = '1.8'
31 | }
32 | }
33 |
34 | dependencies {
35 | implementation project(':dlna-dms')
36 | implementation 'com.guolindev.permissionx:permissionx:1.7.1'
37 | //noinspection GradleDependency
38 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
39 | implementation 'androidx.appcompat:appcompat:1.4.2'
40 | implementation 'com.google.android.material:material:1.6.1'
41 | }
--------------------------------------------------------------------------------
/server/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
--------------------------------------------------------------------------------
/server/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/server/src/main/java/com/android/cast/dlna/demo/server/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.demo.server
2 |
3 | import android.Manifest.permission
4 | import android.annotation.SuppressLint
5 | import android.os.Bundle
6 | import android.view.View
7 | import android.widget.TextView
8 | import androidx.appcompat.app.AppCompatActivity
9 | import com.android.cast.dlna.core.Utils
10 | import com.android.cast.dlna.dms.DLNAContentService
11 | import com.permissionx.guolindev.PermissionX
12 |
13 | class MainActivity : AppCompatActivity() {
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_main)
17 | PermissionX.init(this)
18 | .permissions(permission.READ_EXTERNAL_STORAGE, permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION)
19 | .request { _: Boolean, _: List?, _: List? ->
20 | resetWifiInfo()
21 | }
22 | DLNAContentService.startService(this)
23 | }
24 |
25 | @SuppressLint("SetTextI18n")
26 | private fun resetWifiInfo() {
27 | (findViewById(R.id.network_info) as TextView).text = "${Utils.getWiFiName(this)} - ${Utils.getWiFiIpAddress(this)}"
28 | }
29 | }
--------------------------------------------------------------------------------
/server/src/main/java/com/android/cast/dlna/demo/server/ServerApplication.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.demo.server
2 |
3 | import android.app.Application
4 | import java.util.logging.Level
5 |
6 | class ServerApplication : Application() {
7 | override fun onCreate() {
8 | super.onCreate()
9 | // LoggingUtil.resetRootHandler(FixedAndroidLogHandler())
10 | java.util.logging.Logger.getLogger("org.fourthline.cling").level = Level.CONFIG
11 | com.android.cast.dlna.core.Logger.printThread = true
12 | com.android.cast.dlna.core.Logger.enabled = true
13 | com.android.cast.dlna.core.Logger.level = com.android.cast.dlna.core.Level.D
14 | com.android.cast.dlna.core.Logger.create("ServerApplication").i("Application onCreate.")
15 | }
16 | }
--------------------------------------------------------------------------------
/server/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
14 |
17 |
20 |
23 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/server/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/server/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/server/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/server/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/server/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/server/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/server/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/server/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/server/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/server/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/server/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/server/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/server/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/server/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 | #FFBB86FC
11 |
--------------------------------------------------------------------------------
/server/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DLNA 内容服务器
3 |
--------------------------------------------------------------------------------
/server/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | include ':tv'
3 | include ':dlna-core'
4 | include ':dlna-dmc'
5 | include ':dlna-dms'
6 | include ':dlna-dmr'
7 | include ':server'
8 |
--------------------------------------------------------------------------------
/tv/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/tv/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | }
5 |
6 | android {
7 | namespace 'com.android.cast.dlna.demo.renderer'
8 | compileSdk 32
9 | defaultConfig {
10 | applicationId "com.android.cast.dlna.demo.renderer"
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-optimize.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 | implementation project(':dlna-dmr')
33 | implementation 'com.guolindev.permissionx:permissionx:1.7.1'
34 | //noinspection GradleDependency
35 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
36 | implementation 'androidx.appcompat:appcompat:1.4.2'
37 | implementation 'com.google.android.material:material:1.6.1'
38 | }
--------------------------------------------------------------------------------
/tv/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
--------------------------------------------------------------------------------
/tv/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/tv/src/main/java/com/android/cast/dlna/demo/renderer/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.demo.renderer
2 |
3 | import android.Manifest.permission
4 | import android.annotation.SuppressLint
5 | import android.os.Bundle
6 | import android.view.View
7 | import android.widget.TextView
8 | import androidx.appcompat.app.AppCompatActivity
9 | import com.android.cast.dlna.core.Utils
10 | import com.android.cast.dlna.dmr.DLNARendererService
11 | import com.permissionx.guolindev.PermissionX
12 |
13 | class MainActivity : AppCompatActivity() {
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_main)
17 | PermissionX.init(this)
18 | .permissions(permission.READ_EXTERNAL_STORAGE, permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION)
19 | .request { _: Boolean, _: List?, _: List? -> resetWifiInfo() }
20 | DLNARendererService.startService(this)
21 | }
22 |
23 | @SuppressLint("SetTextI18n")
24 | private fun resetWifiInfo() {
25 | (findViewById(R.id.network_info) as TextView).text = "${Utils.getWiFiName(this)} - ${Utils.getWiFiIpAddress(this)}"
26 | }
27 |
28 | // private val connection = object : ServiceConnection {
29 | // override fun onServiceConnected(name: ComponentName, binder: IBinder) {
30 | // (binder as? RendererServiceBinder)?.service?.updateDevice()
31 | // }
32 | //
33 | // override fun onServiceDisconnected(name: ComponentName) {
34 | // }
35 | //
36 | // }
37 | //
38 | // override fun onStart() {
39 | // super.onStart()
40 | // bindService(Intent(this, DLNARendererService::class.java), connection, Service.BIND_AUTO_CREATE)
41 | // }
42 | }
--------------------------------------------------------------------------------
/tv/src/main/java/com/android/cast/dlna/demo/renderer/RendererApplication.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.demo.renderer
2 |
3 | import android.app.Application
4 | import java.util.logging.Level
5 |
6 | class RendererApplication : Application() {
7 | override fun onCreate() {
8 | super.onCreate()
9 | // LoggingUtil.resetRootHandler(FixedAndroidLogHandler())
10 | java.util.logging.Logger.getLogger("org.fourthline.cling").level = Level.CONFIG
11 | com.android.cast.dlna.core.Logger.printThread = true
12 | com.android.cast.dlna.core.Logger.enabled = true
13 | com.android.cast.dlna.core.Logger.level = com.android.cast.dlna.core.Level.D
14 | com.android.cast.dlna.core.Logger.create("RendererApplication").i("Application onCreate.")
15 | }
16 | }
--------------------------------------------------------------------------------
/tv/src/main/java/com/android/cast/dlna/demo/renderer/VideoViewRendererActivity.kt:
--------------------------------------------------------------------------------
1 | package com.android.cast.dlna.demo.renderer
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.media.AudioManager
6 | import android.net.Uri
7 | import android.os.Bundle
8 | import android.view.KeyEvent
9 | import android.view.View
10 | import android.widget.ProgressBar
11 | import android.widget.TextView
12 | import android.widget.VideoView
13 | import com.android.cast.dlna.dmr.BaseRendererActivity
14 | import com.android.cast.dlna.dmr.RenderControl
15 | import com.android.cast.dlna.dmr.RenderState
16 |
17 | class VideoViewRendererActivity : BaseRendererActivity() {
18 |
19 | private val videoView: VideoView by lazy { findViewById(R.id.video_view) }
20 | private val progressBar: ProgressBar by lazy { findViewById(R.id.video_progress) }
21 | private val errorMsg: TextView by lazy { findViewById(R.id.video_error) }
22 | private var renderState: RenderState = RenderState.IDLE
23 | set(value) {
24 | if (field != value) {
25 | field = value
26 | rendererService?.notifyAvTransportLastChange(field)
27 | }
28 | }
29 |
30 | override fun onServiceConnected() {
31 | rendererService?.bindRealPlayer(VideoViewRenderControl(videoView))
32 | openMedia()
33 | }
34 |
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | super.onCreate(savedInstanceState)
37 | setContentView(R.layout.activity_videoview_renderer)
38 | initComponent()
39 | rendererService?.run {
40 | openMedia()
41 | }
42 | }
43 |
44 | @SuppressLint("SetTextI18n")
45 | private fun initComponent() {
46 | // 方便在平板上调试,模拟遥控器
47 | findViewById(R.id.player_action_bar).visibility = View.VISIBLE
48 | findViewById(R.id.player_pause).setOnClickListener {
49 | if (videoView.isPlaying) {
50 | videoView.pause()
51 | renderState = RenderState.PAUSED
52 | }
53 | }
54 | findViewById(R.id.player_resume).setOnClickListener {
55 | if (!videoView.isPlaying) {
56 | videoView.start()
57 | renderState = RenderState.PLAYING
58 | }
59 | }
60 | videoView.setOnPreparedListener { mp ->
61 | mp.start()
62 | renderState = RenderState.PLAYING
63 | progressBar.visibility = View.INVISIBLE
64 | }
65 | videoView.setOnErrorListener { _, what, extra ->
66 | renderState = RenderState.ERROR
67 | if (nextURI.isNullOrBlank()) {
68 | progressBar.visibility = View.INVISIBLE
69 | errorMsg.visibility = View.VISIBLE
70 | errorMsg.text = "播放错误: $what, $extra"
71 | } else {
72 | videoView.setVideoURI(Uri.parse(nextURI))
73 | nextURI = null
74 | }
75 | true
76 | }
77 | videoView.setOnCompletionListener {
78 | renderState = RenderState.STOPPED
79 | if (nextURI.isNullOrBlank()) {
80 | finish()
81 | } else {
82 | videoView.setVideoURI(Uri.parse(nextURI))
83 | nextURI = null
84 | }
85 | }
86 | }
87 |
88 | override fun onNewIntent(newIntent: Intent) {
89 | super.onNewIntent(newIntent)
90 | openMedia()
91 | }
92 |
93 | private var nextURI: String? = null
94 |
95 | @SuppressLint("SetTextI18n")
96 | private fun openMedia() {
97 | castAction?.currentURI?.run {
98 | progressBar.visibility = View.VISIBLE
99 | errorMsg.visibility = View.INVISIBLE
100 | videoView.setVideoURI(Uri.parse(this))
101 | }
102 | castAction?.nextURI?.run {
103 | nextURI = this
104 | }
105 | castAction?.stop?.run {
106 | finish()
107 | }
108 | }
109 |
110 | override fun onDestroy() {
111 | renderState = RenderState.STOPPED
112 | videoView.stopPlayback()
113 | super.onDestroy()
114 | }
115 |
116 | override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
117 | val handled = super.onKeyDown(keyCode, event)
118 | if (rendererService != null) {
119 | if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) {
120 | val volume = (application.getSystemService(AUDIO_SERVICE) as AudioManager).getStreamVolume(AudioManager.STREAM_MUSIC)
121 | rendererService?.notifyRenderControlLastChange(volume)
122 | } else if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
123 | if (videoView.isPlaying) {
124 | videoView.pause()
125 | renderState = RenderState.PAUSED
126 | } else {
127 | videoView.resume()
128 | renderState = RenderState.PLAYING
129 | }
130 | }
131 | }
132 | return handled
133 | }
134 |
135 | private inner class VideoViewRenderControl(private val videoView: VideoView) : RenderControl {
136 | override val currentPosition: Long
137 | get() = videoView.currentPosition.toLong()
138 | override val duration: Long
139 | get() = videoView.duration.toLong()
140 |
141 | override fun play(speed: Double?) { // video view 不支持倍速播放
142 | videoView.start()
143 | renderState = RenderState.PLAYING
144 | }
145 |
146 | override fun pause() {
147 | videoView.pause()
148 | renderState = RenderState.PAUSED
149 | }
150 |
151 | override fun seek(millSeconds: Long) = videoView.seekTo(millSeconds.toInt())
152 | override fun stop() {
153 | videoView.stopPlayback()
154 | renderState = RenderState.STOPPED
155 | // close player
156 | finish()
157 | }
158 |
159 | override fun getState(): RenderState = renderState
160 | }
161 | }
162 |
163 |
--------------------------------------------------------------------------------
/tv/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
16 |
21 |
26 |
31 |
36 |
41 |
46 |
51 |
56 |
61 |
66 |
71 |
76 |
81 |
86 |
91 |
96 |
101 |
106 |
111 |
116 |
121 |
126 |
131 |
136 |
141 |
146 |
151 |
156 |
161 |
166 |
171 |
172 |
--------------------------------------------------------------------------------
/tv/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
14 |
17 |
20 |
23 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/tv/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
--------------------------------------------------------------------------------
/tv/src/main/res/layout/activity_videoview_renderer.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
19 |
20 |
32 |
33 |
46 |
47 |
55 |
56 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/tv/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/tv/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/tv/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/tv/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/tv/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/tv/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/tv/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/tv/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/tv/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devin1014/DLNA-Cast/6c00d1cc2e74393ccd44ae37e5220495817a77b3/tv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/tv/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/tv/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
11 | #FF03DAC5
12 |
--------------------------------------------------------------------------------
/tv/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DLNA 播放器
3 |
--------------------------------------------------------------------------------
/tv/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
--------------------------------------------------------------------------------
/tv/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------