├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── vmloft │ │ └── develop │ │ └── app │ │ └── screencast │ │ ├── VApplication.java │ │ ├── VConstants.java │ │ ├── VError.java │ │ ├── VIntents.java │ │ ├── callback │ │ ├── AVTransportCallback.java │ │ ├── BaseSubscriptionCallback.java │ │ ├── ContentBrowseCallback.java │ │ ├── ControlCallback.java │ │ └── RenderingControlCallback.java │ │ ├── database │ │ └── MediaContentDao.java │ │ ├── entity │ │ ├── AVTransportInfo.java │ │ ├── ClingDevice.java │ │ ├── RemoteItem.java │ │ ├── RenderingControlInfo.java │ │ └── VItem.java │ │ ├── listener │ │ ├── ClingRegistryListener.java │ │ └── ItemClickListener.java │ │ ├── manager │ │ ├── ClingManager.java │ │ ├── ControlManager.java │ │ └── DeviceManager.java │ │ ├── service │ │ ├── ClingService.java │ │ ├── SystemService.java │ │ └── upnp │ │ │ ├── AndroidJettyServletContainer.java │ │ │ ├── AudioResourceServlet.java │ │ │ ├── ClingContentDirectoryService.java │ │ │ ├── ImageResourceServlet.java │ │ │ ├── JettyResourceServer.java │ │ │ └── VideoResourceServlet.java │ │ ├── ui │ │ ├── DeviceListActivity.java │ │ ├── DeviceListFragment.java │ │ ├── LocalContentFragment.java │ │ ├── MainActivity.java │ │ ├── MediaPlayActivity.java │ │ ├── RemoteContentFragment.java │ │ ├── adapter │ │ │ ├── ClingDeviceAdapter.java │ │ │ ├── LocalContentAdapter.java │ │ │ └── RemoteContentAdapter.java │ │ └── event │ │ │ ├── ControlEvent.java │ │ │ ├── DIDLEvent.java │ │ │ └── DeviceEvent.java │ │ └── utils │ │ └── ClingUtil.java │ └── res │ ├── drawable │ ├── ic_arrow_back_24dp.xml │ ├── ic_cast_24dp.xml │ ├── ic_close_24dp.xml │ ├── ic_done_24dp.xml │ ├── ic_file_24dp.xml │ ├── ic_folder_24dp.xml │ ├── ic_launcher_background.xml │ ├── ic_pause_circle_outline_24dp.xml │ ├── ic_play_circle_outline_24dp.xml │ ├── ic_power_24dp.xml │ ├── ic_refresh_24dp.xml │ ├── ic_skip_next_24dp.xml │ ├── ic_skip_previous_24dp.xml │ ├── ic_stop_24dp.xml │ ├── ic_volume_off_24dp.xml │ └── ic_volume_up_24dp.xml │ ├── layout │ ├── activity_device_list.xml │ ├── activity_main.xml │ ├── activity_media_play.xml │ ├── fragment_local_content.xml │ ├── fragment_remote_content.xml │ ├── framgnet_device_list.xml │ └── item_common_layout.xml │ ├── menu │ └── play_menu.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-v21 │ └── styles.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | VMScreenCast 2 | ======== 3 | 4 | 使用 Cling 实现了 DMS DMC 功能的投屏应用 5 | 6 | ### #简介 7 | 本项目使用 [4thline/cling](https://github.com/4thline/cling) 库实现投屏功能,其中实现了`DMS`及`DMC`功能, 8 | 测试是通过智能电视和盒子设备,包括手机端安装 AirPin 软件进行测试 9 | 10 | 11 | ### #实现功能 12 | - 浏览本地资源投屏播放 13 | - 请求网络资源,投屏播放网络视频 14 | - 控制投屏设备,静音,拖动,播放,暂停等 15 | - 根据投屏设备播放状态,同步更新本地设备播放状态,包括进度音量等 16 | 17 | 18 | ### #遇到的问题及解决 19 | - 首先是在刚开始用另一台手机安装 AirPin 测试的,测试`设置音量`和`视频的拖动进度`不可用投屏设备无效, 20 | 后来在真正的智能电视和盒子上测试有效,这个应该是因为手机设备本身不支持导致 21 | - 后来在只能电视和盒子上测试时发现`BaseSubscriptionCallback`这个回调部分支持的并不全, 22 | 比如在 AirPin 上测试回调会有当前播放进度,当前音量和静音状态,这个在电视和盒子上是都不会回调的, 23 | 所以这里为了兼容实现,这里做了调整,改为手动去开定时器去获取当前播放进度和声音状态 24 | - 现在又遇到一个新的问题,就是在小米盒子上,获取到的声音永远是 0,这个暂时还没找到解决方案 25 | 26 | 27 | ### #参考&感谢 28 | - [Cling DLNA库](https://github.com/4thline/cling) 29 | - [DLNA 百科介绍](https://baike.baidu.com/item/DLNA) 30 | - [简书-细卷子](https://www.jianshu.com/p/4452182d2b48) 31 | - [hubing8658/UPnP-DLNA-Demo](https://github.com/hubing8658/UPnP-DLNA-Demo) 32 | - [kevinshine/BeyondUPnP](https://github.com/kevinshine/BeyondUPnP) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | 5 | compileSdkVersion 27 6 | buildToolsVersion '27.0.3' 7 | 8 | useLibrary 'org.apache.http.legacy' 9 | 10 | defaultConfig { 11 | applicationId "com.vmloft.develop.app.screencast" 12 | minSdkVersion 16 13 | targetSdkVersion 22 14 | versionCode 1 15 | versionName '0.1.0' 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_1_7 25 | targetCompatibility JavaVersion.VERSION_1_7 26 | } 27 | packagingOptions { 28 | exclude 'META-INF/beans.xml' 29 | } 30 | } 31 | 32 | dependencies { 33 | implementation fileTree(include: ['*.jar'], dir: 'libs') 34 | implementation 'com.android.support:design:27.1.1' 35 | // ButterKnife 库 36 | implementation 'com.jakewharton:butterknife:8.8.1' 37 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' 38 | implementation 'org.greenrobot:eventbus:3.0.0' 39 | // Cling library 40 | implementation 'org.fourthline.cling:cling-core:2.1.1' 41 | implementation 'org.fourthline.cling:cling-support:2.1.1' 42 | // Jetty library 43 | implementation 'org.eclipse.jetty:jetty-server:8.1.21.v20160908' 44 | implementation 'org.eclipse.jetty:jetty-servlet:8.1.21.v20160908' 45 | implementation 'org.eclipse.jetty:jetty-client:8.1.21.v20160908' 46 | // 日志管理 47 | implementation 'org.slf4j:slf4j-simple:1.7.21' 48 | // https://mvnrepository.com/artifact/javax.enterprise/cdi-api 49 | compileOnly group: 'javax.enterprise', name: 'cdi-api', version: '2.0' 50 | 51 | // 自己封装的工具库 52 | implementation 'com.vmloft.library:vmtools:0.1.2' 53 | } 54 | -------------------------------------------------------------------------------- /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 | 12 | 13 | 14 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/VApplication.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast; 2 | 3 | import com.vmloft.develop.app.screencast.manager.ClingManager; 4 | import com.vmloft.develop.library.tools.VMApp; 5 | 6 | /** 7 | * Created by lzan13 on 2018/3/15. 8 | */ 9 | public class VApplication extends VMApp { 10 | @Override 11 | public void onCreate() { 12 | super.onCreate(); 13 | 14 | init(); 15 | } 16 | 17 | 18 | public void init() { 19 | ClingManager.getInstance().startClingService(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/VConstants.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast; 2 | 3 | /** 4 | * Created by lzan13 on 2018/3/10. 5 | * 常量类 6 | */ 7 | public class VConstants { 8 | 9 | public static final int JETTY_SERVER_PORT = 55677; 10 | 11 | public static final String ROOT_OBJECT_ID = "0"; 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/VError.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast; 2 | 3 | /** 4 | * Created by lzan13 on 2018/3/19. 5 | */ 6 | 7 | public class VError { 8 | public static int NO_ERROR = 0; 9 | public static int UNKNOWN = 1; 10 | public static int SYSTEM = 2; 11 | 12 | public static int DEVICE_IS_NULL = 100; 13 | public static int SERVICE_IS_NULL = 101; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/VIntents.java: -------------------------------------------------------------------------------- 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.vmloft.develop.app.screencast; 17 | 18 | import android.content.Intent; 19 | 20 | import java.io.Serializable; 21 | 22 | public class VIntents { 23 | /** 24 | * Prefix for all intents created 25 | */ 26 | public static final String INTENT_PREFIX = "com.vmloft.develop.app.screencast."; 27 | 28 | /** 29 | * Prefix for all extra data added to intents 30 | */ 31 | public static final String INTENT_EXTRA_PREFIX = INTENT_PREFIX + "extra."; 32 | 33 | /** 34 | * Prefix for all action in intents 35 | */ 36 | public static final String INTENT_ACTION_PREFIX = INTENT_PREFIX + "action."; 37 | 38 | /** 39 | * Playing action for MediaPlayer 40 | */ 41 | public static final String ACTION_PLAYING = INTENT_ACTION_PREFIX + "playing"; 42 | 43 | /** 44 | * Paused playback action for MediaPlayer 45 | */ 46 | public static final String ACTION_PAUSED_PLAYBACK = INTENT_ACTION_PREFIX + "paused_playback"; 47 | 48 | /** 49 | * Stopped action for MediaPlayer 50 | */ 51 | public static final String ACTION_STOPPED = INTENT_ACTION_PREFIX + "stopped"; 52 | 53 | /** 54 | * Change device action for MediaPlayer 55 | */ 56 | public static final String ACTION_CHANGE_DEVICE = INTENT_ACTION_PREFIX + "change_device"; 57 | 58 | /** 59 | * Set volume action for MediaPlayer 60 | */ 61 | public static final String ACTION_SET_VOLUME = INTENT_ACTION_PREFIX + "set_volume"; 62 | 63 | /** 64 | * Update the lastChange value action for MediaPlayer 65 | */ 66 | public static final String ACTION_UPDATE_LAST_CHANGE = INTENT_ACTION_PREFIX + "update_last_change"; 67 | 68 | /** 69 | * Builder for generating an intent configured with extra data. 70 | */ 71 | public static class Builder { 72 | 73 | private final Intent intent; 74 | 75 | /** 76 | * Create builder with suffix 77 | * 78 | * @param actionSuffix 79 | */ 80 | public Builder(String actionSuffix) { 81 | intent = new Intent(INTENT_PREFIX + actionSuffix); 82 | } 83 | 84 | /** 85 | * Add extra field data value to intent being built up 86 | * 87 | * @param fieldName 88 | * @param value 89 | * @return this builder 90 | */ 91 | public Builder add(String fieldName, String value) { 92 | intent.putExtra(fieldName, value); 93 | return this; 94 | } 95 | 96 | /** 97 | * Add extra field data values to intent being built up 98 | * 99 | * @param fieldName 100 | * @param values 101 | * @return this builder 102 | */ 103 | public Builder add(String fieldName, CharSequence[] values) { 104 | intent.putExtra(fieldName, values); 105 | return this; 106 | } 107 | 108 | /** 109 | * Add extra field data value to intent being built up 110 | * 111 | * @param fieldName 112 | * @param value 113 | * @return this builder 114 | */ 115 | public Builder add(String fieldName, int value) { 116 | intent.putExtra(fieldName, value); 117 | return this; 118 | } 119 | 120 | /** 121 | * Add extra field data value to intent being built up 122 | * 123 | * @param fieldName 124 | * @param values 125 | * @return this builder 126 | */ 127 | public Builder add(String fieldName, int[] values) { 128 | intent.putExtra(fieldName, values); 129 | return this; 130 | } 131 | 132 | /** 133 | * Add extra field data value to intent being built up 134 | * 135 | * @param fieldName 136 | * @param values 137 | * @return this builder 138 | */ 139 | public Builder add(String fieldName, boolean[] values) { 140 | intent.putExtra(fieldName, values); 141 | return this; 142 | } 143 | 144 | /** 145 | * Add extra field data value to intent being built up 146 | * 147 | * @param fieldName 148 | * @param value 149 | * @return this builder 150 | */ 151 | public Builder add(String fieldName, Serializable value) { 152 | intent.putExtra(fieldName, value); 153 | return this; 154 | } 155 | 156 | /** 157 | * Get built intent 158 | * 159 | * @return intent 160 | */ 161 | public Intent toIntent() { 162 | return intent; 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/callback/AVTransportCallback.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.callback; 2 | 3 | import com.vmloft.develop.app.screencast.entity.AVTransportInfo; 4 | import com.vmloft.develop.library.tools.utils.VMLog; 5 | 6 | import org.fourthline.cling.model.meta.Service; 7 | import org.fourthline.cling.support.avtransport.lastchange.AVTransportLastChangeParser; 8 | import org.fourthline.cling.support.lastchange.EventedValue; 9 | import org.fourthline.cling.support.lastchange.LastChangeParser; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Created by lzan13 on 2018/3/5. 15 | * 投屏传输状态相关回调,这里主要回调播放状态,以及播放进度,播放资源的整体时间, 16 | * 但是经测试这个进度和整体时间只在 AirPin 上有效,小米盒子和天猫盒子无效 17 | */ 18 | public abstract class AVTransportCallback extends BaseSubscriptionCallback { 19 | private final String TAG = this.getClass().getSimpleName(); 20 | private final String AVT_STATE = "TransportState"; 21 | private final String AVT_DURATION = "CurrentMediaDuration"; 22 | private final String AVT_RELATIVE_TIME = "RelativeTimePosition"; 23 | private final String AVT_ABSOLUTE_TIME = "AbsoluteTimePosition"; 24 | 25 | 26 | protected AVTransportCallback(Service service) { 27 | super(service); 28 | } 29 | 30 | @Override 31 | protected LastChangeParser getLastChangeParser() { 32 | return new AVTransportLastChangeParser(); 33 | } 34 | 35 | @Override 36 | protected void onReceived(List values) { 37 | AVTransportInfo info = new AVTransportInfo(); 38 | EventedValue value = values.get(0); 39 | String name = value.getName(); 40 | Object obj = value.getValue(); 41 | VMLog.d("AVTransportCallback onReceived: %s, %s, %d", name, obj.toString(), values.size()); 42 | 43 | if (AVT_STATE.equals(name)) { 44 | info.setState(value.getValue().toString()); 45 | } else if (AVT_DURATION.equals(name)) { 46 | info.setMediaDuration(value.getValue().toString()); 47 | } else if (AVT_RELATIVE_TIME.equals(name)) { 48 | // info.setTimePosition((String) value.getValue()); 49 | } else if (AVT_ABSOLUTE_TIME.equals(name)) { 50 | info.setTimePosition(value.getValue().toString()); 51 | } 52 | 53 | received(info); 54 | } 55 | 56 | protected abstract void received(AVTransportInfo info); 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/callback/BaseSubscriptionCallback.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.callback; 2 | 3 | import com.vmloft.develop.library.tools.utils.VMLog; 4 | 5 | import org.fourthline.cling.controlpoint.SubscriptionCallback; 6 | import org.fourthline.cling.model.gena.CancelReason; 7 | import org.fourthline.cling.model.gena.GENASubscription; 8 | import org.fourthline.cling.model.message.UpnpResponse; 9 | import org.fourthline.cling.model.meta.Service; 10 | import org.fourthline.cling.model.state.StateVariableValue; 11 | import org.fourthline.cling.support.lastchange.Event; 12 | import org.fourthline.cling.support.lastchange.EventedValue; 13 | import org.fourthline.cling.support.lastchange.InstanceID; 14 | import org.fourthline.cling.support.lastchange.LastChange; 15 | import org.fourthline.cling.support.lastchange.LastChangeParser; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | /** 22 | * Created by lzan13 on 2018/3/9. 23 | */ 24 | public abstract class BaseSubscriptionCallback extends SubscriptionCallback { 25 | 26 | // 订阅持续时间 秒,这里设置3小时 27 | private static final int SUB_DURATION = 60 * 60 * 3; 28 | 29 | protected BaseSubscriptionCallback(Service service) { 30 | this(service, SUB_DURATION); 31 | } 32 | 33 | protected BaseSubscriptionCallback(Service service, int requestedDurationSeconds) { 34 | super(service, requestedDurationSeconds); 35 | } 36 | 37 | @Override 38 | protected void failed(GENASubscription subscription, UpnpResponse responseStatus, Exception exception, String defaultMsg) { 39 | VMLog.d("SubscriptionCallback failed"); 40 | } 41 | 42 | @Override 43 | protected void ended(GENASubscription subscription, CancelReason reason, UpnpResponse responseStatus) { 44 | VMLog.d("SubscriptionCallback ended"); 45 | } 46 | 47 | @Override 48 | protected void established(GENASubscription subscription) {} 49 | 50 | @Override 51 | protected void eventsMissed(GENASubscription subscription, int numberOfMissedEvents) {} 52 | 53 | @Override 54 | protected void eventReceived(GENASubscription subscription) { 55 | Map values = subscription.getCurrentValues(); 56 | if (values != null && values.containsKey("LastChange")) { 57 | LastChangeParser parser = getLastChangeParser(); 58 | String lastChangeValue = values.get("LastChange").toString(); 59 | VMLog.d("Last change value: %s", lastChangeValue); 60 | try { 61 | Event event = parser.parse(lastChangeValue); 62 | List ids = event.getInstanceIDs(); 63 | List eventValues = new ArrayList<>(); 64 | for (InstanceID id : ids) { 65 | eventValues.addAll(id.getValues()); 66 | } 67 | onReceived(eventValues); 68 | } catch (Exception e) { 69 | e.printStackTrace(); 70 | return; 71 | } 72 | } 73 | } 74 | 75 | protected abstract LastChangeParser getLastChangeParser(); 76 | 77 | protected abstract void onReceived(List values); 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/callback/ContentBrowseCallback.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.callback; 2 | 3 | import org.fourthline.cling.model.meta.Service; 4 | import org.fourthline.cling.support.contentdirectory.callback.Browse; 5 | import org.fourthline.cling.support.model.BrowseFlag; 6 | import org.fourthline.cling.support.model.SortCriterion; 7 | 8 | /** 9 | * Created by lzan13 on 2018/3/18. 10 | */ 11 | public abstract class ContentBrowseCallback extends Browse { 12 | 13 | public ContentBrowseCallback(Service service, String containerId) { 14 | super(service, containerId, BrowseFlag.DIRECT_CHILDREN, "*", 0, null, 15 | new SortCriterion(true, "dc:title")); 16 | } 17 | 18 | @Override 19 | public void updateStatus(Status status) { 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/callback/ControlCallback.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.callback; 2 | 3 | /** 4 | * Created by lzan13 on 2018/3/10. 5 | * 投屏控制回调 6 | */ 7 | public interface ControlCallback { 8 | void onSuccess(); 9 | 10 | void onError(int code, String msg); 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/callback/RenderingControlCallback.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.callback; 2 | 3 | import android.content.Context; 4 | 5 | import com.vmloft.develop.app.screencast.entity.RenderingControlInfo; 6 | import com.vmloft.develop.library.tools.utils.VMLog; 7 | 8 | import org.fourthline.cling.model.meta.Service; 9 | import org.fourthline.cling.support.avtransport.lastchange.AVTransportLastChangeParser; 10 | import org.fourthline.cling.support.lastchange.EventedValue; 11 | import org.fourthline.cling.support.lastchange.LastChange; 12 | import org.fourthline.cling.support.lastchange.LastChangeParser; 13 | import org.fourthline.cling.support.model.Channel; 14 | import org.fourthline.cling.support.renderingcontrol.lastchange.ChannelMute; 15 | import org.fourthline.cling.support.renderingcontrol.lastchange.ChannelVolume; 16 | import org.fourthline.cling.support.renderingcontrol.lastchange.RenderingControlLastChangeParser; 17 | import org.fourthline.cling.support.renderingcontrol.lastchange.RenderingControlVariable; 18 | 19 | import java.util.List; 20 | 21 | /** 22 | * Created by lzan13 on 2018/3/5. 23 | * 投屏播放控制相关回调,这里主要会回调音量和是否静音 24 | */ 25 | public abstract class RenderingControlCallback extends BaseSubscriptionCallback { 26 | private final String TAG = this.getClass().getSimpleName(); 27 | 28 | protected RenderingControlCallback(Service service) { 29 | super(service); 30 | } 31 | 32 | @Override 33 | protected LastChangeParser getLastChangeParser() { 34 | return new RenderingControlLastChangeParser(); 35 | } 36 | 37 | @Override 38 | protected void onReceived(List values) { 39 | RenderingControlInfo info = new RenderingControlInfo(); 40 | for (EventedValue entry : values) { 41 | if ("Mute".equals(entry.getName())) { 42 | Object obj = entry.getValue(); 43 | if (obj instanceof ChannelMute) { 44 | ChannelMute cm = (ChannelMute) obj; 45 | if (Channel.Master.equals(cm.getChannel())) { 46 | info.setMute(cm.getMute()); 47 | } 48 | } 49 | } 50 | if ("Volume".equals(entry.getName())) { 51 | Object obj = entry.getValue(); 52 | if (obj instanceof ChannelVolume) { 53 | ChannelVolume cv = (ChannelVolume) obj; 54 | if (Channel.Master.equals(cv.getChannel())) { 55 | info.setVolume(cv.getVolume()); 56 | } 57 | } 58 | } 59 | if ("PresetNameList".equals(entry.getName())) { 60 | Object obj = entry.getValue(); 61 | info.setPresetNameList(obj.toString()); 62 | } 63 | } 64 | VMLog.d("RenderingControlCallback onReceived:%b, %d", info.isMute(), info.getVolume()); 65 | received(info); 66 | } 67 | 68 | protected abstract void received(RenderingControlInfo info); 69 | 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/database/MediaContentDao.java: -------------------------------------------------------------------------------- 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.vmloft.develop.app.screencast.database; 17 | 18 | import android.content.ContentResolver; 19 | import android.content.Context; 20 | import android.database.Cursor; 21 | import android.provider.MediaStore; 22 | import android.provider.MediaStore.Video; 23 | import android.provider.MediaStore.Audio; 24 | import android.provider.MediaStore.Images; 25 | 26 | import com.vmloft.develop.app.screencast.VConstants; 27 | import com.vmloft.develop.app.screencast.entity.VItem; 28 | import com.vmloft.develop.library.tools.utils.VMNetwork; 29 | 30 | import org.fourthline.cling.model.ModelUtil; 31 | import org.fourthline.cling.support.model.PersonWithRole; 32 | import org.fourthline.cling.support.model.Res; 33 | import org.fourthline.cling.support.model.item.ImageItem; 34 | import org.fourthline.cling.support.model.item.Item; 35 | import org.fourthline.cling.support.model.item.Movie; 36 | import org.fourthline.cling.support.model.item.MusicTrack; 37 | import org.seamless.util.MimeType; 38 | 39 | import java.io.File; 40 | import java.util.ArrayList; 41 | 42 | 43 | public class MediaContentDao { 44 | 45 | private ContentResolver cr; 46 | private String serverURL; 47 | 48 | public MediaContentDao(Context context) { 49 | cr = context.getContentResolver(); 50 | serverURL = "http://" + VMNetwork.getLocalIP() + ":" + VConstants.JETTY_SERVER_PORT + "/"; 51 | } 52 | 53 | public ArrayList getAudioItems() { 54 | ArrayList items = new ArrayList<>(); 55 | 56 | String[] audioColumns = {Audio.Media._ID, Audio.Media.TITLE, Audio.Media.DATA, 57 | Audio.Media.ARTIST, Audio.Media.MIME_TYPE, Audio.Media.SIZE, Audio.Media.DURATION, 58 | Audio.Media.ALBUM}; 59 | 60 | Cursor cur = cr.query(Audio.Media.EXTERNAL_CONTENT_URI, audioColumns, null, null, null); 61 | 62 | if (cur == null) { 63 | return items; 64 | } 65 | 66 | while (cur.moveToNext()) { 67 | String id = String.valueOf(cur.getInt(cur.getColumnIndex(Audio.Media._ID))); 68 | String title = cur.getString(cur.getColumnIndex(Audio.Media.TITLE)); 69 | String creator = cur.getString(cur.getColumnIndex(Audio.Media.ARTIST)); 70 | String mimeType = cur.getString(cur.getColumnIndex(Audio.Media.MIME_TYPE)); 71 | long size = cur.getLong(cur.getColumnIndex(Audio.Media.SIZE)); 72 | long duration = cur.getLong(cur.getColumnIndex(Audio.Media.DURATION)); 73 | String durationStr = ModelUtil.toTimeString(duration / 1000); 74 | String album = cur.getString(cur.getColumnIndex(Audio.Media.ALBUM)); 75 | String data = cur.getString(cur.getColumnIndexOrThrow(Audio.Media.DATA)); 76 | String fileName = data.substring(data.lastIndexOf(File.separator)); 77 | String ext = fileName.substring(fileName.lastIndexOf(".")); 78 | data = data.replace(fileName, File.separator + id + ext); 79 | String url = serverURL + "audio" + data; 80 | Res res = new Res(mimeType, size, durationStr, null, url); 81 | MusicTrack musicTrack = new MusicTrack(id, VItem.AUDIO_ID, title, creator, album, 82 | new PersonWithRole(creator), res); 83 | items.add(musicTrack); 84 | } 85 | 86 | return items; 87 | } 88 | 89 | public ArrayList getImageItems() { 90 | ArrayList items = new ArrayList<>(); 91 | 92 | String[] imageColumns = {Images.Media._ID, Images.Media.TITLE, Images.Media.DATA, 93 | Images.Media.MIME_TYPE, Images.Media.SIZE}; 94 | 95 | Cursor cur = cr.query(Images.Media.EXTERNAL_CONTENT_URI, imageColumns, null, null, null); 96 | 97 | if (cur == null) { 98 | return items; 99 | } 100 | 101 | while (cur.moveToNext()) { 102 | String id = String.valueOf(cur.getInt(cur.getColumnIndex(MediaStore.Images.Media._ID))); 103 | String title = cur.getString(cur.getColumnIndex(MediaStore.Images.Media.TITLE)); 104 | String creator = cur.getString(cur.getColumnIndexOrThrow(Images.Media.TITLE)); 105 | String mimeType = cur.getString(cur.getColumnIndex(MediaStore.Images.Media.MIME_TYPE)); 106 | long size = cur.getLong(cur.getColumnIndex(MediaStore.Images.Media.SIZE)); 107 | String data = cur.getString(cur.getColumnIndexOrThrow(Audio.Media.DATA)); 108 | String fileName = data.substring(data.lastIndexOf(File.separator)); 109 | String ext = fileName.substring(fileName.lastIndexOf(".")); 110 | data = data.replace(fileName, File.separator + id + ext); 111 | String url = serverURL + "image" + data; 112 | Res res = new Res(new MimeType(mimeType.substring(0, mimeType.indexOf('/')), 113 | mimeType.substring(mimeType.indexOf('/') + 1)), size, url); 114 | ImageItem imageItem = new ImageItem(id, VItem.IMAGE_ID, title, creator, res); 115 | items.add(imageItem); 116 | } 117 | 118 | return items; 119 | } 120 | 121 | public ArrayList getVideoItems() { 122 | ArrayList items = new ArrayList<>(); 123 | 124 | String[] videoColumns = {Video.Media._ID, Video.Media.TITLE, Video.Media.DATA, 125 | Video.Media.ARTIST, Video.Media.MIME_TYPE, Video.Media.SIZE, Video.Media.DURATION, 126 | Video.Media.RESOLUTION}; 127 | 128 | Cursor cur = cr.query(Video.Media.EXTERNAL_CONTENT_URI, videoColumns, null, null, null); 129 | 130 | if (cur == null) { 131 | return items; 132 | } 133 | 134 | while (cur.moveToNext()) { 135 | String id = String.valueOf(cur.getInt(cur.getColumnIndex(Video.Media._ID))); 136 | String title = cur.getString(cur.getColumnIndex(Video.Media.TITLE)); 137 | String creator = cur.getString(cur.getColumnIndex(Video.Media.ARTIST)); 138 | String mimeType = cur.getString(cur.getColumnIndex(Video.Media.MIME_TYPE)); 139 | long size = cur.getLong(cur.getColumnIndex(Video.Media.SIZE)); 140 | long duration = cur.getLong(cur.getColumnIndex(Video.Media.DURATION)); 141 | String durationStr = ModelUtil.toTimeString(duration / 1000); 142 | String resolution = cur.getString(cur.getColumnIndex(Video.Media.RESOLUTION)); 143 | String data = cur.getString(cur.getColumnIndexOrThrow(Audio.Media.DATA)); 144 | String fileName = data.substring(data.lastIndexOf(File.separator)); 145 | String ext = fileName.substring(fileName.lastIndexOf(".")); 146 | data = data.replace(fileName, File.separator + id + ext); 147 | String url = serverURL + "video" + data; 148 | Res res = new Res(mimeType, size, durationStr, null, url); 149 | res.setDuration(ModelUtil.toTimeString(duration / 1000)); 150 | res.setResolution(resolution); 151 | Movie movie = new Movie(id, VItem.VIDEO_ID, title, creator, res); 152 | items.add(movie); 153 | } 154 | return items; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/entity/AVTransportInfo.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.entity; 2 | 3 | /** 4 | * Created by lzan13 on 2018/4/11. 5 | */ 6 | public class AVTransportInfo { 7 | 8 | public static String TRANSITIONING = "TRANSITIONING"; 9 | public static String PLAYING = "PLAYING"; 10 | public static String PAUSED_PLAYBACK = "PAUSED_PLAYBACK"; 11 | public static String STOPPED = "STOPPED"; 12 | 13 | private String state; 14 | private String mediaDuration; 15 | private String timePosition; 16 | 17 | public AVTransportInfo() {} 18 | 19 | public String getState() { 20 | return state; 21 | } 22 | 23 | public void setState(String state) { 24 | this.state = state; 25 | } 26 | 27 | public String getMediaDuration() { 28 | return mediaDuration; 29 | } 30 | 31 | public void setMediaDuration(String mediaDuration) { 32 | this.mediaDuration = mediaDuration; 33 | } 34 | 35 | public String getTimePosition() { 36 | return timePosition; 37 | } 38 | 39 | public void setTimePosition(String timePosition) { 40 | this.timePosition = timePosition; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/entity/ClingDevice.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.entity; 2 | 3 | import org.fourthline.cling.model.meta.Device; 4 | 5 | /** 6 | * Created by lzan13 on 2018/3/5. 7 | */ 8 | public class ClingDevice { 9 | private Device device; 10 | private boolean isSelected = false; 11 | 12 | public ClingDevice(Device device) { 13 | this.device = device; 14 | } 15 | 16 | public Device getDevice() { 17 | return device; 18 | } 19 | 20 | public boolean isSelected() { 21 | return isSelected; 22 | } 23 | 24 | public void setSelected(boolean selected) { 25 | isSelected = selected; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/entity/RemoteItem.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.entity; 2 | 3 | /** 4 | * Created by lzan13 on 2018/3/24. 5 | * 网络资源实体类 6 | */ 7 | public class RemoteItem { 8 | private String title; 9 | private String id; 10 | private String creator; 11 | private long size; 12 | private String duration; 13 | private String resolution; 14 | private String url; 15 | 16 | public RemoteItem(String title, String id, String creator, long size, String duration, 17 | String resolution, String url) { 18 | setTitle(title); 19 | setId(id); 20 | setCreator(creator); 21 | setSize(size); 22 | setDuration(duration); 23 | setResolution(resolution); 24 | setUrl(url); 25 | } 26 | 27 | public String getTitle() { 28 | return title; 29 | } 30 | 31 | public void setTitle(String title) { 32 | this.title = title; 33 | } 34 | 35 | public String getId() { 36 | return id; 37 | } 38 | 39 | public void setId(String id) { 40 | this.id = id; 41 | } 42 | 43 | public String getCreator() { 44 | return creator; 45 | } 46 | 47 | public void setCreator(String creator) { 48 | this.creator = creator; 49 | } 50 | 51 | public long getSize() { 52 | return size; 53 | } 54 | 55 | public void setSize(long size) { 56 | this.size = size; 57 | } 58 | 59 | public String getDuration() { 60 | return duration; 61 | } 62 | 63 | public void setDuration(String duration) { 64 | this.duration = duration; 65 | } 66 | 67 | public String getResolution() { 68 | return resolution; 69 | } 70 | 71 | public void setResolution(String resolution) { 72 | this.resolution = resolution; 73 | } 74 | 75 | public String getUrl() { 76 | return url; 77 | } 78 | 79 | public void setUrl(String url) { 80 | this.url = url; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/entity/RenderingControlInfo.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.entity; 2 | 3 | /** 4 | * Created by lzan13 on 2018/4/11. 5 | */ 6 | public class RenderingControlInfo { 7 | private boolean isMute; 8 | private int volume; 9 | private String presetNameList; 10 | 11 | public RenderingControlInfo() {} 12 | 13 | public RenderingControlInfo(boolean mute, int volume) { 14 | this.isMute = mute; 15 | this.volume = volume; 16 | } 17 | 18 | public boolean isMute() { 19 | return isMute; 20 | } 21 | 22 | public void setMute(boolean mute) { 23 | isMute = mute; 24 | } 25 | 26 | public int getVolume() { 27 | return volume; 28 | } 29 | 30 | public void setVolume(int volume) { 31 | this.volume = volume; 32 | } 33 | 34 | public String getPresetNameList() { 35 | return presetNameList; 36 | } 37 | 38 | public void setPresetNameList(String presetNameList) { 39 | this.presetNameList = presetNameList; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/entity/VItem.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.entity; 2 | 3 | import org.fourthline.cling.support.model.DIDLObject; 4 | import org.fourthline.cling.support.model.DescMeta; 5 | import org.fourthline.cling.support.model.Res; 6 | import org.fourthline.cling.support.model.WriteStatus; 7 | import org.fourthline.cling.support.model.container.Container; 8 | import org.fourthline.cling.support.model.item.Item; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * Created by lzan13 on 2018/3/21. 15 | * 自定义 Cling 实体类, 16 | */ 17 | public class VItem { 18 | 19 | public final static String ROOT_ID = "0"; 20 | public final static String AUDIO_ID = "10"; 21 | public final static String VIDEO_ID = "20"; 22 | public final static String IMAGE_ID = "30"; 23 | 24 | public static final DIDLObject.Class AUDIO_CLASS = new DIDLObject.Class( 25 | "object.container.audio"); 26 | public static final DIDLObject.Class IMAGE_CLASS = new DIDLObject.Class( 27 | "object.item.imageItem"); 28 | public static final DIDLObject.Class VIDEO_CLASS = new DIDLObject.Class( 29 | "object.container.video"); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/listener/ClingRegistryListener.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.listener; 2 | 3 | import com.vmloft.develop.app.screencast.manager.DeviceManager; 4 | import com.vmloft.develop.library.tools.utils.VMLog; 5 | 6 | import org.fourthline.cling.model.meta.Device; 7 | import org.fourthline.cling.model.meta.LocalDevice; 8 | import org.fourthline.cling.model.meta.RemoteDevice; 9 | import org.fourthline.cling.registry.DefaultRegistryListener; 10 | import org.fourthline.cling.registry.Registry; 11 | 12 | /** 13 | * Created by lzan13 on 2018/3/9. 14 | * 监听当前局域网设备变化 15 | */ 16 | public class ClingRegistryListener extends DefaultRegistryListener { 17 | @Override 18 | public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) { 19 | VMLog.d("remoteDeviceDiscoveryStarted %s", device.getDisplayString()); 20 | // onDeviceAdded(device); 21 | } 22 | 23 | @Override 24 | public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex) { 25 | VMLog.e("remoteDeviceDiscoveryFailed %s - %s", device.getDisplayString(), ex.toString()); 26 | // onDeviceRemoved(device); 27 | } 28 | 29 | @Override 30 | public void remoteDeviceAdded(Registry registry, RemoteDevice device) { 31 | VMLog.i("remoteDeviceAdded %s", device.getDisplayString()); 32 | onDeviceAdded(device); 33 | } 34 | 35 | @Override 36 | public void remoteDeviceRemoved(Registry registry, RemoteDevice device) { 37 | VMLog.e("remoteDeviceRemoved %s", device.getDisplayString()); 38 | onDeviceRemoved(device); 39 | } 40 | 41 | @Override 42 | public void localDeviceAdded(Registry registry, LocalDevice device) { 43 | VMLog.d("localDeviceAdded %s", device.getDisplayString()); 44 | // onDeviceAdded(device); 45 | } 46 | 47 | @Override 48 | public void localDeviceRemoved(Registry registry, LocalDevice device) { 49 | VMLog.d("localDeviceRemoved %s", device.getDisplayString()); 50 | // onDeviceRemoved(device); 51 | } 52 | 53 | /** 54 | * 新增 DLNA 设备 55 | */ 56 | public void onDeviceAdded(Device device) { 57 | DeviceManager.getInstance().addDevice(device); 58 | } 59 | 60 | /** 61 | * 移除 DLNA 设备 62 | */ 63 | public void onDeviceRemoved(Device device) { 64 | DeviceManager.getInstance().removeDevice(device); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/listener/ItemClickListener.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.listener; 2 | 3 | import com.vmloft.develop.library.tools.adapter.VMAdapter; 4 | 5 | /** 6 | * Created by lzan13 on 2018/3/10. 7 | */ 8 | public abstract class ItemClickListener implements VMAdapter.ICListener { 9 | 10 | @Override 11 | public abstract void onItemAction(int action, Object object); 12 | 13 | @Override 14 | public void onItemLongAction(int action, Object object) { 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/manager/ClingManager.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.manager; 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.IBinder; 8 | 9 | import com.vmloft.develop.app.screencast.VApplication; 10 | import com.vmloft.develop.app.screencast.callback.ContentBrowseCallback; 11 | import com.vmloft.develop.app.screencast.entity.RemoteItem; 12 | import com.vmloft.develop.app.screencast.listener.ClingRegistryListener; 13 | import com.vmloft.develop.app.screencast.service.ClingService; 14 | import com.vmloft.develop.app.screencast.service.SystemService; 15 | import com.vmloft.develop.app.screencast.ui.event.DIDLEvent; 16 | import com.vmloft.develop.library.tools.utils.VMLog; 17 | 18 | import org.fourthline.cling.controlpoint.ControlPoint; 19 | import org.fourthline.cling.model.action.ActionInvocation; 20 | import org.fourthline.cling.model.message.UpnpResponse; 21 | import org.fourthline.cling.model.meta.Device; 22 | import org.fourthline.cling.model.meta.Service; 23 | import org.fourthline.cling.model.types.ServiceType; 24 | import org.fourthline.cling.model.types.UDAServiceType; 25 | import org.fourthline.cling.registry.Registry; 26 | import org.fourthline.cling.support.model.DIDLContent; 27 | import org.fourthline.cling.support.model.item.Item; 28 | import org.greenrobot.eventbus.EventBus; 29 | 30 | /** 31 | * Created by lzan13 on 2018/3/6. 32 | */ 33 | public class ClingManager { 34 | private final String TAG = this.getClass().getSimpleName(); 35 | public static final ServiceType CONTENT_DIRECTORY = new UDAServiceType("ContentDirectory"); 36 | 37 | private static ClingManager instance; 38 | private Context context; 39 | 40 | private ClingRegistryListener clingRegistryListener; 41 | private ServiceConnection clingServiceConnection; 42 | private ClingService clingService; 43 | 44 | private ServiceConnection systemServiceConnection; 45 | private SystemService systemService; 46 | 47 | private Item localItem; 48 | private RemoteItem remoteItem; 49 | 50 | /** 51 | * 私有构造方法 52 | */ 53 | private ClingManager() { 54 | context = VApplication.getContext(); 55 | } 56 | 57 | public static ClingManager getInstance() { 58 | if (instance == null) { 59 | instance = new ClingManager(); 60 | } 61 | return instance; 62 | } 63 | 64 | /** 65 | * 获取 UPnP 堆栈核心,注册跟组设备和资源 66 | */ 67 | public Registry getRegistry() { 68 | return clingService.getRegistry(); 69 | } 70 | 71 | /** 72 | * 获取控制点 73 | */ 74 | public ControlPoint getControlPoint() { 75 | return clingService.getControlPoint(); 76 | } 77 | 78 | /** 79 | * 搜索设备 80 | */ 81 | public void searchDevices() { 82 | getControlPoint().search(); 83 | } 84 | 85 | /** 86 | * 设置 ClingService 87 | */ 88 | public void setClingService(ClingService service) { 89 | clingService = service; 90 | } 91 | 92 | public void setSystemService(SystemService service) { 93 | systemService = service; 94 | } 95 | 96 | public void setLocalItem(Item item) { 97 | localItem = item; 98 | remoteItem = null; 99 | ControlManager.getInstance().setState(ControlManager.CastState.STOPED); 100 | } 101 | 102 | public Item getLocalItem() { 103 | return localItem; 104 | } 105 | 106 | public RemoteItem getRemoteItem() { 107 | return remoteItem; 108 | } 109 | 110 | public void setRemoteItem(RemoteItem remoteItem) { 111 | this.remoteItem = remoteItem; 112 | this.localItem = null; 113 | ControlManager.getInstance().setState(ControlManager.CastState.STOPED); 114 | } 115 | 116 | public void startClingService() { 117 | bindService(); 118 | } 119 | 120 | /** 121 | * 绑定服务 122 | */ 123 | private void bindService() { 124 | clingServiceConnection = new ServiceConnection() { 125 | @Override 126 | public void onServiceConnected(ComponentName name, IBinder service) { 127 | VMLog.i("onServiceConnected - %s", name); 128 | ClingService.LocalBinder binder = (ClingService.LocalBinder) service; 129 | ClingService clingService = binder.getService(); 130 | 131 | setClingService(clingService); 132 | 133 | clingRegistryListener = new ClingRegistryListener(); 134 | getRegistry().addListener(clingRegistryListener); 135 | 136 | searchDevices(); 137 | searchLocalContent("0"); 138 | } 139 | 140 | @Override 141 | public void onServiceDisconnected(ComponentName name) { 142 | VMLog.e("onServiceDisconnected - %s", name); 143 | setClingService(null); 144 | } 145 | }; 146 | Intent clingServiceIntent = new Intent(context, ClingService.class); 147 | context.bindService(clingServiceIntent, clingServiceConnection, Context.BIND_AUTO_CREATE); 148 | 149 | 150 | systemServiceConnection = new ServiceConnection() { 151 | @Override 152 | public void onServiceConnected(ComponentName name, IBinder service) { 153 | VMLog.i("onServiceConnected - %s", name); 154 | SystemService.LocalBinder binder = (SystemService.LocalBinder) service; 155 | setSystemService(binder.getService()); 156 | } 157 | 158 | @Override 159 | public void onServiceDisconnected(ComponentName name) { 160 | VMLog.e("onServiceDisconnected - %s", name); 161 | setSystemService(null); 162 | } 163 | }; 164 | Intent systemServiceIntent = new Intent(context, SystemService.class); 165 | context.bindService(systemServiceIntent, systemServiceConnection, Context.BIND_AUTO_CREATE); 166 | } 167 | 168 | public void stopClingService() { 169 | unbindService(); 170 | } 171 | 172 | /** 173 | * 取消服务绑定 174 | */ 175 | private void unbindService() { 176 | if (clingServiceConnection != null) { 177 | context.unbindService(clingServiceConnection); 178 | clingServiceConnection = null; 179 | } 180 | if (systemServiceConnection != null) { 181 | context.unbindService(systemServiceConnection); 182 | systemServiceConnection = null; 183 | } 184 | if (clingService != null) { 185 | clingService.onDestroy(); 186 | clingService = null; 187 | } 188 | if (systemService != null) { 189 | systemService.onDestroy(); 190 | systemService = null; 191 | } 192 | clingRegistryListener = null; 193 | } 194 | 195 | public void searchLocalContent(String containerId) { 196 | Device localDevice = clingService.getLocalDevice(); 197 | Service service = localDevice.findService(CONTENT_DIRECTORY); 198 | ControlPoint controlPoint = clingService.getControlPoint(); 199 | 200 | controlPoint.execute(new ContentBrowseCallback(service, containerId) { 201 | @Override 202 | public void received(ActionInvocation actionInvocation, DIDLContent didl) { 203 | VMLog.e("Load local content! containers:%d, items:%d", didl.getContainers().size(), 204 | didl.getItems().size()); 205 | DIDLEvent event = new DIDLEvent(); 206 | event.content = didl; 207 | EventBus.getDefault().post(event); 208 | } 209 | 210 | @Override 211 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 212 | VMLog.e("Load local content failure %s", msg); 213 | } 214 | }); 215 | } 216 | 217 | public void destroy() { 218 | stopClingService(); 219 | instance = null; 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/manager/ControlManager.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.manager; 2 | 3 | import com.vmloft.develop.app.screencast.VError; 4 | import com.vmloft.develop.app.screencast.callback.AVTransportCallback; 5 | import com.vmloft.develop.app.screencast.callback.ControlCallback; 6 | import com.vmloft.develop.app.screencast.callback.RenderingControlCallback; 7 | import com.vmloft.develop.app.screencast.entity.AVTransportInfo; 8 | import com.vmloft.develop.app.screencast.entity.ClingDevice; 9 | import com.vmloft.develop.app.screencast.entity.RemoteItem; 10 | import com.vmloft.develop.app.screencast.entity.RenderingControlInfo; 11 | import com.vmloft.develop.app.screencast.ui.event.ControlEvent; 12 | import com.vmloft.develop.app.screencast.utils.ClingUtil; 13 | import com.vmloft.develop.library.tools.utils.VMDate; 14 | import com.vmloft.develop.library.tools.utils.VMLog; 15 | 16 | import org.fourthline.cling.controlpoint.ControlPoint; 17 | import org.fourthline.cling.model.action.ActionInvocation; 18 | import org.fourthline.cling.model.message.UpnpResponse; 19 | import org.fourthline.cling.model.meta.Service; 20 | import org.fourthline.cling.model.types.UDAServiceType; 21 | import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; 22 | import org.fourthline.cling.support.avtransport.callback.GetPositionInfo; 23 | import org.fourthline.cling.support.avtransport.callback.GetTransportInfo; 24 | import org.fourthline.cling.support.avtransport.callback.Pause; 25 | import org.fourthline.cling.support.avtransport.callback.Play; 26 | import org.fourthline.cling.support.avtransport.callback.Seek; 27 | import org.fourthline.cling.support.avtransport.callback.SetAVTransportURI; 28 | import org.fourthline.cling.support.avtransport.callback.Stop; 29 | import org.fourthline.cling.support.contentdirectory.DIDLParser; 30 | import org.fourthline.cling.support.model.DIDLContent; 31 | import org.fourthline.cling.support.model.PositionInfo; 32 | import org.fourthline.cling.support.model.TransportInfo; 33 | import org.fourthline.cling.support.model.TransportState; 34 | import org.fourthline.cling.support.model.item.Item; 35 | import org.fourthline.cling.support.renderingcontrol.callback.GetVolume; 36 | import org.fourthline.cling.support.renderingcontrol.callback.SetMute; 37 | import org.fourthline.cling.support.renderingcontrol.callback.SetVolume; 38 | import org.greenrobot.eventbus.EventBus; 39 | 40 | /** 41 | * Created by lzan13 on 2018/3/10. 42 | * 控制点管理器 43 | */ 44 | public class ControlManager { 45 | 46 | // 视频传输服务 47 | public static final String AV_TRANSPORT = "AVTransport"; 48 | // DMR 设备的控制服务 49 | public static final String RENDERING_CONTROL = "RenderingControl"; 50 | 51 | 52 | private static ControlManager instance; 53 | private Service avtService; 54 | private Service rcService; 55 | private UnsignedIntegerFourBytes instanceId; 56 | 57 | private AVTransportCallback avtCallback; 58 | private RenderingControlCallback rcCallback; 59 | private boolean isScreenCast = false; 60 | private String absTimeStr; 61 | private long absTime; 62 | private String trackDurationStr; 63 | private long trackDuration; 64 | 65 | private CastState state = CastState.STOPED; 66 | private boolean isMute = false; 67 | 68 | private ControlManager() { 69 | avtService = findServiceFromDevice(AV_TRANSPORT); 70 | rcService = findServiceFromDevice(RENDERING_CONTROL); 71 | instanceId = new UnsignedIntegerFourBytes("0"); 72 | } 73 | 74 | public static ControlManager getInstance() { 75 | if (instance == null) { 76 | instance = new ControlManager(); 77 | } 78 | return instance; 79 | } 80 | 81 | /** 82 | * 开始新的投屏播放,需要先停止上一次的投屏 83 | * 84 | * @param item 需要投屏播放的本地资源对象 85 | */ 86 | public void newPlayCast(final Item item, final ControlCallback callback) { 87 | stopCast(new ControlCallback() { 88 | @Override 89 | public void onSuccess() { 90 | setAVTransportURI(item, new ControlCallback() { 91 | @Override 92 | public void onSuccess() { 93 | playCast(callback); 94 | } 95 | 96 | @Override 97 | public void onError(int code, String msg) { 98 | callback.onError(code, msg); 99 | } 100 | }); 101 | } 102 | 103 | @Override 104 | public void onError(int code, String msg) { 105 | callback.onError(code, msg); 106 | } 107 | }); 108 | } 109 | 110 | /** 111 | * 开始投屏,需要先停止上一个投屏 112 | * 113 | * @param item 需要投屏的远程网络资源对象 114 | */ 115 | public void newPlayCast(final RemoteItem item, final ControlCallback callback) { 116 | stopCast(new ControlCallback() { 117 | @Override 118 | public void onSuccess() { 119 | setAVTransportURI(item, new ControlCallback() { 120 | @Override 121 | public void onSuccess() { 122 | playCast(callback); 123 | } 124 | 125 | @Override 126 | public void onError(int code, String msg) { 127 | callback.onError(code, msg); 128 | } 129 | }); 130 | } 131 | 132 | @Override 133 | public void onError(int code, String msg) { 134 | callback.onError(code, msg); 135 | } 136 | }); 137 | } 138 | 139 | /** 140 | * 播放投屏 141 | */ 142 | public void playCast(final ControlCallback callback) { 143 | if (checkAVTService()) { 144 | callback.onError(VError.SERVICE_IS_NULL, "AVTService is null"); 145 | return; 146 | } 147 | ControlPoint controlPoint = ClingManager.getInstance().getControlPoint(); 148 | controlPoint.execute(new Play(instanceId, avtService) { 149 | @Override 150 | public void success(ActionInvocation invocation) { 151 | VMLog.i("Play success"); 152 | callback.onSuccess(); 153 | } 154 | 155 | @Override 156 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 157 | VMLog.e("Play error %s", msg); 158 | callback.onError(VError.UNKNOWN, msg); 159 | } 160 | }); 161 | } 162 | 163 | /** 164 | * 暂停投屏 165 | */ 166 | public void pauseCast(final ControlCallback callback) { 167 | if (checkAVTService()) { 168 | callback.onError(VError.SERVICE_IS_NULL, "AVTService is null"); 169 | return; 170 | } 171 | ControlPoint controlPoint = ClingManager.getInstance().getControlPoint(); 172 | controlPoint.execute(new Pause(instanceId, avtService) { 173 | @Override 174 | public void success(ActionInvocation invocation) { 175 | VMLog.i("Pause success"); 176 | callback.onSuccess(); 177 | } 178 | 179 | @Override 180 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 181 | VMLog.e("Pause error %s", msg); 182 | callback.onError(VError.UNKNOWN, msg); 183 | } 184 | }); 185 | } 186 | 187 | /** 188 | * 停止投屏 189 | */ 190 | public void stopCast(final ControlCallback callback) { 191 | if (checkAVTService()) { 192 | callback.onError(VError.SERVICE_IS_NULL, "AVTService is null"); 193 | return; 194 | } 195 | ControlPoint controlPoint = ClingManager.getInstance().getControlPoint(); 196 | controlPoint.execute(new Stop(instanceId, avtService) { 197 | @Override 198 | public void success(ActionInvocation invocation) { 199 | VMLog.i("Stop success"); 200 | callback.onSuccess(); 201 | } 202 | 203 | @Override 204 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 205 | VMLog.e("Stop error %s", msg); 206 | callback.onError(VError.UNKNOWN, msg); 207 | } 208 | }); 209 | } 210 | 211 | /** 212 | * 设置投屏进度 213 | * 214 | * @param target 目标进度 215 | */ 216 | public void seekCast(final String target, final ControlCallback callback) { 217 | if (checkAVTService()) { 218 | callback.onError(VError.SERVICE_IS_NULL, "AVTService is null"); 219 | return; 220 | } 221 | ControlPoint controlPoint = ClingManager.getInstance().getControlPoint(); 222 | controlPoint.execute(new Seek(instanceId, avtService, target) { 223 | @Override 224 | public void success(ActionInvocation invocation) { 225 | VMLog.d("Seek success - %s", target); 226 | callback.onSuccess(); 227 | } 228 | 229 | @Override 230 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 231 | VMLog.e("Seek error %s", msg); 232 | callback.onError(VError.UNKNOWN, msg); 233 | } 234 | }); 235 | } 236 | 237 | /** 238 | * -------------- RCService 相关操作 -------------- 239 | * 设置投屏音量 240 | */ 241 | public void setVolumeCast(int volume, final ControlCallback callback) { 242 | if (checkRCService()) { 243 | callback.onError(VError.SERVICE_IS_NULL, "RCService is null"); 244 | return; 245 | } 246 | ControlPoint controlPoint = ClingManager.getInstance().getControlPoint(); 247 | controlPoint.execute(new SetVolume(instanceId, rcService, volume) { 248 | @Override 249 | public void success(ActionInvocation invocation) { 250 | VMLog.d("setVolume success"); 251 | callback.onSuccess(); 252 | } 253 | 254 | @Override 255 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 256 | VMLog.e("setVolume error %s", msg); 257 | callback.onError(VError.UNKNOWN, msg); 258 | } 259 | }); 260 | } 261 | 262 | /** 263 | * 静音投屏 264 | */ 265 | public void muteCast(boolean mute, final ControlCallback callback) { 266 | if (checkRCService()) { 267 | callback.onError(VError.SERVICE_IS_NULL, "RCService is null"); 268 | return; 269 | } 270 | ControlPoint controlPoint = ClingManager.getInstance().getControlPoint(); 271 | controlPoint.execute(new SetMute(instanceId, rcService, mute) { 272 | @Override 273 | public void success(ActionInvocation invocation) { 274 | VMLog.d("Mute success"); 275 | callback.onSuccess(); 276 | } 277 | 278 | @Override 279 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 280 | VMLog.e("Mute error %s", msg); 281 | callback.onError(VError.UNKNOWN, msg); 282 | } 283 | }); 284 | } 285 | 286 | /** 287 | * 设置本地资源音视频传输 URI 288 | * 289 | * @param item 需要投屏的资源 290 | */ 291 | public void setAVTransportURI(Item item, final ControlCallback callback) { 292 | if (checkAVTService()) { 293 | callback.onError(VError.SERVICE_IS_NULL, "service is null"); 294 | return; 295 | } 296 | final String uri = item.getFirstResource().getValue(); 297 | DIDLContent content = new DIDLContent(); 298 | content.addItem(item); 299 | String metadata = ""; 300 | try { 301 | metadata = new DIDLParser().generate(content); 302 | } catch (Exception e) { 303 | e.printStackTrace(); 304 | } 305 | VMLog.d("metadata: %s", metadata); 306 | ControlPoint controlPoint = ClingManager.getInstance().getControlPoint(); 307 | controlPoint.execute(new SetAVTransportURI(instanceId, avtService, uri, metadata) { 308 | @Override 309 | public void success(ActionInvocation invocation) { 310 | VMLog.i("setAVTransportURI success %s", uri); 311 | callback.onSuccess(); 312 | } 313 | 314 | @Override 315 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 316 | VMLog.e("setAVTransportURI - error %s url:%s", msg, uri); 317 | callback.onError(VError.UNKNOWN, msg); 318 | } 319 | }); 320 | } 321 | 322 | /** 323 | * 设置远程资源音视频传输 URI 324 | */ 325 | public void setAVTransportURI(RemoteItem item, final ControlCallback callback) { 326 | if (checkAVTService()) { 327 | callback.onError(VError.SERVICE_IS_NULL, "service is null"); 328 | return; 329 | } 330 | String metadata = ClingUtil.getItemMetadata(item); 331 | VMLog.i("metadata: " + metadata); 332 | final String uri = item.getUrl(); 333 | ControlPoint controlPoint = ClingManager.getInstance().getControlPoint(); 334 | controlPoint.execute(new SetAVTransportURI(instanceId, avtService, item.getUrl(), metadata) { 335 | @Override 336 | public void success(ActionInvocation invocation) { 337 | VMLog.i("setAVTransportURI success url:%s", uri); 338 | callback.onSuccess(); 339 | } 340 | 341 | @Override 342 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 343 | VMLog.e("setAVTransportURI - error %s url:%s", msg, uri); 344 | callback.onError(VError.UNKNOWN, msg); 345 | } 346 | }); 347 | } 348 | 349 | /** 350 | * 初始化投屏相关回调 351 | */ 352 | public void initScreenCastCallback() { 353 | unInitScreenCastCallback(); 354 | isScreenCast = true; 355 | VMLog.d("initScreenCastCallback"); 356 | new Thread(new Runnable() { 357 | @Override 358 | public void run() { 359 | while (isScreenCast) { 360 | try { 361 | getPositionInfo(); 362 | getTransportInfo(); 363 | getVolume(); 364 | // 如果是暂停状态就睡眠5秒 365 | if (state == CastState.PAUSED) { 366 | Thread.sleep(2000); 367 | } else { 368 | Thread.sleep(1000); 369 | } 370 | } catch (InterruptedException e) { 371 | e.printStackTrace(); 372 | } 373 | } 374 | } 375 | }).start(); 376 | // 设置投屏传输相关回调 377 | avtCallback = new AVTransportCallback(avtService) { 378 | @Override 379 | protected void received(AVTransportInfo info) { 380 | ControlEvent event = new ControlEvent(); 381 | event.setAvtInfo(info); 382 | EventBus.getDefault().post(event); 383 | } 384 | }; 385 | ClingManager.getInstance().getControlPoint().execute(avtCallback); 386 | 387 | // 设置播放控制相关回调,这个其实在大部分设备上都无效 388 | rcCallback = new RenderingControlCallback(rcService) { 389 | @Override 390 | protected void received(RenderingControlInfo info) { 391 | VMLog.d("RenderingControlCallback received: mute:%b, volume:%d", info.isMute(), info 392 | .getVolume()); 393 | ControlEvent event = new ControlEvent(); 394 | event.setRcInfo(info); 395 | EventBus.getDefault().post(event); 396 | } 397 | }; 398 | ClingManager.getInstance().getControlPoint().execute(rcCallback); 399 | } 400 | 401 | /** 402 | * 取消初始化投屏相关回调 403 | */ 404 | public void unInitScreenCastCallback() { 405 | VMLog.d("unInitScreenCastCallback"); 406 | absTimeStr = "00:00:00"; 407 | absTime = 0; 408 | trackDurationStr = "00:00:00"; 409 | trackDuration = 0; 410 | 411 | isScreenCast = false; 412 | avtCallback = null; 413 | rcCallback = null; 414 | } 415 | 416 | /** 417 | * 获取投屏设备端播放进度信息 418 | */ 419 | public void getPositionInfo() { 420 | if (checkAVTService()) { 421 | return; 422 | } 423 | ControlPoint controlPoint = ClingManager.getInstance().getControlPoint(); 424 | controlPoint.execute(new GetPositionInfo(instanceId, avtService) { 425 | @Override 426 | public void received(ActionInvocation invocation, PositionInfo positionInfo) { 427 | if (positionInfo != null) { 428 | AVTransportInfo info = new AVTransportInfo(); 429 | info.setTimePosition(positionInfo.getAbsTime()); 430 | info.setMediaDuration(positionInfo.getTrackDuration()); 431 | ControlEvent event = new ControlEvent(); 432 | event.setAvtInfo(info); 433 | EventBus.getDefault().post(event); 434 | 435 | absTimeStr = positionInfo.getAbsTime(); 436 | absTime = VMDate.fromTimeString(absTimeStr); 437 | trackDurationStr = positionInfo.getTrackDuration(); 438 | trackDuration = VMDate.fromTimeString(trackDurationStr); 439 | if (absTimeStr.equals(trackDurationStr) && absTime != 0 && trackDuration != 0) { 440 | unInitScreenCastCallback(); 441 | } 442 | } 443 | VMLog.d("getPositionInfo success positionInfo:" + positionInfo.toString()); 444 | } 445 | 446 | @Override 447 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 448 | VMLog.e("getPositionInfo failed"); 449 | } 450 | }); 451 | } 452 | 453 | /** 454 | * 获取投屏设备播放端数据传输状态信息 455 | */ 456 | public void getTransportInfo() { 457 | if (checkAVTService()) { 458 | return; 459 | } 460 | ControlPoint controlPoint = ClingManager.getInstance().getControlPoint(); 461 | controlPoint.execute(new GetTransportInfo(instanceId, avtService) { 462 | 463 | @Override 464 | public void received(ActionInvocation invocation, TransportInfo transportInfo) { 465 | TransportState ts = transportInfo.getCurrentTransportState(); 466 | AVTransportInfo info = new AVTransportInfo(); 467 | if (TransportState.TRANSITIONING == ts) { 468 | info.setState(AVTransportInfo.TRANSITIONING); 469 | } else if (TransportState.PLAYING == ts) { 470 | info.setState(AVTransportInfo.PLAYING); 471 | } else if (TransportState.PAUSED_PLAYBACK == ts) { 472 | info.setState(AVTransportInfo.PAUSED_PLAYBACK); 473 | } else if (TransportState.STOPPED == ts) { 474 | info.setState(AVTransportInfo.STOPPED); 475 | if (absTime != 0 && trackDuration != 0) { 476 | unInitScreenCastCallback(); 477 | } 478 | } else { 479 | info.setState(AVTransportInfo.STOPPED); 480 | if (absTime != 0 && trackDuration != 0) { 481 | unInitScreenCastCallback(); 482 | } 483 | } 484 | ControlEvent event = new ControlEvent(); 485 | event.setAvtInfo(info); 486 | EventBus.getDefault().post(event); 487 | VMLog.d("getTransportInfo success transportInfo:" + ts.getValue()); 488 | } 489 | 490 | @Override 491 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 492 | VMLog.e("getTransportInfo failed"); 493 | } 494 | }); 495 | } 496 | 497 | /** 498 | * 获取投屏音量 499 | */ 500 | public void getVolume() { 501 | if (checkRCService()) { 502 | return; 503 | } 504 | ControlPoint controlPoint = ClingManager.getInstance().getControlPoint(); 505 | controlPoint.execute(new GetVolume(instanceId, rcService) { 506 | @Override 507 | public void received(ActionInvocation actionInvocation, int currentVolume) { 508 | RenderingControlInfo info = new RenderingControlInfo(); 509 | info.setVolume(currentVolume); 510 | info.setMute(false); 511 | ControlEvent event = new ControlEvent(); 512 | event.setRcInfo(info); 513 | EventBus.getDefault().post(event); 514 | VMLog.d("getVolume success volume:" + currentVolume); 515 | } 516 | 517 | @Override 518 | public void failure(ActionInvocation invocation, UpnpResponse operation, String msg) { 519 | VMLog.e("getVolume error %s", msg); 520 | } 521 | }); 522 | } 523 | 524 | /** 525 | * 检查视频传输服务是否存在 526 | */ 527 | private boolean checkAVTService() { 528 | if (avtService == null) { 529 | avtService = findServiceFromDevice(AV_TRANSPORT); 530 | } 531 | return avtService == null; 532 | } 533 | 534 | /** 535 | * 检查视频播放控制服务是否存在 536 | */ 537 | private boolean checkRCService() { 538 | if (rcService == null) { 539 | rcService = findServiceFromDevice(RENDERING_CONTROL); 540 | } 541 | return rcService == null; 542 | } 543 | 544 | /** 545 | * 通过指定服务类型,搜索当前选择的设备的服务 546 | * 547 | * @param type 需要的服务类型 548 | */ 549 | public Service findServiceFromDevice(String type) { 550 | UDAServiceType serviceType = new UDAServiceType(type); 551 | ClingDevice device = DeviceManager.getInstance().getCurrClingDevice(); 552 | if (device == null) { 553 | return null; 554 | } 555 | return device.getDevice().findService(serviceType); 556 | } 557 | 558 | public CastState getState() { 559 | return state; 560 | } 561 | 562 | public void setState(CastState state) { 563 | this.state = state; 564 | } 565 | 566 | public boolean isMute() { 567 | return isMute; 568 | } 569 | 570 | public void setMute(boolean mute) { 571 | isMute = mute; 572 | } 573 | 574 | /** 575 | * 销毁,释放资源 576 | */ 577 | public void destroy() { 578 | instance = null; 579 | avtService = null; 580 | rcService = null; 581 | } 582 | 583 | public enum CastState { 584 | TRANSITIONING, 585 | PLAYING, 586 | PAUSED, 587 | STOPED 588 | } 589 | 590 | } 591 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/manager/DeviceManager.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.manager; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import com.vmloft.develop.app.screencast.entity.ClingDevice; 6 | import com.vmloft.develop.app.screencast.ui.event.DeviceEvent; 7 | 8 | import org.fourthline.cling.model.meta.Device; 9 | import org.fourthline.cling.model.types.DeviceType; 10 | import org.fourthline.cling.model.types.UDADeviceType; 11 | import org.greenrobot.eventbus.EventBus; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * Created by lzan13 on 2018/3/9. 18 | * 设备管理器,保存当前包含设备列表,以及当前选中设备 19 | */ 20 | public class DeviceManager { 21 | // DMR 设备 类型 22 | public static final DeviceType DMR_DEVICE = new UDADeviceType("MediaRenderer"); 23 | 24 | private static DeviceManager instance; 25 | private List clingDeviceList; 26 | private ClingDevice currClingDevice; 27 | 28 | /** 29 | * 私有构造方法 30 | */ 31 | private DeviceManager() { 32 | if (clingDeviceList == null) { 33 | clingDeviceList = new ArrayList<>(); 34 | } 35 | clingDeviceList.clear(); 36 | } 37 | 38 | /** 39 | * 唯一获取单例对象实例方法 40 | */ 41 | public static DeviceManager getInstance() { 42 | if (instance == null) { 43 | instance = new DeviceManager(); 44 | } 45 | return instance; 46 | } 47 | 48 | /** 49 | * 获取当前 Cling 设备 50 | */ 51 | public ClingDevice getCurrClingDevice() { 52 | return currClingDevice; 53 | } 54 | 55 | /** 56 | * 设置当前 Cling 设备 57 | */ 58 | public void setCurrClingDevice(ClingDevice currClingDevice) { 59 | this.currClingDevice = currClingDevice; 60 | } 61 | 62 | /** 63 | * 添加设备到设备列表 64 | */ 65 | public void addDevice(@NonNull Device device) { 66 | if (device.getType().equals(DMR_DEVICE)) { 67 | ClingDevice clingDevice = new ClingDevice(device); 68 | clingDeviceList.add(clingDevice); 69 | EventBus.getDefault().post(new DeviceEvent()); 70 | } 71 | } 72 | 73 | /** 74 | * 从设备列表移除设备 75 | */ 76 | public void removeDevice(@NonNull Device device) { 77 | ClingDevice clingDevice = getClingDevice(device); 78 | if (clingDevice != null) { 79 | clingDeviceList.remove(clingDevice); 80 | } 81 | } 82 | 83 | /** 84 | * 获取设备 85 | */ 86 | public ClingDevice getClingDevice(@NonNull Device device) { 87 | for (ClingDevice tmpDevice : clingDeviceList) { 88 | if (device.equals(tmpDevice.getDevice())) { 89 | return tmpDevice; 90 | } 91 | } 92 | return null; 93 | } 94 | 95 | /** 96 | * 获取设备列表 97 | */ 98 | public List getClingDeviceList() { 99 | return clingDeviceList; 100 | } 101 | 102 | /** 103 | * 设置设备列表 104 | */ 105 | public void setClingDeviceList(List list) { 106 | clingDeviceList = list; 107 | } 108 | 109 | 110 | /** 111 | * 销毁 112 | */ 113 | public void destroy() { 114 | if (clingDeviceList != null) { 115 | clingDeviceList.clear(); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/service/ClingService.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.service; 2 | 3 | import android.content.Intent; 4 | import android.os.IBinder; 5 | 6 | import com.vmloft.develop.app.screencast.service.upnp.AndroidJettyServletContainer; 7 | import com.vmloft.develop.app.screencast.service.upnp.ClingContentDirectoryService; 8 | import com.vmloft.develop.library.tools.utils.VMNetwork; 9 | import com.vmloft.develop.library.tools.utils.VMLog; 10 | 11 | import org.fourthline.cling.UpnpServiceConfiguration; 12 | import org.fourthline.cling.android.AndroidUpnpServiceConfiguration; 13 | import org.fourthline.cling.android.AndroidUpnpServiceImpl; 14 | import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder; 15 | import org.fourthline.cling.controlpoint.ControlPoint; 16 | import org.fourthline.cling.model.DefaultServiceManager; 17 | import org.fourthline.cling.model.ValidationException; 18 | import org.fourthline.cling.model.meta.DeviceDetails; 19 | import org.fourthline.cling.model.meta.DeviceIdentity; 20 | import org.fourthline.cling.model.meta.LocalDevice; 21 | import org.fourthline.cling.model.meta.LocalService; 22 | import org.fourthline.cling.model.types.UDADeviceType; 23 | import org.fourthline.cling.model.types.UDN; 24 | import org.fourthline.cling.registry.Registry; 25 | import org.fourthline.cling.transport.impl.AsyncServletStreamServerConfigurationImpl; 26 | import org.fourthline.cling.transport.impl.AsyncServletStreamServerImpl; 27 | import org.fourthline.cling.transport.spi.NetworkAddressFactory; 28 | import org.fourthline.cling.transport.spi.StreamServer; 29 | 30 | import java.util.UUID; 31 | 32 | /** 33 | * Created by lzan13 on 2018/3/1. 34 | * Cling service,获取 UPnP 相关对象 35 | */ 36 | public class ClingService extends AndroidUpnpServiceImpl { 37 | 38 | private final String TAG = this.getClass().getSimpleName(); 39 | 40 | private LocalDevice localDevice = null; 41 | 42 | @Override 43 | public void onCreate() { 44 | super.onCreate(); 45 | binder = new LocalBinder(); 46 | initLocalDevice(); 47 | } 48 | 49 | @Override 50 | public void onDestroy() { 51 | super.onDestroy(); 52 | } 53 | 54 | @Override 55 | public IBinder onBind(Intent intent) { 56 | return binder; 57 | } 58 | 59 | 60 | private void initLocalDevice() { 61 | //Create LocalDevice 62 | LocalService localService = new AnnotationLocalServiceBinder().read( 63 | ClingContentDirectoryService.class); 64 | localService.setManager( 65 | new DefaultServiceManager<>(localService, ClingContentDirectoryService.class)); 66 | 67 | String macAddress = VMNetwork.getMacAddress(); 68 | //Generate UUID by MAC address 69 | UDN udn = UDN.valueOf(UUID.nameUUIDFromBytes(macAddress.getBytes()).toString()); 70 | 71 | UDADeviceType type = new UDADeviceType("MediaServer"); 72 | // DeviceDetails details = new DeviceDetails(localDeviceName); 73 | DeviceDetails details = new DeviceDetails("VAndroidMediaServer"); 74 | try { 75 | localDevice = new LocalDevice(new DeviceIdentity(udn), type, details, 76 | new LocalService[]{localService}); 77 | upnpService.getRegistry().addDevice(localDevice); 78 | } catch (ValidationException e) { 79 | e.printStackTrace(); 80 | } 81 | 82 | VMLog.d(TAG, "MediaServer device created! name:%s, manufacturer:%s, model:%s", 83 | details.getFriendlyName(), details.getManufacturerDetails().getManufacturer(), 84 | details.getModelDetails().getModelName()); 85 | } 86 | 87 | public LocalDevice getLocalDevice() { 88 | return localDevice; 89 | } 90 | 91 | /** 92 | * 获取控制点 93 | */ 94 | public ControlPoint getControlPoint() { 95 | return upnpService.getControlPoint(); 96 | } 97 | 98 | /** 99 | * 获取 UPnP 核心注册组件 100 | */ 101 | public Registry getRegistry() { 102 | return upnpService.getRegistry(); 103 | } 104 | 105 | @Override 106 | protected UpnpServiceConfiguration createConfiguration() { 107 | return new FixedAndroidUpnpServiceConfiguration(); 108 | } 109 | 110 | public UpnpServiceConfiguration getConfiguration() { 111 | return upnpService.getConfiguration(); 112 | } 113 | 114 | class FixedAndroidUpnpServiceConfiguration extends AndroidUpnpServiceConfiguration { 115 | @Override 116 | public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) { 117 | // Use Jetty, start/stop a new shared instance of JettyServletContainer 118 | return new AsyncServletStreamServerImpl(new AsyncServletStreamServerConfigurationImpl( 119 | AndroidJettyServletContainer.INSTANCE, 120 | networkAddressFactory.getStreamListenPort())); 121 | } 122 | } 123 | 124 | public class LocalBinder extends Binder { 125 | public ClingService getService() { 126 | return ClingService.this; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/service/SystemService.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.service; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.os.Binder; 6 | import android.os.IBinder; 7 | import android.support.annotation.Nullable; 8 | import android.util.Log; 9 | 10 | import com.vmloft.develop.app.screencast.VIntents; 11 | import com.vmloft.develop.app.screencast.manager.ControlManager; 12 | import com.vmloft.develop.app.screencast.service.upnp.JettyResourceServer; 13 | 14 | import org.fourthline.cling.controlpoint.ControlPoint; 15 | import org.fourthline.cling.controlpoint.SubscriptionCallback; 16 | import org.fourthline.cling.model.gena.CancelReason; 17 | import org.fourthline.cling.model.gena.GENASubscription; 18 | import org.fourthline.cling.model.message.UpnpResponse; 19 | import org.fourthline.cling.model.meta.Device; 20 | import org.fourthline.cling.model.state.StateVariableValue; 21 | import org.fourthline.cling.model.types.UDAServiceType; 22 | import org.fourthline.cling.support.avtransport.lastchange.AVTransportLastChangeParser; 23 | import org.fourthline.cling.support.avtransport.lastchange.AVTransportVariable; 24 | import org.fourthline.cling.support.contentdirectory.DIDLParser; 25 | import org.fourthline.cling.support.lastchange.EventedValueString; 26 | import org.fourthline.cling.support.lastchange.LastChange; 27 | import org.fourthline.cling.support.model.DIDLContent; 28 | import org.fourthline.cling.support.model.TransportState; 29 | import org.fourthline.cling.support.model.item.Item; 30 | 31 | import java.util.Map; 32 | import java.util.concurrent.ExecutorService; 33 | import java.util.concurrent.Executors; 34 | 35 | /** 36 | * Created by lzan13 on 2018/3/22. 37 | */ 38 | public class SystemService extends Service{ 39 | private static final String TAG = SystemService.class.getSimpleName(); 40 | 41 | private Binder binder = new LocalBinder(); 42 | private Device mSelectedDevice; 43 | private int mDeviceVolume; 44 | private AVTransportSubscriptionCallback mAVTransportSubscriptionCallback; 45 | 46 | //Jetty DMS Server 47 | private ExecutorService mThreadPool = Executors.newCachedThreadPool(); 48 | private JettyResourceServer mJettyResourceServer; 49 | 50 | @Override 51 | public void onCreate() { 52 | super.onCreate(); 53 | //Start Local Server 54 | mJettyResourceServer = new JettyResourceServer(); 55 | mThreadPool.execute(mJettyResourceServer); 56 | } 57 | 58 | @Override 59 | public void onDestroy() { 60 | //End all subscriptions 61 | if (mAVTransportSubscriptionCallback != null) 62 | mAVTransportSubscriptionCallback.end(); 63 | 64 | //Stop Jetty 65 | mJettyResourceServer.stopIfRunning(); 66 | 67 | super.onDestroy(); 68 | } 69 | 70 | @Override 71 | public IBinder onBind(Intent intent) { 72 | return binder; 73 | } 74 | 75 | public class LocalBinder extends Binder { 76 | public SystemService getService() { 77 | return SystemService.this; 78 | } 79 | } 80 | 81 | public Device getSelectedDevice() { 82 | return mSelectedDevice; 83 | } 84 | 85 | public void setSelectedDevice(Device selectedDevice, ControlPoint controlPoint) { 86 | if (selectedDevice == mSelectedDevice) return; 87 | 88 | Log.i(TAG, "Change selected device."); 89 | mSelectedDevice = selectedDevice; 90 | //End last device's subscriptions 91 | if (mAVTransportSubscriptionCallback != null) { 92 | mAVTransportSubscriptionCallback.end(); 93 | } 94 | UDAServiceType type = new UDAServiceType(ControlManager.AV_TRANSPORT); 95 | //Init Subscriptions 96 | mAVTransportSubscriptionCallback = new AVTransportSubscriptionCallback(mSelectedDevice.findService(type)); 97 | controlPoint.execute(mAVTransportSubscriptionCallback); 98 | 99 | Intent intent = new Intent(VIntents.ACTION_CHANGE_DEVICE); 100 | sendBroadcast(intent); 101 | } 102 | 103 | public int getDeviceVolume() { 104 | return mDeviceVolume; 105 | } 106 | 107 | public void setDeviceVolume(int currentVolume) { 108 | mDeviceVolume = currentVolume; 109 | } 110 | 111 | private class AVTransportSubscriptionCallback extends SubscriptionCallback { 112 | 113 | protected AVTransportSubscriptionCallback(org.fourthline.cling.model.meta.Service service) { 114 | super(service); 115 | } 116 | 117 | @Override 118 | protected void failed(GENASubscription subscription, UpnpResponse responseStatus, Exception exception, String defaultMsg) { 119 | Log.e(TAG, "AVTransportSubscriptionCallback failed."); 120 | } 121 | 122 | @Override 123 | protected void established(GENASubscription subscription) { 124 | } 125 | 126 | @Override 127 | protected void ended(GENASubscription subscription, CancelReason reason, UpnpResponse responseStatus) { 128 | Log.i(TAG, "AVTransportSubscriptionCallback ended."); 129 | } 130 | 131 | @Override 132 | protected void eventReceived(GENASubscription subscription) { 133 | Map values = subscription.getCurrentValues(); 134 | if (values != null && values.containsKey("LastChange")) { 135 | String lastChangeValue = values.get("LastChange").toString(); 136 | Log.i(TAG, "LastChange:" + lastChangeValue); 137 | LastChange lastChange; 138 | try { 139 | lastChange = new LastChange(new AVTransportLastChangeParser(), lastChangeValue); 140 | } catch (Exception e) { 141 | e.printStackTrace(); 142 | return; 143 | } 144 | 145 | //Parse TransportState value. 146 | AVTransportVariable.TransportState transportState = lastChange.getEventedValue(0, AVTransportVariable.TransportState.class); 147 | if (transportState != null) { 148 | TransportState ts = transportState.getValue(); 149 | if (ts == TransportState.PLAYING) { 150 | Intent intent = new Intent(VIntents.ACTION_PLAYING); 151 | sendBroadcast(intent); 152 | } else if (ts == TransportState.PAUSED_PLAYBACK) { 153 | Intent intent = new Intent(VIntents.ACTION_PAUSED_PLAYBACK); 154 | sendBroadcast(intent); 155 | } else if (ts == TransportState.STOPPED) { 156 | Intent intent = new Intent(VIntents.ACTION_STOPPED); 157 | sendBroadcast(intent); 158 | } 159 | } 160 | 161 | //Parse CurrentTrackMetaData value. 162 | EventedValueString currentTrackMetaData = lastChange.getEventedValue(0, AVTransportVariable.CurrentTrackMetaData.class); 163 | if (currentTrackMetaData != null && currentTrackMetaData.getValue() != null) { 164 | DIDLParser didlParser = new DIDLParser(); 165 | Intent lastChangeIntent; 166 | try { 167 | DIDLContent content = didlParser.parse(currentTrackMetaData.getValue()); 168 | Item item = content.getItems().get(0); 169 | String creator = item.getCreator(); 170 | String title = item.getTitle(); 171 | 172 | lastChangeIntent = new Intent(VIntents.ACTION_UPDATE_LAST_CHANGE); 173 | lastChangeIntent.putExtra("creator", creator); 174 | lastChangeIntent.putExtra("title", title); 175 | } catch (Exception e) { 176 | Log.e(TAG, "Parse CurrentTrackMetaData error."); 177 | lastChangeIntent = null; 178 | } 179 | 180 | if (lastChangeIntent != null) 181 | sendBroadcast(lastChangeIntent); 182 | } 183 | } 184 | } 185 | 186 | @Override 187 | protected void eventsMissed(GENASubscription subscription, int numberOfMissedEvents) { 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/service/upnp/AndroidJettyServletContainer.java: -------------------------------------------------------------------------------- 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.vmloft.develop.app.screencast.service.upnp; 17 | 18 | import org.eclipse.jetty.server.AbstractHttpConnection; 19 | import org.eclipse.jetty.server.Connector; 20 | import org.eclipse.jetty.server.Request; 21 | import org.eclipse.jetty.server.Server; 22 | import org.eclipse.jetty.server.bio.SocketConnector; 23 | import org.eclipse.jetty.servlet.ServletContextHandler; 24 | import org.eclipse.jetty.servlet.ServletHolder; 25 | import org.eclipse.jetty.util.thread.ExecutorThreadPool; 26 | import org.fourthline.cling.transport.spi.ServletContainerAdapter; 27 | 28 | import java.io.IOException; 29 | import java.net.Socket; 30 | import java.util.concurrent.ExecutorService; 31 | import java.util.logging.Level; 32 | import java.util.logging.Logger; 33 | 34 | import javax.servlet.Servlet; 35 | import javax.servlet.http.HttpServletRequest; 36 | 37 | 38 | public class AndroidJettyServletContainer implements ServletContainerAdapter { 39 | final private static Logger log = Logger.getLogger(AndroidJettyServletContainer.class.getName()); 40 | 41 | // Singleton 42 | public static final AndroidJettyServletContainer INSTANCE = new AndroidJettyServletContainer(); 43 | 44 | private AndroidJettyServletContainer() { 45 | resetServer(); 46 | } 47 | 48 | protected Server server; 49 | 50 | @Override 51 | synchronized public void setExecutorService(ExecutorService executorService) { 52 | if (INSTANCE.server.getThreadPool() == null) { 53 | INSTANCE.server.setThreadPool(new ExecutorThreadPool(executorService) { 54 | @Override 55 | protected void doStop() throws Exception { 56 | // Do nothing, don't shut down the Cling ExecutorService when Jetty stops! 57 | } 58 | }); 59 | } 60 | } 61 | 62 | @Override 63 | synchronized public int addConnector(String host, int port) throws IOException { 64 | SocketConnector connector = new SocketConnector(); 65 | connector.setHost(host); 66 | connector.setPort(port); 67 | 68 | // Open immediately so we can get the assigned local port 69 | connector.open(); 70 | 71 | // Only add if open() succeeded 72 | server.addConnector(connector); 73 | 74 | // stats the connector if the server is started (server starts all connectors when started) 75 | if (server.isStarted()) { 76 | try { 77 | connector.start(); 78 | } catch (Exception ex) { 79 | log.severe("Couldn't start connector: " + connector + " " + ex); 80 | throw new RuntimeException(ex); 81 | } 82 | } 83 | return connector.getLocalPort(); 84 | } 85 | 86 | synchronized public void removeConnector(String host, int port) { 87 | Connector[] connectors = server.getConnectors(); 88 | if (connectors == null) 89 | return; 90 | 91 | for (Connector connector : connectors) { 92 | //Fix getPort() 93 | if (connector.getHost().equals(host) && connector.getLocalPort() == port) { 94 | if (connector.isStarted() || connector.isStarting()) { 95 | try { 96 | connector.stop(); 97 | } catch (Exception ex) { 98 | log.severe("Couldn't stop connector: " + connector + " " + ex); 99 | throw new RuntimeException(ex); 100 | } 101 | } 102 | server.removeConnector(connector); 103 | if (connectors.length == 1) { 104 | log.info("No more connectors, stopping Jetty server"); 105 | stopIfRunning(); 106 | } 107 | break; 108 | } 109 | } 110 | } 111 | 112 | @Override 113 | synchronized public void registerServlet(String contextPath, Servlet servlet) { 114 | if (server.getHandler() != null) { 115 | return; 116 | } 117 | log.info("Registering UPnP servlet under context path: " + contextPath); 118 | ServletContextHandler servletHandler = 119 | new ServletContextHandler(ServletContextHandler.NO_SESSIONS); 120 | if (contextPath != null && contextPath.length() > 0) 121 | servletHandler.setContextPath(contextPath); 122 | ServletHolder s = new ServletHolder(servlet); 123 | servletHandler.addServlet(s, "/*"); 124 | server.setHandler(servletHandler); 125 | } 126 | 127 | @Override 128 | synchronized public void startIfNotRunning() { 129 | if (!server.isStarted() && !server.isStarting()) { 130 | log.info("Starting Jetty server... "); 131 | try { 132 | server.start(); 133 | } catch (Exception ex) { 134 | log.severe("Couldn't start Jetty server: " + ex); 135 | throw new RuntimeException(ex); 136 | } 137 | } 138 | } 139 | 140 | @Override 141 | synchronized public void stopIfRunning() { 142 | if (!server.isStopped() && !server.isStopping()) { 143 | log.info("Stopping Jetty server..."); 144 | try { 145 | server.stop(); 146 | } catch (Exception ex) { 147 | log.severe("Couldn't stop Jetty server: " + ex); 148 | throw new RuntimeException(ex); 149 | } finally { 150 | resetServer(); 151 | } 152 | } 153 | } 154 | 155 | protected void resetServer() { 156 | server = new Server(); // Has its own QueuedThreadPool 157 | server.setGracefulShutdown(1000); // Let's wait a second for ongoing transfers to complete 158 | } 159 | 160 | /** 161 | * Casts the request to a Jetty API and tries to write a space character to the output stream of the socket. 162 | *

