被老板叼了几次了,最近实在忍不了,准备离职;
但是担心离职后长时间没有办法找到工作
就想贡献一套平时琢磨出来的程序,请各位有能力的话带我熬过这凛冽的寒冬。
目前写出来的,有三个端:安卓TV端,安卓手机端,Windows-Qt端
这篇是TV端的部分代码,当然要理解一下,我并不能粘贴出完整的代码,请见谅;
想要获取完整源码可以直接私信我,万分感激
那下面就开始粘贴部分源码:
1.SSDP的发送
DLNA是通过监听网络当中的广播进行解析的,也就是在循环发送几个消息
这些消息,有的是关于设备信息的,有些是关于控制命令的,总之得根据自己的来
public String toString(int i) {
//都是些固定格式,NT和USN是两辆相关
StringBuilder content = new StringBuilder();
content.append("NOTIFY * HTTP/1.1").append("\r\n");
content.append("SERVER: Linux/3.14.29 UPnP/1.0 Cling/2.0").append("\r\n");
content.append("CACHE-CONTROL: max-age=1800").append("\r\n");//指定通知消息存活时间,如果超过此时间间隔,控制点可以认为设备不存在
content.append("LOCATION: "+_LOCATION).append("\r\n");
//乐播投屏加了下面三行,加了就搜索不到了
// content.append("OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01; ds=0").append("\r\n");
// content.append("01-NLS: 0dcebdf8-1dd2-11b2-8b72-f07170d94291").append("\r\n");
// content.append("X-User-Agent: redsonic").append("\r\n");
content.append("NTS: ssdp:alive").append("\r\n");
if(i==0){
content.append("NT: upnp:rootdevice").append("\r\n");
content.append("USN: uuid:"+ MainActivity._UUID+"::upnp:rootdevice").append("\r\n");
}else if(i==1){
content.append("NT: uuid:"+MainActivity._UUID).append("\r\n");
content.append("USN: uuid:"+MainActivity._UUID).append("\r\n");
} else if(i==2){
content.append("NT: urn:schemas-upnp-org:device:MediaRenderer:1").append("\r\n");
content.append("USN: uuid:"+MainActivity._UUID+"::urn:schemas-upnp-org:device:MediaRenderer:1").append("\r\n");
}else if(i==3){
content.append("NT: urn:schemas-upnp-org:service:RenderingControl:1").append("\r\n");
content.append("USN: uuid:"+MainActivity._UUID+"::urn:schemas-upnp-org:service:RenderingControl:1").append("\r\n");
}else if(i==4){
content.append("NT: urn:schemas-upnp-org:service:ConnectionManager:1").append("\r\n");
content.append("USN: uuid:"+MainActivity._UUID+"::urn:schemas-upnp-org:service:ConnectionManager:1").append("\r\n");
}else if(i==5){
content.append("NT: urn:schemas-upnp-org:service:AVTransport:1").append("\r\n");
content.append("USN: uuid:"+MainActivity._UUID+"::urn:schemas-upnp-org:service:AVTransport:1").append("\r\n");
}
content.append("HOST: 239.255.255.250:1900").append("\r\n");
content.append("\r\n");
//Log.e("SSDPSendMsg",MainActivity._UUID+"\r\n\r\n");
return content.toString();
//https://blog.csdn.net/thebestleo/article/details/50800781
}
安卓端,发送成功后,其实并不能在各大平台的设备上正常显示
这时候就需要在安卓端搭建一个web服务,web服务里面指向一个描述的xml,有关这个投屏设备的一些可以使用的信息,比如酒店某个"房间号","品牌+房间号"都可以动态设置,比如我就设置的MyDLNA
2.搭建一个web服务
//noinspection GradleDependency
api 'org.eclipse.jetty:jetty-server:8.1.21.v20160908'
//noinspection GradleDependency
api 'org.eclipse.jetty:jetty-servlet:8.1.21.v20160908'
//noinspection GradleDependency
api 'org.eclipse.jetty:jetty-client:8.1.21.v20160908'
api 'org.nanohttpd:nanohttpd:2.3.1'
我这边就不粘贴很多
粘贴一下认证部分的内容,认证说起来也简单,就是电视端给一个链接让手机端可以访问,电视端把这个链接转换成二维码;
手机要先连接和电视机端同样的网络下面,手机扫码后直接就请求电视机了
扫码后请求电视机会拿到请求的头部,也就请求来的手机ip地址,把这个ip缓存一下,做一下判断,只给这个手机ip发送广播,这样就可以实现扫码后单一的投屏,就不会在同一个酒店下面显示所有房间的电视机了
public class ControlHandler extends org.eclipse.jetty.server.handler.DefaultHandler {
private Activity myactivity;
private String PlayerURL = "";
private String DesiredVolume = "";
private String PlayTarget = "";
private String PlayState = "NO_MEDIA_PRESENT";
private String PlayerURLTemp = "xxx";
public ControlHandler(Activity activity) {
super();
this.myactivity = activity;
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
if (response.isCommitted() || baseRequest.isHandled()) {
return;
}
Log.e("RequestShare", request.getRequestURI());
//认证过就返回
if (request.getRequestURI().contains("/certification")) {
Constants.IPDEVICE = request.getRemoteAddr();
Log.e("RequestShare", "正在认证设备");
baseRequest.setHandled(true);
ServletMsg.certificationMsg(response);
}
//这个是读取认证的设备的(可以使得投屏设备具有唯一性)
if (request.getRequestURI().contains("/upnp/dev/" + MainActivity._UUID + "/desc")) {//验证可以通过对比IP实现指定设备投屏
if (Constants.ALLDEVICES) {
Log.e("RequestShare", "所有设备都可以投屏");
baseRequest.setHandled(true);
ServletMsg.defaultMsg(response);
} else {
if (request.getRemoteAddr().contains(Constants.IPDEVICE)) {//验证可以通过对比IP实现指定设备投屏
Log.e("RequestShare", "指定:" + request.getRemoteAddr() + "设备符合投屏条件");
baseRequest.setHandled(true);
response.addHeader("Content-type: ", "text/xml");
response.addHeader("SERVER: ", "Linux/3.14.29 UPnP/1.0 Cling/2.0");
response.addHeader("Cache-control: ", "max-age=1800");
// response.addHeader("X-User-Agent: ","redsonic");
ServletMsg.defaultMsg(response);
}
}
}
//请求分析
if (Constants.ALLDEVICES) {
getHeader(request, baseRequest, response); //头部执行播放暂停之类的简单操作
getBody(request, baseRequest, response);//这个主要是主要处理有携带参数的数据,response还是加了一下,避免以后自己做控制端
//爱奇艺和腾讯会请求里面链接,单纯回复可能不管用,加的一项
ServletRequest(request, baseRequest, response);
} else {
if (request.getRemoteAddr().contains(Constants.IPDEVICE)) {//验证可以通过对比IP实现指定设备投屏
Log.e("RequestShare1", request.getRemoteAddr());//IP地址
Log.e("RequestShare2", request.getRequestURI());//请求的路径
getHeader(request, baseRequest, response); //头部执行播放暂停之类的简单操作
getBody(request, baseRequest, response);//这个主要是主要处理有携带参数的数据,response还是加了一下,避免以后自己做控制端
//爱奇艺和腾讯会请求里面链接,单纯回复可能不管用,加的一项
ServletRequest(request, baseRequest, response);
}
}
}
3.投射视频
讲到投射视频,就很简单了,可以理解为手机向电视机发送了一个请求,只不过这个请求头请求体都需要根据特定格式来
例如:
<?xml version="1.0" encoding="UTF-8"?><s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><CurrentURI>http://183.131.147.16/upgcxcode/09/78/1255627809/1255627809-1-192.mp4?e=ig8euxZM2rNcNbNBhWdVhwdlhbU1hwdVhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8gNEVE5XREto8z5JZC2X2gkX5L5F1eTX1jkXlsTXHeux_f2o859IMvNC8xNbLEkF6MuwLStj8fqJ0EkX1ftx7Sqr_aio8_\&ua=tvproj\&uipk=5\&nbs=1\&deadline=1693797974\&gen=playurlv2\&os=bcache\&oi=3728755326\&trid=00002c92df64c42a43fa9d53a3d4ca9263eaT\&mid=434537794\&upsig=a446d61c8f79924e141b2973d94a0715\&uparams=e%2Cua%2Cuipk%2Cnbs%2Cdeadline%2Cgen%2Cos%2Coi%2Ctrid%2Cmid\&cdnid=22222\&bvc=vod\&nettype=0\&bw=251536\&orderid=0%2C3\&buvid=Y947C4998063ABA44C5595C15C70401D62DF\&build=70100100\&mobi_app=iphone\&f=T_0_0\&logo=80000000\&_nva_ext_=\</CurrentURI><CurrentURIMetaData><DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"><item id="0" parentID="-1" restricted="1"><dc:title>我花1万块重庆打车到上海,却被全国网友说成老赖?</dc:title><upnp:storageMedium>UNKNOWN</upnp:storageMedium><upnp:writeStatus>UNKNOWN</upnp:writeStatus><upnp:longDescription>8ZPKtX3aKq2ata7O0mJBw8X-jfpm6X7rvBzgi6zEJLdfXlUYjO_zomV6xIkLrNS8Y97j6EBY4i-BmrgA2WWIqDn9Gd6p83nZCVDTdbhDOos4x_QGAByL3FXnYPdYvFYaHluTvVQ5MKCckyKCQIKKxdYatoPoEqsgeVHUPN4WtaAChNi5UaHQfwtwRNaZO_SSa9jnMEYFDhN4kpzC4A0TmmqKUbUQvdIR9fh7acHLpKz3Rt7XmnEu-o9jfV7km3nwBpPT8il6Ycxp5Gw0k-kEa192ccOzzA0V11Bg7EntZzm7aJKn2QVwqg5t8hFPxpdS32W6yzM1W-7usjfm2SOESC4guqmfBlaOxdVL8hnG3mdl_BGU3kmcVhKz8u0u79UsDTWEElD1088XFetEb--WevzgLqcQF53VlmwPzCF1hoIXID5L4UaTmLu6cSzi3dKW</upnp:longDescription><res protocolInfo="http-get:*:video/mp4:DLNA.ORG_PN=MPEG4_P2_SP_AAC;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000">http://183.131.147.16/upgcxcode/09/78/1255627809/1255627809-1-192.mp4?e=ig8euxZM2rNcNbNBhWdVhwdlhbU1hwdVhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8gNEVE5XREto8z5JZC2X2gkX5L5F1eTX1jkXlsTXHeux_f2o859IMvNC8xNbLEkF6MuwLStj8fqJ0EkX1ftx7Sqr_aio8_\&amp;ua=tvproj\&amp;uipk=5\&amp;nbs=1\&amp;deadline=1693797974\&amp;gen=playurlv2\&amp;os=bcache\&amp;oi=3728755326\&amp;trid=00002c92df64c42a43fa9d53a3d4ca9263eaT\&amp;mid=434537794\&amp;upsig=a446d61c8f79924e141b2973d94a0715\&amp;uparams=e%2Cua%2Cuipk%2Cnbs%2Cdeadline%2Cgen%2Cos%2Coi%2Ctrid%2Cmid\&amp;cdnid=22222\&amp;bvc=vod\&amp;nettype=0\&amp;bw=251536\&amp;orderid=0%2C3\&amp;buvid=Y947C4998063ABA44C5595C15C70401D62DF\&amp;build=70100100\&amp;mobi_app=iphone\&amp;f=T_0_0\&amp;logo=80000000\&amp;_nva_ext_=\</res\>\<upnp:class\>object.item.videoItem\</upnp:class\>\</item\>\</DIDL-Lite\>\</CurrentURIMetaData></u:SetAVTransportURI></s:Body></s:Envelope>
这就是发过来的视频连接,适当解析后就可以正常播放了
4.监听播放与控制进度
手机端会不断给电视端发送实时数据,比如音量啊,进度啊,这些;手机监听到系统音量后,会给DLNA也发一条关于音量的数据,电视机拿到就处理一下即可
大部分视频厂家都是可以正常上报,优酷和芒果倒是加了些机制,需要电视机端给设备发消息,说我已经开始播放了,然后优酷才会继续播,不然播了两秒,优酷没有收到电视机的消息,会立刻把链接关掉云云..
//获取body信息
private void getBody(HttpServletRequest request, Request baseRequest, HttpServletResponse response) throws IOException {
Log.e("getBody", "------getBody分割线start------");
BufferedReader br = request.getReader();
String str, wholeStr = "";
while ((str = br.readLine()) != null) {
wholeStr += str;
}
Log.e("getBody", wholeStr);
//拿到播放的链接
if (wholeStr.contains("u:SetAVTransportURI")) {
Log.e("55m5m53", wholeStr);
String temp = wholeStr.substring(0, wholeStr.indexOf("</CurrentURI>"));
PlayerURL = temp.substring(temp.indexOf("<CurrentURI>"))
.replace("<CurrentURI>", "")
.replace("&", "&")
.replace(""", "\"")
.replace("<", "<")
.replace(">", "<")
.replace(" ", " ")
.replace("'", "'");
Log.e("55m5m54", PlayerURL);
PlayState = "PLAYING";
//Response响应一下
baseRequest.setHandled(true);
ServletMsg.SetAVTransportURIResponseMsg(response);
//发送并打开页面给播放器
// Intent intent = new Intent(myactivity, PlayerActivity.class);
// intent.putExtra("PlayerURL", PlayerURL);
// myactivity.startActivity(intent);
//芒果TV一次性发了5个
// if (!PlayerURL.equals(PlayerURLTemp)) {
// PlayerURLTemp = PlayerURL;
countDownTimer.start();
// intent.putExtra("PlayerURL", PlayerURL);
// myactivity.startActivity(intent);
// }
} else if (wholeStr.contains("u:SetVolume")) {
//摘取出要我设置的音量
String Volume = wholeStr.substring(0, wholeStr.indexOf("</DesiredVolume>"));
DesiredVolume = Volume.substring(Volume.indexOf("<DesiredVolume>")).replace("<DesiredVolume>", "");
Log.e("SetVolume", DesiredVolume);
//Response响应一下
baseRequest.setHandled(true);
ServletMsg.SetVolumeResponseMsg(response);
//到这边的意思就是和播放器说要控制一下设备的音频
SendBroadcast("Volume#" + DesiredVolume);
} else if (wholeStr.contains("u:Seek")) {
//摘取出要我设置的进度
String Target = wholeStr.substring(0, wholeStr.indexOf("</Target>"));
PlayTarget = Target.substring(Target.indexOf("<Target>")).replace("<Target>", "");
Log.e("PlayTarget", PlayTarget);
SendBroadcast("Seek#" + PlayTarget);//告诉播放器,我要设置进度了
} else if (wholeStr.contains("u:GetTransportInfo")) {
//告诉控制端,我还活着
Log.e("u:GetTransportInfo", "电视系统的State:" + PlayState);
baseRequest.setHandled(true);
ServletMsg.GetTransportInfoMsg(response, PlayState, "OK");
} else if (wholeStr.contains("u:GetVolume")) {
//腾讯bilibili会要求获取一下音量,就给呗
//bilibili给了后,会一直索要~好奇怪
Log.e("u:GetVolume", "电视系统的Volume:" + PlayerActivity.playerData.get_volume());
baseRequest.setHandled(true);
ServletMsg.GetVolumeMsg(response, PlayerActivity.playerData.get_volume());
} else if (wholeStr.contains("u:GetPositionInfo")) {
//告诉控制端我当前的播放状态
GetPositionInfo(response, baseRequest);
} else if (wholeStr.contains("u:GetMediaInfo")) {
//爱奇艺单独列了这个~,尝试发了,但没反应,找了几个数据,应该是正确的
Log.e("getBody", "我拿到GetMediaInfo了");
GetMediaInfo(response, baseRequest);
} else if (wholeStr.contains("u:Stop")) {
//有些东西需要响应的,就响应一下
Log.e("getBody", "我拿到Stop了");
baseRequest.setHandled(true);
ServletMsg.StopResponseMsg(response);
}
Log.e("getBody", "------getBody分割线end------");
}
private CountDownTimer countDownTimer = new CountDownTimer(1000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
}
@Override
public void onFinish() {
//PlayerURLTemp="Fowlet";
if(isTopActivity(Constants.PLAYACTIVITY)){
SendBroadcast("State#Stop");
}
Intent intent = new Intent(myactivity, PlayerActivity.class);
//Intent intent = new Intent(myactivity, Player2Activity.class);
intent.putExtra("PlayerURL", PlayerURL);
myactivity.startActivity(intent);
}
};
//获取的头部信息
private void getHeader(HttpServletRequest request, Request baseRequest, HttpServletResponse response) {
Log.e("getHeader", "------getHeader分割线start------");
Enumeration<String> em = request.getHeaderNames();
while (em.hasMoreElements()) {
String name = (String) em.nextElement();
String value = request.getHeader(name);
//System.out.println(name + "=" + value);
Log.e("getHeader", name + "=" + value);
if (name.equals("SOAPAction") | name.equals("SOAPACTION")) {
String[] split = value.split("#");
String result = split[1];
//获取头部的一些快捷操作,如果说有一些要参数要回调的话,还是要走body
String Transport = result.substring(0, result.indexOf("\""));
Log.e("yongge", Transport);
if (Transport.equals("Play")) {
//播放
PlayState = "PLAYING";
Log.e("Header__", "我拿到Play了:" + PlayState);
//开始播放了就让播放器开始收集信息
//SendBroadcast("GetInfo#"+Transport);
//暂停播放也会用的到
SendBroadcast("State#" + Transport);
} else if (Transport.equals("Stop")) {
//停止
PlayState = "STOPPED";
Log.e("Header", "我拿到Stop了:" + PlayState);
SendBroadcast("State#" + Transport);
} else if (Transport.equals("Pause")) {
//暂停
PlayState = "PAUSED_PLAYBACK";
Log.e("Header__", "我拿到Pause了:" + PlayState);
SendBroadcast("State#" + Transport);
} else if (Transport.equals("SetVolume")) {
//设置音量
Log.e("Header", "我拿到SetVolume了" + DesiredVolume);
} else if (Transport.equals("GetPositionInfo")) {
SendBroadcast("GetInfo#" + Transport);//告诉播放器,你要开始收集播放器信息了
//获取进度
Log.e("Header", "我拿到GetPositionInfo了");
} else if (Transport.equals("GetTransportInfo")) {
//获取传输状态
//安卓端的腾讯是拿的GetTransportInfo
Log.e("Header", "我拿到GetTransportInfo了");
} else if (Transport.equals("SetMute")) {
//静音/取消
Log.e("Header", "我拿到SetMute了");
} else if (Transport.equals("Seek")) {
//设置进度
Log.e("Header", "我拿到Seek了" + PlayerActivity.playerData.get_seek());
} else if (Transport.equals("GetMediaInfo")) {
//爱奇艺单独列了这个~
Log.e("Header", "我拿到GetMediaInfo了");
} else if (Transport.equals("GetVolume")) {
Log.e("Header", "我拿到GetVolume了");
}
}
//优酷是要走订阅
if (name.equals("CALLBACK")) {
String callbackUrl = value.replace("<", "").replace(">", "");
Log.e("callbackUrl", callbackUrl);
String str[] = callbackUrl.replace("/","").split(":");
try {
Socket socket = new Socket(str[1], Integer.parseInt(str[2]));
Log.e("callbackUrl2", str[1]+"---"+str[2]);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("HTTP/1.1 200 OK\r\n");
bw.write("Server: Linux/3.14.29 UPnP/1.0 Cling/2.0\r\n");
bw.write("SID: uuid:"+MainActivity._UUID+"\r\n");
bw.write("Content-Type: text/html; charset=\"utf-8\""+"\r\n");
bw.write("TIMEOUT: Second-3600"+"\r\n\r\n");
bw.flush();
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while(true) {
String readLine = null;
if ((readLine = br.readLine()) != null) {
Log.e("socket", readLine);
} else {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
// YoukuMsg youkuMsg = new YoukuMsg();
// //要返回一下订阅成功
// baseRequest.setHandled(true);
// ServletMsg.VendorMsg(response, youkuMsg.toString());
// SubResponse subRespons = new SubResponse();
// subRespons.getTest(callbackUrl,MainActivity._UUID);
// onUnicastSend("172.16.0.121", 49152);//源IP和源端口
//
// response.setStatus(SC_OK);
// response.addHeader("Server: ","Linux/3.14.29 UPnP/1.0 Cling/2.0");
// response.addHeader("SID: ","uuid:"+MainActivity._UUID);
// //response.addHeader("Content-Type: ","text/html; charset=\"utf-8\"");
// response.addHeader("TIMEOUT: ","Second-3600");
// response.setContentType("text/html; charset=\"utf-8\"");
//
// baseRequest.setHandled(true);
// ServletMsg.VendorMsg(response, TencentMsg.RenderingControl_desc);
// baseRequest.setHandled(true);
// ServletMsg.VendorMsg(response, "");
// response.setHeader();
}
}
Log.e("getHeader", "------getHeader分割线end:" + PlayState + "------");
}
处理返回的进度