163 | * This space character might confuse the HTTP client. The Cling transports for Jetty Client and 164 | * Apache HttpClient have been tested to work with space characters. Unfortunately, Sun JDK's 165 | * HttpURLConnection does not gracefully handle any garbage in the HTTP request! 166 | *

167 | */ 168 | public static boolean isConnectionOpen(HttpServletRequest request) { 169 | return isConnectionOpen(request, " ".getBytes()); 170 | } 171 | 172 | public static boolean isConnectionOpen(HttpServletRequest request, byte[] heartbeat) { 173 | Request jettyRequest = (Request) request; 174 | AbstractHttpConnection connection = jettyRequest.getConnection(); 175 | Socket socket = (Socket) connection.getEndPoint().getTransport(); 176 | if (log.isLoggable(Level.FINE)) 177 | log.fine("Checking if client connection is still open: " + socket.getRemoteSocketAddress()); 178 | try { 179 | socket.getOutputStream().write(heartbeat); 180 | socket.getOutputStream().flush(); 181 | return true; 182 | } catch (IOException ex) { 183 | if (log.isLoggable(Level.FINE)) 184 | log.fine("Client connection has been closed: " + socket.getRemoteSocketAddress()); 185 | return false; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/service/upnp/AudioResourceServlet.java: -------------------------------------------------------------------------------- 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.vmloft.develop.app.screencast.service.upnp; 17 | 18 | import android.content.ContentUris; 19 | import android.database.Cursor; 20 | import android.net.Uri; 21 | import android.provider.MediaStore; 22 | import android.util.Log; 23 | 24 | import com.vmloft.develop.app.screencast.VApplication; 25 | import com.vmloft.develop.library.tools.utils.VMFile; 26 | 27 | import org.eclipse.jetty.servlet.DefaultServlet; 28 | import org.eclipse.jetty.util.resource.FileResource; 29 | import org.eclipse.jetty.util.resource.Resource; 30 | 31 | import java.io.File; 32 | 33 | public class AudioResourceServlet extends DefaultServlet { 34 | 35 | @Override 36 | public Resource getResource(String pathInContext) { 37 | Resource resource = null; 38 | 39 | Log.i(AudioResourceServlet.class.getSimpleName(), "Path:" + pathInContext); 40 | try { 41 | String id = VMFile.parseResourceId(pathInContext); 42 | Log.i(AudioResourceServlet.class.getSimpleName(), "Id:" + id); 43 | 44 | Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, Long.parseLong(id)); 45 | Cursor cursor = VApplication.getContext() 46 | .getContentResolver() 47 | .query(uri, null, null, null, null); 48 | cursor.moveToFirst(); 49 | String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)); 50 | File file = new File(path); 51 | if (file.exists()) { 52 | resource = FileResource.newResource(file); 53 | } 54 | } catch (Exception e) { 55 | e.printStackTrace(); 56 | } 57 | 58 | return resource; 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/service/upnp/ClingContentDirectoryService.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.service.upnp; 2 | 3 | import com.vmloft.develop.app.screencast.VApplication; 4 | import com.vmloft.develop.app.screencast.database.MediaContentDao; 5 | import com.vmloft.develop.app.screencast.entity.VItem; 6 | 7 | import org.fourthline.cling.support.contentdirectory.AbstractContentDirectoryService; 8 | import org.fourthline.cling.support.contentdirectory.ContentDirectoryErrorCode; 9 | import org.fourthline.cling.support.contentdirectory.ContentDirectoryException; 10 | import org.fourthline.cling.support.contentdirectory.DIDLParser; 11 | import org.fourthline.cling.support.model.BrowseFlag; 12 | import org.fourthline.cling.support.model.BrowseResult; 13 | import org.fourthline.cling.support.model.DIDLContent; 14 | import org.fourthline.cling.support.model.SortCriterion; 15 | import org.fourthline.cling.support.model.container.Container; 16 | import org.fourthline.cling.support.model.item.Item; 17 | 18 | import java.util.List; 19 | 20 | /** 21 | * Created by lzan13 on 2018/3/18. 22 | * 本地设备内容目录服务,主要用来读取本地音频和视频文件 23 | */ 24 | public class ClingContentDirectoryService extends AbstractContentDirectoryService { 25 | 26 | @Override 27 | public BrowseResult browse(String objectID, BrowseFlag browseFlag, String filter, long firstResult, long maxResults, SortCriterion[] orderBy) throws ContentDirectoryException { 28 | Container resultBean = ContainerFactory.createContainer(objectID); 29 | DIDLContent content = new DIDLContent(); 30 | for (Container c : resultBean.getContainers()) { 31 | content.addContainer(c); 32 | } 33 | for (Item item : resultBean.getItems()) { 34 | content.addItem(item); 35 | } 36 | int count = resultBean.getChildCount(); 37 | String contentModel = ""; 38 | try { 39 | contentModel = new DIDLParser().generate(content); 40 | } catch (Exception e) { 41 | throw new ContentDirectoryException(ContentDirectoryErrorCode.CANNOT_PROCESS, e.toString()); 42 | } 43 | return new BrowseResult(contentModel, count, count); 44 | } 45 | 46 | static class ContainerFactory { 47 | static Container createContainer(String containerId) { 48 | Container result = new Container(); 49 | result.setChildCount(0); 50 | 51 | if (VItem.ROOT_ID.equals(containerId)) { 52 | // 定义音频资源 53 | Container audioContainer = new Container(); 54 | audioContainer.setId(VItem.AUDIO_ID); 55 | audioContainer.setParentID(VItem.ROOT_ID); 56 | audioContainer.setClazz(VItem.AUDIO_CLASS); 57 | audioContainer.setTitle("Audios"); 58 | 59 | result.addContainer(audioContainer); 60 | result.setChildCount(result.getChildCount() + 1); 61 | 62 | // 定义图片资源 63 | Container imageContainer = new Container(); 64 | imageContainer.setId(VItem.IMAGE_ID); 65 | imageContainer.setParentID(VItem.ROOT_ID); 66 | imageContainer.setClazz(VItem.IMAGE_CLASS); 67 | imageContainer.setTitle("Images"); 68 | 69 | result.addContainer(imageContainer); 70 | result.setChildCount(result.getChildCount() + 1); 71 | 72 | // 定义视频资源 73 | Container videoContainer = new Container(); 74 | videoContainer.setId(VItem.VIDEO_ID); 75 | videoContainer.setParentID(VItem.ROOT_ID); 76 | videoContainer.setClazz(VItem.VIDEO_CLASS); 77 | videoContainer.setTitle("Videos"); 78 | 79 | result.addContainer(videoContainer); 80 | result.setChildCount(result.getChildCount() + 1); 81 | } else if (VItem.AUDIO_ID.equals(containerId)) { 82 | MediaContentDao contentDao = new MediaContentDao(VApplication.getContext()); 83 | //Get audio items 84 | List items = contentDao.getAudioItems(); 85 | for (Item item : items) { 86 | result.addItem(item); 87 | result.setChildCount(result.getChildCount() + 1); 88 | } 89 | } else if (VItem.IMAGE_ID.equals(containerId)) { 90 | MediaContentDao contentDao = new MediaContentDao(VApplication.getContext()); 91 | //Get image items 92 | List items = contentDao.getImageItems(); 93 | for (Item item : items) { 94 | result.addItem(item); 95 | result.setChildCount(result.getChildCount() + 1); 96 | } 97 | } else if (VItem.VIDEO_ID.equals(containerId)) { 98 | MediaContentDao contentDao = new MediaContentDao(VApplication.getContext()); 99 | //Get video items 100 | List items = contentDao.getVideoItems(); 101 | for (Item item : items) { 102 | result.addItem(item); 103 | result.setChildCount(result.getChildCount() + 1); 104 | } 105 | } 106 | return result; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/service/upnp/ImageResourceServlet.java: -------------------------------------------------------------------------------- 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.vmloft.develop.app.screencast.service.upnp; 17 | 18 | import android.content.ContentUris; 19 | import android.database.Cursor; 20 | import android.net.Uri; 21 | import android.provider.MediaStore; 22 | import android.util.Log; 23 | 24 | import com.vmloft.develop.app.screencast.VApplication; 25 | import com.vmloft.develop.library.tools.utils.VMFile; 26 | 27 | import org.eclipse.jetty.servlet.DefaultServlet; 28 | import org.eclipse.jetty.util.resource.FileResource; 29 | import org.eclipse.jetty.util.resource.Resource; 30 | 31 | import java.io.File; 32 | 33 | public class ImageResourceServlet extends DefaultServlet { 34 | 35 | @Override 36 | public Resource getResource(String pathInContext) { 37 | Resource resource = null; 38 | 39 | Log.i(ImageResourceServlet.class.getSimpleName(), "Path:" + pathInContext); 40 | try { 41 | String id = VMFile.parseResourceId(pathInContext); 42 | Log.i(ImageResourceServlet.class.getSimpleName(), "Id:" + id); 43 | 44 | Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, Long.parseLong(id)); 45 | Cursor cursor = VApplication.getContext() 46 | .getContentResolver() 47 | .query(uri, null, null, null, null); 48 | cursor.moveToFirst(); 49 | String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)); 50 | File file = new File(path); 51 | if (file.exists()) { 52 | resource = FileResource.newResource(file); 53 | } 54 | } catch (Exception e) { 55 | e.printStackTrace(); 56 | } 57 | 58 | return resource; 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/service/upnp/JettyResourceServer.java: -------------------------------------------------------------------------------- 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.vmloft.develop.app.screencast.service.upnp; 17 | 18 | import com.vmloft.develop.app.screencast.VConstants; 19 | 20 | import org.eclipse.jetty.server.Server; 21 | import org.eclipse.jetty.servlet.ServletContextHandler; 22 | 23 | import java.util.logging.Logger; 24 | 25 | public class JettyResourceServer implements Runnable { 26 | final private static Logger log = Logger.getLogger(JettyResourceServer.class.getName()); 27 | 28 | private Server mServer; 29 | 30 | public JettyResourceServer() { 31 | mServer = new Server(VConstants.JETTY_SERVER_PORT); // Has its own QueuedThreadPool 32 | mServer.setGracefulShutdown(1000); // Let's wait a second for ongoing transfers to complete 33 | } 34 | 35 | synchronized public void startIfNotRunning() { 36 | if (!mServer.isStarted() && !mServer.isStarting()) { 37 | log.info("Starting JettyResourceServer"); 38 | try { 39 | mServer.start(); 40 | } catch (Exception ex) { 41 | log.severe("Couldn't start Jetty server: " + ex); 42 | throw new RuntimeException(ex); 43 | } 44 | } 45 | } 46 | 47 | synchronized public void stopIfRunning() { 48 | if (!mServer.isStopped() && !mServer.isStopping()) { 49 | log.info("Stopping JettyResourceServer"); 50 | try { 51 | mServer.stop(); 52 | } catch (Exception ex) { 53 | log.severe("Couldn't stop Jetty server: " + ex); 54 | throw new RuntimeException(ex); 55 | } 56 | } 57 | } 58 | 59 | public String getServerState() { 60 | return mServer.getState(); 61 | } 62 | 63 | @Override 64 | public void run() { 65 | ServletContextHandler context = new ServletContextHandler(); 66 | context.setContextPath("/"); 67 | context.setInitParameter("org.eclipse.jetty.servlet.Default.gzip", "false"); 68 | mServer.setHandler(context); 69 | 70 | context.addServlet(AudioResourceServlet.class, "/audio/*"); 71 | context.addServlet(ImageResourceServlet.class, "/image/*"); 72 | context.addServlet(VideoResourceServlet.class, "/video/*"); 73 | 74 | startIfNotRunning(); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/service/upnp/VideoResourceServlet.java: -------------------------------------------------------------------------------- 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.vmloft.develop.app.screencast.service.upnp; 17 | 18 | import android.content.ContentUris; 19 | import android.database.Cursor; 20 | import android.net.Uri; 21 | import android.provider.MediaStore; 22 | import android.util.Log; 23 | 24 | import com.vmloft.develop.app.screencast.VApplication; 25 | import com.vmloft.develop.library.tools.utils.VMFile; 26 | 27 | import org.eclipse.jetty.servlet.DefaultServlet; 28 | import org.eclipse.jetty.util.resource.FileResource; 29 | import org.eclipse.jetty.util.resource.Resource; 30 | 31 | import java.io.File; 32 | 33 | public class VideoResourceServlet extends DefaultServlet { 34 | 35 | @Override 36 | public Resource getResource(String pathInContext) { 37 | Resource resource = null; 38 | 39 | Log.i(VideoResourceServlet.class.getSimpleName(), "Path:" + pathInContext); 40 | try { 41 | String id = VMFile.parseResourceId(pathInContext); 42 | Log.i(VideoResourceServlet.class.getSimpleName(), "Id:" + id); 43 | 44 | Uri uri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, Long.parseLong(id)); 45 | Cursor cursor = VApplication.getContext() 46 | .getContentResolver() 47 | .query(uri, null, null, null, null); 48 | cursor.moveToFirst(); 49 | String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)); 50 | File file = new File(path); 51 | if (file.exists()) { 52 | resource = FileResource.newResource(file); 53 | } 54 | } catch (Exception e) { 55 | e.printStackTrace(); 56 | } 57 | 58 | return resource; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/DeviceListActivity.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.FragmentManager; 5 | import android.support.v4.app.FragmentTransaction; 6 | import android.support.v7.widget.Toolbar; 7 | import android.view.View; 8 | 9 | import com.vmloft.develop.app.screencast.R; 10 | import com.vmloft.develop.library.tools.VMActivity; 11 | 12 | import butterknife.BindView; 13 | import butterknife.ButterKnife; 14 | 15 | /** 16 | * Created by lzan13 on 2018/3/19. 17 | * 投屏设备列表 18 | */ 19 | public class DeviceListActivity extends VMActivity { 20 | 21 | @BindView(R.id.widget_toolbar) Toolbar toolbar; 22 | 23 | private DeviceListFragment deviceListFragment; 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | setContentView(R.layout.activity_device_list); 29 | 30 | ButterKnife.bind(activity); 31 | 32 | init(); 33 | } 34 | 35 | private void init() { 36 | 37 | toolbar.setTitle("选择投屏设备"); 38 | setSupportActionBar(toolbar); 39 | toolbar.setNavigationIcon(R.drawable.ic_arrow_back_24dp); 40 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 41 | @Override 42 | public void onClick(View v) { 43 | finish(); 44 | } 45 | }); 46 | 47 | deviceListFragment = new DeviceListFragment(); 48 | FragmentManager fragmentManager = getSupportFragmentManager(); 49 | FragmentTransaction fts = fragmentManager.beginTransaction(); 50 | fts.replace(R.id.fragment_container, deviceListFragment); 51 | fts.commit(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/DeviceListFragment.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui; 2 | 3 | 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.support.v7.widget.RecyclerView.LayoutManager; 7 | import android.widget.Toast; 8 | 9 | import com.vmloft.develop.app.screencast.R; 10 | import com.vmloft.develop.app.screencast.listener.ItemClickListener; 11 | import com.vmloft.develop.app.screencast.entity.ClingDevice; 12 | import com.vmloft.develop.app.screencast.manager.DeviceManager; 13 | import com.vmloft.develop.app.screencast.ui.adapter.ClingDeviceAdapter; 14 | import com.vmloft.develop.app.screencast.ui.event.DeviceEvent; 15 | import com.vmloft.develop.library.tools.VMFragment; 16 | 17 | import org.greenrobot.eventbus.EventBus; 18 | import org.greenrobot.eventbus.Subscribe; 19 | import org.greenrobot.eventbus.ThreadMode; 20 | 21 | import butterknife.BindView; 22 | import butterknife.ButterKnife; 23 | 24 | /** 25 | * Created by lzan13 on 2018/3/9. 26 | */ 27 | 28 | public class DeviceListFragment extends VMFragment { 29 | 30 | @BindView(R.id.recycler_view) RecyclerView recycleView; 31 | 32 | private ClingDeviceAdapter adapter; 33 | private LayoutManager layoutManager; 34 | 35 | @Override 36 | protected int initLayoutId() { 37 | return R.layout.framgnet_device_list; 38 | } 39 | 40 | @Override 41 | protected void initView() { 42 | ButterKnife.bind(this, getView()); 43 | 44 | layoutManager = new LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false); 45 | adapter = new ClingDeviceAdapter(activity); 46 | recycleView.setLayoutManager(layoutManager); 47 | recycleView.setAdapter(adapter); 48 | 49 | setItemClickListener(); 50 | } 51 | 52 | @Override 53 | protected void initData() { 54 | 55 | } 56 | 57 | private void setItemClickListener() { 58 | adapter.setItemClickListener(new ItemClickListener() { 59 | @Override 60 | public void onItemAction(int action, Object object) { 61 | ClingDevice device = (ClingDevice) object; 62 | DeviceManager.getInstance().setCurrClingDevice(device); 63 | Toast.makeText(activity, 64 | "选择了设备 " + device.getDevice().getDetails().getFriendlyName(), 65 | Toast.LENGTH_LONG).show(); 66 | refresh(); 67 | } 68 | }); 69 | } 70 | 71 | public void refresh() { 72 | if (adapter == null) { 73 | adapter = new ClingDeviceAdapter(activity); 74 | recycleView.setAdapter(adapter); 75 | } 76 | adapter.refresh(); 77 | } 78 | 79 | @Subscribe(threadMode = ThreadMode.MAIN) 80 | public void onEventBus(DeviceEvent event) { 81 | refresh(); 82 | } 83 | 84 | @Override 85 | public void onStart() { 86 | super.onStart(); 87 | EventBus.getDefault().register(this); 88 | } 89 | 90 | @Override 91 | public void onStop() { 92 | super.onStop(); 93 | EventBus.getDefault().unregister(this); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/LocalContentFragment.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui; 2 | 3 | 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.support.v7.widget.RecyclerView.LayoutManager; 9 | import android.widget.LinearLayout; 10 | 11 | import com.vmloft.develop.app.screencast.R; 12 | import com.vmloft.develop.app.screencast.listener.ItemClickListener; 13 | import com.vmloft.develop.app.screencast.manager.ClingManager; 14 | import com.vmloft.develop.app.screencast.ui.adapter.LocalContentAdapter; 15 | import com.vmloft.develop.app.screencast.ui.event.DIDLEvent; 16 | import com.vmloft.develop.library.tools.VMFragment; 17 | 18 | import org.fourthline.cling.support.model.DIDLObject; 19 | import org.fourthline.cling.support.model.container.Container; 20 | import org.fourthline.cling.support.model.item.Item; 21 | import org.greenrobot.eventbus.EventBus; 22 | import org.greenrobot.eventbus.Subscribe; 23 | import org.greenrobot.eventbus.ThreadMode; 24 | 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | import butterknife.BindView; 29 | import butterknife.ButterKnife; 30 | import butterknife.OnClick; 31 | 32 | /** 33 | * Created by lzan13 on 2018/3/19. 34 | * 本地内容界面 35 | */ 36 | public class LocalContentFragment extends VMFragment { 37 | 38 | @BindView(R.id.layout_parent_directory) LinearLayout parentDirectory; 39 | @BindView(R.id.recycler_view) RecyclerView recyclerView; 40 | 41 | private LayoutManager layoutManager; 42 | private LocalContentAdapter adapter; 43 | private List objectList = new ArrayList<>(); 44 | 45 | /** 46 | * 创建实例对象的工厂方法 47 | */ 48 | public static LocalContentFragment newInstance() { 49 | LocalContentFragment fragment = new LocalContentFragment(); 50 | Bundle args = new Bundle(); 51 | fragment.setArguments(args); 52 | return fragment; 53 | } 54 | 55 | 56 | @Override 57 | protected int initLayoutId() { 58 | return R.layout.fragment_local_content; 59 | } 60 | 61 | @Override 62 | protected void initView() { 63 | ButterKnife.bind(this, getView()); 64 | layoutManager = new LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false); 65 | adapter = new LocalContentAdapter(activity, objectList); 66 | recyclerView.setLayoutManager(layoutManager); 67 | recyclerView.setAdapter(adapter); 68 | setItemClickListener(); 69 | } 70 | 71 | private void setItemClickListener() { 72 | adapter.setItemClickListener(new ItemClickListener() { 73 | @Override 74 | public void onItemAction(int action, Object object) { 75 | if (object instanceof Container) { 76 | Container container = (Container) object; 77 | searchLocalContent(container.getId()); 78 | } else if (object instanceof Item) { 79 | Item item = (Item) object; 80 | ClingManager.getInstance().setLocalItem(item); 81 | startActivity(new Intent(activity, MediaPlayActivity.class)); 82 | } 83 | } 84 | }); 85 | } 86 | 87 | @Override 88 | protected void initData() { 89 | } 90 | 91 | @OnClick(R.id.layout_parent_directory) 92 | public void onClick() { 93 | searchLocalContent("0"); 94 | } 95 | 96 | 97 | private void searchLocalContent(String containerId) { 98 | ClingManager.getInstance().searchLocalContent(containerId); 99 | } 100 | 101 | @Subscribe(threadMode = ThreadMode.MAIN) 102 | public void onEventBus(DIDLEvent event) { 103 | objectList.clear(); 104 | if (event.content.getContainers().size() > 0) { 105 | objectList.addAll(event.content.getContainers()); 106 | } else if (event.content.getItems().size() > 0) { 107 | objectList.addAll(event.content.getItems()); 108 | } 109 | adapter.refresh(); 110 | } 111 | 112 | @Override 113 | public void onStart() { 114 | super.onStart(); 115 | EventBus.getDefault().register(this); 116 | } 117 | 118 | @Override 119 | public void onStop() { 120 | super.onStop(); 121 | EventBus.getDefault().unregister(this); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.design.widget.TabLayout; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v4.app.FragmentManager; 8 | import android.support.v4.app.FragmentPagerAdapter; 9 | import android.support.v4.view.ViewPager; 10 | import android.support.v7.widget.Toolbar; 11 | 12 | import com.vmloft.develop.app.screencast.R; 13 | import com.vmloft.develop.library.tools.VMActivity; 14 | 15 | import butterknife.BindView; 16 | import butterknife.ButterKnife; 17 | import butterknife.OnClick; 18 | 19 | public class MainActivity extends VMActivity { 20 | 21 | @BindView(R.id.widget_toolbar) Toolbar toolbar; 22 | @BindView(R.id.widget_tab_layout) TabLayout tabLayout; 23 | @BindView(R.id.view_pager) ViewPager viewPager; 24 | 25 | private LocalContentFragment localContentFragment; 26 | private RemoteContentFragment remoteContentFragment; 27 | 28 | private Fragment[] fragments; 29 | private int currTabIndex = 0; 30 | private String[] tabsTitle = new String[]{"本地资源", "远程资源"}; 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_main); 36 | 37 | ButterKnife.bind(activity); 38 | 39 | init(); 40 | initFragment(); 41 | } 42 | 43 | private void init() { 44 | setSupportActionBar(toolbar); 45 | toolbar.setTitle(R.string.app_name); 46 | } 47 | 48 | private void initFragment() { 49 | localContentFragment = LocalContentFragment.newInstance(); 50 | remoteContentFragment = RemoteContentFragment.newInstance(); 51 | 52 | fragments = new Fragment[]{localContentFragment, remoteContentFragment}; 53 | ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager()); 54 | viewPager.setAdapter(adapter); 55 | 56 | // 设置 ViewPager 缓存个数 57 | viewPager.setOffscreenPageLimit(1); 58 | viewPager.setCurrentItem(currTabIndex); 59 | // 添加 ViewPager 页面改变监听 60 | viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { 61 | @Override 62 | public void onPageScrolled(int position, float positionOffset, 63 | int positionOffsetPixels) { 64 | } 65 | 66 | @Override 67 | public void onPageSelected(int position) { 68 | } 69 | 70 | @Override 71 | public void onPageScrollStateChanged(int state) { 72 | 73 | } 74 | }); 75 | tabLayout.setupWithViewPager(viewPager); 76 | } 77 | 78 | @OnClick(R.id.fab_screen_cast) 79 | public void onClick() { 80 | startActivity(new Intent(activity, DeviceListActivity.class)); 81 | } 82 | 83 | @Override 84 | protected void onDestroy() { 85 | super.onDestroy(); 86 | 87 | // ClingManager.getInstance().destroy(); 88 | // ControlManager.getInstance().destroy(); 89 | // DeviceManager.getInstance().destroy(); 90 | } 91 | 92 | 93 | /** 94 | * 自定义 ViewPager 适配器子类 95 | */ 96 | class ViewPagerAdapter extends FragmentPagerAdapter { 97 | 98 | public ViewPagerAdapter(FragmentManager fm) { 99 | super(fm); 100 | } 101 | 102 | @Override 103 | public CharSequence getPageTitle(int position) { 104 | return tabsTitle[position]; 105 | } 106 | 107 | @Override 108 | public Fragment getItem(int position) { 109 | return fragments[position]; 110 | } 111 | 112 | @Override 113 | public int getCount() { 114 | return tabsTitle.length; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/MediaPlayActivity.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.widget.Toolbar; 6 | import android.text.TextUtils; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.view.View; 10 | import android.widget.ImageView; 11 | import android.widget.SeekBar; 12 | import android.widget.TextView; 13 | import android.widget.Toast; 14 | 15 | import com.vmloft.develop.app.screencast.R; 16 | import com.vmloft.develop.app.screencast.callback.ControlCallback; 17 | import com.vmloft.develop.app.screencast.entity.AVTransportInfo; 18 | import com.vmloft.develop.app.screencast.entity.RemoteItem; 19 | import com.vmloft.develop.app.screencast.entity.RenderingControlInfo; 20 | import com.vmloft.develop.app.screencast.manager.ClingManager; 21 | import com.vmloft.develop.app.screencast.manager.ControlManager; 22 | import com.vmloft.develop.app.screencast.ui.event.ControlEvent; 23 | import com.vmloft.develop.library.tools.VMActivity; 24 | import com.vmloft.develop.library.tools.utils.VMDate; 25 | import com.vmloft.develop.library.tools.utils.VMLog; 26 | 27 | import org.fourthline.cling.support.model.item.Item; 28 | import org.greenrobot.eventbus.EventBus; 29 | import org.greenrobot.eventbus.Subscribe; 30 | import org.greenrobot.eventbus.ThreadMode; 31 | 32 | import butterknife.BindView; 33 | import butterknife.ButterKnife; 34 | import butterknife.OnClick; 35 | 36 | /** 37 | * Created by lzan13 on 2018/3/19. 38 | * 播放控制界面 39 | */ 40 | public class MediaPlayActivity extends VMActivity { 41 | @BindView(R.id.widget_toolbar) Toolbar toolbar; 42 | 43 | @BindView(R.id.text_content_title) TextView contentTitleView; 44 | @BindView(R.id.text_content_url) TextView contentUrlView; 45 | 46 | @BindView(R.id.img_volume) ImageView volumeView; 47 | @BindView(R.id.seek_bar_volume) SeekBar volumeSeekbar; 48 | @BindView(R.id.seek_bar_progress) SeekBar progressSeekbar; 49 | @BindView(R.id.text_play_time) TextView playTimeView; 50 | @BindView(R.id.text_play_max_time) TextView playMaxTimeView; 51 | @BindView(R.id.img_stop) ImageView stopView; 52 | @BindView(R.id.img_previous) ImageView previousView; 53 | @BindView(R.id.img_play) ImageView playView; 54 | @BindView(R.id.img_next) ImageView nextView; 55 | 56 | public Item localItem; 57 | public RemoteItem remoteItem; 58 | 59 | private int defaultVolume = 10; 60 | private int currVolume = defaultVolume; 61 | private boolean isMute = false; 62 | private int currProgress = 0; 63 | 64 | 65 | @Override 66 | protected void onCreate(Bundle savedInstanceState) { 67 | super.onCreate(savedInstanceState); 68 | setContentView(R.layout.activity_media_play); 69 | 70 | ButterKnife.bind(activity); 71 | 72 | init(); 73 | } 74 | 75 | private void init() { 76 | String title = getString(R.string.title_cast_control); 77 | toolbar.setTitle(title); 78 | setSupportActionBar(toolbar); 79 | toolbar.setNavigationIcon(R.drawable.ic_arrow_back_24dp); 80 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 81 | @Override 82 | public void onClick(View v) { 83 | finish(); 84 | } 85 | }); 86 | 87 | localItem = ClingManager.getInstance().getLocalItem(); 88 | remoteItem = ClingManager.getInstance().getRemoteItem(); 89 | String url = ""; 90 | String duration = ""; 91 | if (localItem != null) { 92 | title = localItem.getTitle(); 93 | url = localItem.getFirstResource().getValue(); 94 | duration = localItem.getFirstResource().getDuration(); 95 | } 96 | if (remoteItem != null) { 97 | title = remoteItem.getTitle(); 98 | url = remoteItem.getUrl(); 99 | duration = remoteItem.getDuration(); 100 | } 101 | toolbar.setTitle(title); 102 | contentTitleView.setText(title); 103 | contentUrlView.setText(url); 104 | 105 | if (!TextUtils.isEmpty(duration)) { 106 | playMaxTimeView.setText(duration); 107 | progressSeekbar.setMax((int) VMDate.fromTimeString(duration)); 108 | } 109 | 110 | setVolumeSeekListener(); 111 | setProgressSeekListener(); 112 | } 113 | 114 | /** 115 | * 设置音量拖动监听 116 | */ 117 | private void setVolumeSeekListener() { 118 | volumeSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 119 | @Override 120 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 121 | VMLog.d("Volume seek position: %d", progress); 122 | } 123 | 124 | @Override 125 | public void onStartTrackingTouch(SeekBar seekBar) { 126 | 127 | } 128 | 129 | @Override 130 | public void onStopTrackingTouch(SeekBar seekBar) { 131 | setVolume(seekBar.getProgress()); 132 | } 133 | }); 134 | } 135 | 136 | /** 137 | * 设置播放进度拖动监听 138 | */ 139 | private void setProgressSeekListener() { 140 | progressSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 141 | @Override 142 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 143 | } 144 | 145 | @Override 146 | public void onStartTrackingTouch(SeekBar seekBar) { 147 | 148 | } 149 | 150 | @Override 151 | public void onStopTrackingTouch(SeekBar seekBar) { 152 | currProgress = seekBar.getProgress(); 153 | playTimeView.setText(VMDate.toTimeString(currProgress)); 154 | seekCast(currProgress); 155 | } 156 | }); 157 | } 158 | 159 | @OnClick({R.id.img_volume, R.id.img_stop, R.id.img_previous, R.id.img_play, R.id.img_next}) 160 | public void onClick(View view) { 161 | switch (view.getId()) { 162 | case R.id.img_volume: 163 | mute(); 164 | break; 165 | case R.id.img_stop: 166 | stop(); 167 | break; 168 | case R.id.img_previous: 169 | break; 170 | case R.id.img_play: 171 | play(); 172 | break; 173 | case R.id.img_next: 174 | 175 | break; 176 | } 177 | } 178 | 179 | /** 180 | * 静音开关 181 | */ 182 | private void mute() { 183 | // 先获取当前是否静音 184 | isMute = ControlManager.getInstance().isMute(); 185 | ControlManager.getInstance().muteCast(!isMute, new ControlCallback() { 186 | @Override 187 | public void onSuccess() { 188 | ControlManager.getInstance().setMute(!isMute); 189 | if (isMute) { 190 | if (currVolume == 0) { 191 | currVolume = defaultVolume; 192 | } 193 | setVolume(currVolume); 194 | } 195 | // 这里是根据之前的状态判断的 196 | if (isMute) { 197 | volumeView.setImageResource(R.drawable.ic_volume_up_24dp); 198 | } else { 199 | volumeView.setImageResource(R.drawable.ic_volume_off_24dp); 200 | } 201 | } 202 | 203 | @Override 204 | public void onError(int code, String msg) { 205 | showToast(String.format("Mute cast failed %s", msg)); 206 | } 207 | }); 208 | } 209 | 210 | /** 211 | * 设置音量大小 212 | */ 213 | private void setVolume(int volume) { 214 | currVolume = volume; 215 | ControlManager.getInstance().setVolumeCast(volume, new ControlCallback() { 216 | @Override 217 | public void onSuccess() { 218 | 219 | } 220 | 221 | @Override 222 | public void onError(int code, String msg) { 223 | showToast(String.format("Set cast volume failed %s", msg)); 224 | } 225 | }); 226 | } 227 | 228 | 229 | /** 230 | * 播放开关 231 | */ 232 | private void play() { 233 | if (ControlManager.getInstance().getState() == ControlManager.CastState.STOPED) { 234 | if (localItem != null) { 235 | newPlayCastLocalContent(); 236 | } else { 237 | newPlayCastRemoteContent(); 238 | } 239 | } else if (ControlManager.getInstance().getState() == ControlManager.CastState.PAUSED) { 240 | playCast(); 241 | } else if (ControlManager.getInstance().getState() == ControlManager.CastState.PLAYING) { 242 | pauseCast(); 243 | } else { 244 | Toast.makeText(activity, "正在连接设备,稍后操作", Toast.LENGTH_SHORT).show(); 245 | } 246 | } 247 | 248 | private void stop() { 249 | ControlManager.getInstance().unInitScreenCastCallback(); 250 | stopCast(); 251 | } 252 | 253 | private void newPlayCastLocalContent() { 254 | ControlManager.getInstance().setState(ControlManager.CastState.TRANSITIONING); 255 | ControlManager.getInstance().newPlayCast(localItem, new ControlCallback() { 256 | @Override 257 | public void onSuccess() { 258 | ControlManager.getInstance().setState(ControlManager.CastState.PLAYING); 259 | ControlManager.getInstance().initScreenCastCallback(); 260 | runOnUiThread(new Runnable() { 261 | @Override 262 | public void run() { 263 | playView.setImageResource(R.drawable.ic_pause_circle_outline_24dp); 264 | } 265 | }); 266 | } 267 | 268 | @Override 269 | public void onError(int code, String msg) { 270 | ControlManager.getInstance().setState(ControlManager.CastState.STOPED); 271 | showToast(String.format("New play cast local content failed %s", msg)); 272 | } 273 | }); 274 | } 275 | 276 | private void newPlayCastRemoteContent() { 277 | ControlManager.getInstance().setState(ControlManager.CastState.TRANSITIONING); 278 | ControlManager.getInstance().newPlayCast(remoteItem, new ControlCallback() { 279 | @Override 280 | public void onSuccess() { 281 | ControlManager.getInstance().setState(ControlManager.CastState.PLAYING); 282 | ControlManager.getInstance().initScreenCastCallback(); 283 | runOnUiThread(new Runnable() { 284 | @Override 285 | public void run() { 286 | playView.setImageResource(R.drawable.ic_pause_circle_outline_24dp); 287 | } 288 | }); 289 | } 290 | 291 | @Override 292 | public void onError(int code, String msg) { 293 | ControlManager.getInstance().setState(ControlManager.CastState.STOPED); 294 | showToast(String.format("New play cast remote content failed %s", msg)); 295 | } 296 | }); 297 | } 298 | 299 | private void playCast() { 300 | ControlManager.getInstance().playCast(new ControlCallback() { 301 | @Override 302 | public void onSuccess() { 303 | ControlManager.getInstance().setState(ControlManager.CastState.PLAYING); 304 | runOnUiThread(new Runnable() { 305 | @Override 306 | public void run() { 307 | playView.setImageResource(R.drawable.ic_pause_circle_outline_24dp); 308 | } 309 | }); 310 | } 311 | 312 | @Override 313 | public void onError(int code, String msg) { 314 | showToast(String.format("Play cast failed %s", msg)); 315 | } 316 | }); 317 | } 318 | 319 | private void pauseCast() { 320 | ControlManager.getInstance().pauseCast(new ControlCallback() { 321 | @Override 322 | public void onSuccess() { 323 | ControlManager.getInstance().setState(ControlManager.CastState.PAUSED); 324 | runOnUiThread(new Runnable() { 325 | @Override 326 | public void run() { 327 | playView.setImageResource(R.drawable.ic_play_circle_outline_24dp); 328 | } 329 | }); 330 | } 331 | 332 | @Override 333 | public void onError(int code, String msg) { 334 | showToast(String.format("Pause cast failed %s", msg)); 335 | } 336 | }); 337 | } 338 | 339 | private void stopCast() { 340 | ControlManager.getInstance().stopCast(new ControlCallback() { 341 | @Override 342 | public void onSuccess() { 343 | ControlManager.getInstance().setState(ControlManager.CastState.STOPED); 344 | runOnUiThread(new Runnable() { 345 | @Override 346 | public void run() { 347 | playView.setImageResource(R.drawable.ic_play_circle_outline_24dp); 348 | onFinish(); 349 | } 350 | }); 351 | } 352 | 353 | @Override 354 | public void onError(int code, String msg) { 355 | showToast(String.format("Stop cast failed %s", msg)); 356 | } 357 | }); 358 | } 359 | 360 | /** 361 | * 改变投屏进度 362 | */ 363 | private void seekCast(int progress) { 364 | String target = VMDate.toTimeString(progress); 365 | ControlManager.getInstance().seekCast(target, new ControlCallback() { 366 | @Override 367 | public void onSuccess() { 368 | 369 | } 370 | 371 | @Override 372 | public void onError(int code, String msg) { 373 | showToast(String.format("Seek cast failed %s", msg)); 374 | } 375 | }); 376 | } 377 | 378 | @Subscribe(threadMode = ThreadMode.MAIN) 379 | public void onEventBus(ControlEvent event) { 380 | AVTransportInfo avtInfo = event.getAvtInfo(); 381 | if (avtInfo != null) { 382 | if (!TextUtils.isEmpty(avtInfo.getState())) { 383 | if (avtInfo.getState().equals("TRANSITIONING")) { 384 | ControlManager.getInstance().setState(ControlManager.CastState.TRANSITIONING); 385 | } else if (avtInfo.getState().equals("PLAYING")) { 386 | ControlManager.getInstance().setState(ControlManager.CastState.PLAYING); 387 | playView.setImageResource(R.drawable.ic_pause_circle_outline_24dp); 388 | } else if (avtInfo.getState().equals("PAUSED_PLAYBACK")) { 389 | ControlManager.getInstance().setState(ControlManager.CastState.PAUSED); 390 | playView.setImageResource(R.drawable.ic_play_circle_outline_24dp); 391 | } else if (avtInfo.getState().equals("STOPPED")) { 392 | ControlManager.getInstance().setState(ControlManager.CastState.STOPED); 393 | playView.setImageResource(R.drawable.ic_play_circle_outline_24dp); 394 | onFinish(); 395 | } else { 396 | ControlManager.getInstance().setState(ControlManager.CastState.STOPED); 397 | playView.setImageResource(R.drawable.ic_play_circle_outline_24dp); 398 | onFinish(); 399 | } 400 | } 401 | if (!TextUtils.isEmpty(avtInfo.getMediaDuration())) { 402 | playMaxTimeView.setText(avtInfo.getMediaDuration()); 403 | } 404 | if (!TextUtils.isEmpty(avtInfo.getTimePosition())) { 405 | long progress = VMDate.fromTimeString(avtInfo.getTimePosition()); 406 | progressSeekbar.setProgress((int) progress); 407 | playTimeView.setText(avtInfo.getTimePosition()); 408 | } 409 | } 410 | 411 | RenderingControlInfo rcInfo = event.getRcInfo(); 412 | if (rcInfo != null && ControlManager.getInstance() 413 | .getState() != ControlManager.CastState.STOPED) { 414 | if (rcInfo.isMute() || rcInfo.getVolume() == 0) { 415 | volumeView.setImageResource(R.drawable.ic_volume_off_24dp); 416 | ControlManager.getInstance().setMute(true); 417 | } else { 418 | volumeView.setImageResource(R.drawable.ic_volume_up_24dp); 419 | ControlManager.getInstance().setMute(false); 420 | } 421 | volumeSeekbar.setProgress(rcInfo.getVolume()); 422 | } 423 | } 424 | 425 | private void showToast(final String msg) { 426 | runOnUiThread(new Runnable() { 427 | @Override 428 | public void run() { 429 | Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show(); 430 | } 431 | }); 432 | } 433 | 434 | @Override 435 | public boolean onCreateOptionsMenu(Menu menu) { 436 | getMenuInflater().inflate(R.menu.play_menu, menu); 437 | return true; 438 | } 439 | 440 | @Override 441 | public boolean onOptionsItemSelected(MenuItem item) { 442 | switch (item.getItemId()) { 443 | case R.id.screen_cast: 444 | startActivity(new Intent(activity, DeviceListActivity.class)); 445 | break; 446 | default: 447 | return super.onOptionsItemSelected(item); 448 | } 449 | return true; 450 | } 451 | 452 | 453 | @Override 454 | public void onStart() { 455 | super.onStart(); 456 | EventBus.getDefault().register(this); 457 | } 458 | 459 | @Override 460 | public void onStop() { 461 | super.onStop(); 462 | EventBus.getDefault().unregister(this); 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/RemoteContentFragment.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui; 2 | 3 | 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.support.v7.widget.RecyclerView.LayoutManager; 9 | 10 | import com.vmloft.develop.app.screencast.R; 11 | import com.vmloft.develop.app.screencast.listener.ItemClickListener; 12 | import com.vmloft.develop.app.screencast.entity.RemoteItem; 13 | import com.vmloft.develop.app.screencast.manager.ClingManager; 14 | import com.vmloft.develop.app.screencast.ui.adapter.RemoteContentAdapter; 15 | import com.vmloft.develop.library.tools.VMFragment; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | import butterknife.BindView; 21 | import butterknife.ButterKnife; 22 | 23 | /** 24 | * Created by lzan13 on 2018/3/19. 25 | * 远程内容界面 26 | */ 27 | public class RemoteContentFragment extends VMFragment { 28 | @BindView(R.id.recycler_view) RecyclerView recyclerView; 29 | 30 | private LayoutManager layoutManager; 31 | private RemoteContentAdapter adapter; 32 | 33 | private List contentList = new ArrayList<>(); 34 | 35 | /** 36 | * 创建实例对象的工厂方法 37 | * 38 | * @return 返回一个新的实例 39 | */ 40 | public static RemoteContentFragment newInstance() { 41 | RemoteContentFragment fragment = new RemoteContentFragment(); 42 | Bundle args = new Bundle(); 43 | fragment.setArguments(args); 44 | return fragment; 45 | } 46 | 47 | 48 | @Override 49 | protected int initLayoutId() { 50 | return R.layout.fragment_remote_content; 51 | } 52 | 53 | @Override 54 | protected void initView() { 55 | ButterKnife.bind(this, getView()); 56 | layoutManager = new LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false); 57 | adapter = new RemoteContentAdapter(activity, contentList); 58 | recyclerView.setLayoutManager(layoutManager); 59 | recyclerView.setAdapter(adapter); 60 | setItemClickListener(); 61 | } 62 | 63 | private void setItemClickListener() { 64 | adapter.setItemClickListener(new ItemClickListener() { 65 | @Override 66 | public void onItemAction(int action, Object object) { 67 | ClingManager.getInstance().setRemoteItem((RemoteItem) object); 68 | startActivity(new Intent(activity, MediaPlayActivity.class)); 69 | } 70 | }); 71 | } 72 | 73 | @Override 74 | protected void initData() { 75 | RemoteItem pfzlItem = new RemoteItem("平凡之路 - 朴树", "p1302", "朴树", 25203597, "00:05:05", 76 | "640x352", "http://melove.net/data/videos/mv/PingFanZhiLu.mp4"); 77 | RemoteItem sugarItem = new RemoteItem("Sugar - 魔力红", "1452282796", "魔力红", 24410338, 78 | "00:05:01", "640x352", "http://melove.net/data/videos/mv/Sugar.mp4"); 79 | RemoteItem wakeItem = new RemoteItem("Wake - Y&F", "425703", "Hillsong Young And Free", 80 | 107362668, "00:04:33", "1280x720", "http://melove.net/data/videos/mv/Wake.mp4"); 81 | contentList.add(pfzlItem); 82 | contentList.add(sugarItem); 83 | contentList.add(wakeItem); 84 | 85 | refresh(); 86 | } 87 | 88 | public void refresh() { 89 | adapter.refresh(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/adapter/ClingDeviceAdapter.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | 12 | import com.vmloft.develop.app.screencast.R; 13 | import com.vmloft.develop.app.screencast.listener.ItemClickListener; 14 | import com.vmloft.develop.app.screencast.entity.ClingDevice; 15 | import com.vmloft.develop.app.screencast.manager.DeviceManager; 16 | 17 | import java.util.List; 18 | 19 | import butterknife.BindView; 20 | import butterknife.ButterKnife; 21 | 22 | /** 23 | * Created by lzan13 on 2018/3/9. 24 | * Cling 设备列表适配器 25 | */ 26 | public class ClingDeviceAdapter extends RecyclerView.Adapter { 27 | 28 | private LayoutInflater layoutInflater; 29 | private List clingDevices; 30 | private ItemClickListener clickListener; 31 | 32 | public ClingDeviceAdapter(Context context) { 33 | super(); 34 | layoutInflater = LayoutInflater.from(context); 35 | clingDevices = DeviceManager.getInstance().getClingDeviceList(); 36 | } 37 | 38 | @NonNull 39 | @Override 40 | public ClingHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 41 | View view = layoutInflater.inflate(R.layout.item_common_layout, parent, false); 42 | return new ClingHolder(view); 43 | } 44 | 45 | @Override 46 | public void onBindViewHolder(@NonNull ClingHolder holder, final int position) { 47 | final ClingDevice device = clingDevices.get(position); 48 | if (device == DeviceManager.getInstance().getCurrClingDevice()) { 49 | holder.iconView.setVisibility(View.VISIBLE); 50 | } else { 51 | holder.iconView.setVisibility(View.INVISIBLE); 52 | } 53 | holder.nameView.setText(device.getDevice().getDetails().getFriendlyName()); 54 | holder.itemView.setOnClickListener(new View.OnClickListener() { 55 | @Override 56 | public void onClick(View v) { 57 | if (clickListener != null) { 58 | clickListener.onItemAction(position, device); 59 | } 60 | } 61 | }); 62 | } 63 | 64 | @Override 65 | public int getItemCount() { 66 | return clingDevices.size(); 67 | } 68 | 69 | public void refresh() { 70 | notifyDataSetChanged(); 71 | } 72 | 73 | public void setItemClickListener(ItemClickListener listener) { 74 | this.clickListener = listener; 75 | } 76 | 77 | static class ClingHolder extends RecyclerView.ViewHolder { 78 | @BindView(R.id.text_name) TextView nameView; 79 | @BindView(R.id.img_icon) ImageView iconView; 80 | 81 | public ClingHolder(View itemView) { 82 | super(itemView); 83 | ButterKnife.bind(this, itemView); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/adapter/LocalContentAdapter.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | 12 | import com.vmloft.develop.app.screencast.R; 13 | import com.vmloft.develop.app.screencast.listener.ItemClickListener; 14 | 15 | import org.fourthline.cling.support.model.DIDLObject; 16 | import org.fourthline.cling.support.model.container.Container; 17 | import org.fourthline.cling.support.model.item.Item; 18 | 19 | import java.util.List; 20 | 21 | import butterknife.BindView; 22 | import butterknife.ButterKnife; 23 | 24 | /** 25 | * Created by lzan13 on 2018/3/19. 26 | */ 27 | 28 | public class LocalContentAdapter extends RecyclerView.Adapter { 29 | 30 | private LayoutInflater layoutInflater; 31 | private List objectList; 32 | private ItemClickListener clickListener; 33 | 34 | public LocalContentAdapter(Context context, List list) { 35 | super(); 36 | objectList = list; 37 | layoutInflater = LayoutInflater.from(context); 38 | } 39 | 40 | @NonNull 41 | @Override 42 | public ContentHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 43 | View view = layoutInflater.inflate(R.layout.item_common_layout, parent, false); 44 | return new ContentHolder(view); 45 | } 46 | 47 | 48 | @Override 49 | public void onBindViewHolder(@NonNull ContentHolder holder, final int position) { 50 | final DIDLObject object = objectList.get(position); 51 | 52 | if (object instanceof Container) { 53 | Container container = (Container) object; 54 | holder.nameView.setText(container.getTitle()); 55 | holder.iconView.setImageResource(R.drawable.ic_folder_24dp); 56 | } else if (object instanceof Item) { 57 | Item item = (Item) object; 58 | holder.nameView.setText(item.getTitle()); 59 | holder.iconView.setImageResource(R.drawable.ic_file_24dp); 60 | } 61 | holder.iconView.setVisibility(View.VISIBLE); 62 | holder.iconView.setColorFilter(R.color.vm_gray_87); 63 | holder.itemView.setOnClickListener(new View.OnClickListener() { 64 | @Override 65 | public void onClick(View v) { 66 | if (clickListener != null) { 67 | clickListener.onItemAction(position, object); 68 | } 69 | } 70 | }); 71 | } 72 | 73 | @Override 74 | public int getItemCount() { 75 | return objectList.size(); 76 | } 77 | 78 | public void refresh() { 79 | notifyDataSetChanged(); 80 | } 81 | 82 | public void setItemClickListener(ItemClickListener listener) { 83 | this.clickListener = listener; 84 | } 85 | 86 | static class ContentHolder extends RecyclerView.ViewHolder { 87 | @BindView(R.id.text_name) TextView nameView; 88 | @BindView(R.id.img_icon) ImageView iconView; 89 | 90 | public ContentHolder(View itemView) { 91 | super(itemView); 92 | ButterKnife.bind(this, itemView); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/adapter/RemoteContentAdapter.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | 12 | import com.vmloft.develop.app.screencast.R; 13 | import com.vmloft.develop.app.screencast.listener.ItemClickListener; 14 | import com.vmloft.develop.app.screencast.entity.RemoteItem; 15 | 16 | import java.util.List; 17 | 18 | import butterknife.BindView; 19 | import butterknife.ButterKnife; 20 | 21 | /** 22 | * Created by lzan13 on 2018/3/9. 23 | * Cling 设备列表适配器 24 | */ 25 | public class RemoteContentAdapter extends RecyclerView.Adapter { 26 | 27 | private LayoutInflater layoutInflater; 28 | private List contentList; 29 | private ItemClickListener clickListener; 30 | 31 | public RemoteContentAdapter(Context context, List list) { 32 | super(); 33 | layoutInflater = LayoutInflater.from(context); 34 | contentList = list; 35 | } 36 | 37 | @NonNull 38 | @Override 39 | public ContentHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 40 | View view = layoutInflater.inflate(R.layout.item_common_layout, parent, false); 41 | return new ContentHolder(view); 42 | } 43 | 44 | @Override 45 | public void onBindViewHolder(@NonNull ContentHolder holder, final int position) { 46 | final RemoteItem item = contentList.get(position); 47 | holder.iconView.setVisibility(View.VISIBLE); 48 | holder.iconView.setColorFilter(R.color.vm_gray_87); 49 | holder.iconView.setImageResource(R.drawable.ic_file_24dp); 50 | holder.nameView.setText(item.getTitle()); 51 | holder.itemView.setOnClickListener(new View.OnClickListener() { 52 | @Override 53 | public void onClick(View v) { 54 | if (clickListener != null) { 55 | clickListener.onItemAction(position, item); 56 | } 57 | } 58 | }); 59 | } 60 | 61 | @Override 62 | public int getItemCount() { 63 | return contentList.size(); 64 | } 65 | 66 | public void refresh() { 67 | notifyDataSetChanged(); 68 | } 69 | 70 | public void setItemClickListener(ItemClickListener listener) { 71 | this.clickListener = listener; 72 | } 73 | 74 | static class ContentHolder extends RecyclerView.ViewHolder { 75 | @BindView(R.id.text_name) TextView nameView; 76 | @BindView(R.id.img_icon) ImageView iconView; 77 | 78 | public ContentHolder(View itemView) { 79 | super(itemView); 80 | ButterKnife.bind(this, itemView); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/event/ControlEvent.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui.event; 2 | 3 | import com.vmloft.develop.app.screencast.entity.AVTransportInfo; 4 | import com.vmloft.develop.app.screencast.entity.RenderingControlInfo; 5 | 6 | /** 7 | * Created by lzan13 on 2018/4/12. 8 | * 投屏控制相关事件类 9 | */ 10 | public class ControlEvent { 11 | private AVTransportInfo avtInfo; 12 | private RenderingControlInfo rcInfo; 13 | 14 | public AVTransportInfo getAvtInfo() { 15 | return avtInfo; 16 | } 17 | 18 | public void setAvtInfo(AVTransportInfo avtInfo) { 19 | this.avtInfo = avtInfo; 20 | } 21 | 22 | public RenderingControlInfo getRcInfo() { 23 | return rcInfo; 24 | } 25 | 26 | public void setRcInfo(RenderingControlInfo rcInfo) { 27 | this.rcInfo = rcInfo; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/event/DIDLEvent.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui.event; 2 | 3 | import org.fourthline.cling.support.model.DIDLContent; 4 | 5 | /** 6 | * Created by lzan13 on 2018/3/19. 7 | * DLNA 本地媒体服务内容变化实践类 8 | */ 9 | public class DIDLEvent { 10 | public DIDLContent content; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/ui/event/DeviceEvent.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.ui.event; 2 | 3 | import com.vmloft.develop.app.screencast.entity.ClingDevice; 4 | 5 | /** 6 | * Created by lzan13 on 2018/3/9. 7 | * 设备相关事件类 8 | */ 9 | public class DeviceEvent { 10 | private ClingDevice clingDevice; 11 | // 变化类型, 12 | private int type = 0; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/screencast/utils/ClingUtil.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.screencast.utils; 2 | 3 | import com.vmloft.develop.app.screencast.entity.RemoteItem; 4 | import com.vmloft.develop.library.tools.utils.VMLog; 5 | 6 | import org.fourthline.cling.support.model.ProtocolInfo; 7 | import org.fourthline.cling.support.model.Res; 8 | import org.fourthline.cling.support.model.item.VideoItem; 9 | import org.seamless.util.MimeType; 10 | 11 | import java.text.SimpleDateFormat; 12 | import java.util.Date; 13 | 14 | /** 15 | * Created by lzan13 on 2018/3/27. 16 | * Cling 相关工具类 17 | */ 18 | public class ClingUtil { 19 | 20 | public static String DIDL_LITE_HEADER = ""; 21 | public static String DIDL_LITE_FOOTER = ""; 22 | 23 | /** 24 | * 获取 Item 资源的 metadata 25 | */ 26 | public static String getItemMetadata(RemoteItem remoteItem) { 27 | Res itemRes = new Res(new MimeType(ProtocolInfo.WILDCARD, ProtocolInfo.WILDCARD), remoteItem 28 | .getSize(), remoteItem.getUrl()); 29 | itemRes.setDuration(remoteItem.getDuration()); 30 | itemRes.setResolution(remoteItem.getResolution()); 31 | VideoItem item = new VideoItem(remoteItem.getId(), "0", remoteItem.getTitle(), remoteItem.getCreator(), itemRes); 32 | 33 | StringBuilder metadata = new StringBuilder(); 34 | metadata.append(DIDL_LITE_HEADER); 35 | 36 | metadata.append(String.format("", item.getId(), item 37 | .getParentID(), item.isRestricted() ? "1" : "0")); 38 | 39 | metadata.append(String.format("%s", item.getTitle())); 40 | String creator = item.getCreator(); 41 | if (creator != null) { 42 | creator = creator.replaceAll("<", "_"); 43 | creator = creator.replaceAll(">", "_"); 44 | } 45 | metadata.append(String.format("%s", item.getClazz().getValue())); 46 | metadata.append(String.format("%s", creator)); 47 | 48 | // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); 49 | // Date now = new Date(); 50 | // String time = sdf.format(now); 51 | // metadata.append(String.format("%s", time)); 52 | 53 | // metadata.append(String.format("%s", 54 | // localItem.get); 55 | 56 | // http://192.168.1.104:8088/Music/07.我醒著做夢.mp3 58 | 59 | Res res = item.getFirstResource(); 60 | if (res != null) { 61 | // protocol info 62 | String protocolInfo = ""; 63 | ProtocolInfo pi = res.getProtocolInfo(); 64 | if (pi != null) { 65 | protocolInfo = String.format("protocolInfo=\"%s:%s:%s:%s\"", pi.getProtocol(), pi.getNetwork(), pi 66 | .getContentFormatMimeType(), pi.getAdditionalInfo()); 67 | } 68 | VMLog.i("protocolInfo: " + protocolInfo); 69 | 70 | // resolution, extra info, not adding yet 71 | String resolution = ""; 72 | if (res.getResolution() != null && res.getResolution().length() > 0) { 73 | resolution = String.format("resolution=\"%s\"", res.getResolution()); 74 | } 75 | 76 | // duration 77 | String duration = ""; 78 | if (res.getDuration() != null && res.getDuration().length() > 0) { 79 | duration = String.format("duration=\"%s\"", res.getDuration()); 80 | } 81 | 82 | // res begin 83 | // metadata.append(String.format("", protocolInfo)); // no resolution & duration yet 84 | metadata.append(String.format("", protocolInfo, resolution, duration)); 85 | 86 | // url 87 | String url = res.getValue(); 88 | metadata.append(url); 89 | 90 | // res end 91 | metadata.append(""); 92 | } 93 | metadata.append(""); 94 | metadata.append(DIDL_LITE_FOOTER); 95 | return metadata.toString(); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cast_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_circle_outline_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_circle_outline_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_power_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_skip_next_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_skip_previous_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_volume_off_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_volume_up_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_device_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 20 | 21 | 29 | 30 | 31 | 36 | 37 | 42 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 20 | 21 | 29 | 30 | 31 | 35 | 36 | 41 | 42 | 52 | 53 | 59 | 60 | 64 | 65 | 66 | 67 | 76 | 77 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_media_play.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 20 | 21 | 29 | 30 | 31 | 36 | 37 | 38 | 42 | 43 | 48 | 49 | 55 | 56 | 63 | 64 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 86 | 87 | 93 | 94 | 100 | 101 | 102 | 107 | 108 | 109 | 113 | 114 | 122 | 123 | 132 | 133 | 142 | 143 | 144 | 145 | 150 | 151 | 158 | 159 | 166 | 167 | 176 | 177 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_local_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 20 | 21 | 26 | 27 | 28 | 32 | 33 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_remote_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/framgnet_device_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_common_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 15 | 16 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/menu/play_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMScreenCast/b4f41ce48f3309c336be58a2452cd02e37d49dc6/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMScreenCast/b4f41ce48f3309c336be58a2452cd02e37d49dc6/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMScreenCast/b4f41ce48f3309c336be58a2452cd02e37d49dc6/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMScreenCast/b4f41ce48f3309c336be58a2452cd02e37d49dc6/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMScreenCast/b4f41ce48f3309c336be58a2452cd02e37d49dc6/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMScreenCast/b4f41ce48f3309c336be58a2452cd02e37d49dc6/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMScreenCast/b4f41ce48f3309c336be58a2452cd02e37d49dc6/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMScreenCast/b4f41ce48f3309c336be58a2452cd02e37d49dc6/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMScreenCast/b4f41ce48f3309c336be58a2452cd02e37d49dc6/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMScreenCast/b4f41ce48f3309c336be58a2452cd02e37d49dc6/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @color/vm_theme_primary 4 | @color/vm_theme_primary_dark 5 | @color/vm_theme_accent 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VMScreenCast 3 | 4 | 投屏设备 5 | 当前投屏资源: 6 | 投屏播放控制 7 | 选择要投屏的设备: 8 | 上级目录 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | 14 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.1.2' 10 | 11 | // NOTE: Do not place your application dependencies here; they belong 12 | // in the individual module build.gradle files 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | mavenCentral() 21 | maven { 22 | url 'http://4thline.org/m2' 23 | } 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMScreenCast/b4f41ce48f3309c336be58a2452cd02e37d49dc6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jun 20 19:27:08 CST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' --------------------------------------------------------------------------------