添加投屏

This commit is contained in:
sifacaii 2023-05-16 16:56:00 +08:00
parent aeac3008b4
commit ea0af079f2
14 changed files with 546 additions and 7 deletions

View File

@ -43,6 +43,7 @@ dependencies {
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.lzy.net:okgo:3.0.4'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View File

@ -3,23 +3,29 @@
xmlns:tools="http://schemas.android.com/tools"
package="org.sifacai.vlcjellyfin">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".application"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true"
android:icon="@drawable/touchicon144"
android:label="@string/app_name"
android:roundIcon="@drawable/touchicon144"
android:supportsRtl="true"
android:theme="@style/jellyfin"
android:usesCleartextTraffic="true"
android:hardwareAccelerated="true"
tools:targetApi="31">
<activity
android:name=".Dlna.DlnaControllActivity"
android:exported="false" />
<activity
android:name=".Dlna.DlnaActivity"
android:exported="false" />
<activity
android:name=".Ui.HomeActivity"
android:exported="true">
@ -41,7 +47,7 @@
<activity
android:name=".Player.VlcPlayerActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.NoActionBar"/>
android:theme="@style/Theme.AppCompat.NoActionBar" />
<meta-data
android:name="design_width_in_dp"

View File

@ -0,0 +1,47 @@
package org.sifacai.vlcjellyfin.Dlna;
import android.os.Parcel;
import android.os.Parcelable;
public class AVTransport implements Parcelable {
public String iconurl;
public String moduleName;
public String UDN;
public String controlURL;
public String eventSubURL;
public String serviceId;
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(iconurl);
parcel.writeString(moduleName);
parcel.writeString(UDN);
parcel.writeString(controlURL);
parcel.writeString(eventSubURL);
parcel.writeString(serviceId);
}
public static final Parcelable.Creator<AVTransport> CREATOR = new Parcelable.Creator<AVTransport>(){
@Override
public AVTransport createFromParcel(Parcel parcel) {
AVTransport av = new AVTransport();
av.moduleName = parcel.readString();
av.UDN = parcel.readString();
av.serviceId = parcel.readString();
av.controlURL = parcel.readString();
av.eventSubURL = parcel.readString();
av.iconurl = parcel.readString();
return av;
}
@Override
public AVTransport[] newArray(int i) {
return new AVTransport[i];
}
};
}

View File

@ -0,0 +1,71 @@
package org.sifacai.vlcjellyfin.Dlna;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
public class AVTransportAdapter extends RecyclerView.Adapter{
private Context context;
private ArrayList<AVTransport> avTransports;
public AVTransportAdapter(Context context) {
this.context = context;
this.avTransports = new ArrayList<>();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View ll = LayoutInflater.from(context).inflate(android.R.layout.simple_spinner_item, parent, false);
return new RecyclerView.ViewHolder(ll) {
};
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, @SuppressLint("RecyclerView") int position) {
TextView tv = holder.itemView.findViewById(android.R.id.text1);
tv.setText(avTransports.get(position).moduleName);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(listener != null){
listener.onClick(avTransports.get(position));
}
}
});
}
public void addDevice(AVTransport avTransport) {
boolean isexits = false;
for(AVTransport av : avTransports){
if(av.UDN.equals(avTransport.UDN)) isexits = true;
}
if(!isexits) {
avTransports.add(avTransport);
notifyDataSetChanged();
}
}
@Override
public int getItemCount() {
return avTransports.size();
}
private OnItemClickListener listener;
public void setOnItemClickListener(OnItemClickListener listener){
this.listener = listener;
}
public interface OnItemClickListener{
void onClick(AVTransport avTransport);
}
}

View File

@ -0,0 +1,303 @@
package org.sifacai.vlcjellyfin.Dlna;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Parcelable;
import android.util.Log;
import android.util.Xml;
import android.view.View;
import com.owen.tvrecyclerview.widget.V7LinearLayoutManager;
import org.sifacai.vlcjellyfin.Component.JRecyclerView;
import org.sifacai.vlcjellyfin.R;
import org.sifacai.vlcjellyfin.Ui.BaseActivity;
import org.sifacai.vlcjellyfin.Utils.JfClient;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.StringReader;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.HashMap;
public class DlnaActivity extends BaseActivity {
private String TAG = "Dlna播放器";
private JRecyclerView rv;
private AVTransportAdapter avTransportAdapter;
private MulticastSocket mSocket;
private Thread listen_thread;
private byte[] NOTIFY_rootdevice = ("M-SEARCH * HTTP/1.1\n" +
"ST: upnp:rootdevice\n" +
"MX: 10\n" +
"MAN: \"ssdp:discover\"\n" +
"Content-Length: 0\n" +
"HOST: 239.255.255.250:1900").getBytes();
private byte[] NOTIFY_MediaRenderer = ("M-SEARCH * HTTP/1.1\n" +
"ST: urn:schemas-upnp-org:device:MediaRenderer:1\n" +
"MX: 10\n" +
"MAN: \"ssdp:discover\"\n" +
"Content-Length: 0\n" +
"HOST: 239.255.255.250:1900").getBytes();
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 1:
Bundle b = msg.getData();
AVTransport avt = new AVTransport();
avt.moduleName = b.getString("moduleName");
avt.serviceId = b.getString("serviceId");
avt.UDN = b.getString("UDN");
avt.controlURL = b.getString("controlURL");
avt.eventSubURL = b.getString("eventSubURL");
avt.iconurl = b.getString("iconurl");
avTransportAdapter.addDevice(avt);
Log.d(TAG, "handleMessage: " + avt);
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dlna);
getSupportActionBar().hide();
try {
mSocket = new MulticastSocket(1900);
mSocket.joinGroup(InetAddress.getByName("239.255.255.250"));
} catch (IOException e) {
throw new RuntimeException(e);
}
listen_thread = new Thread(listen_Runnable);
findViewById(R.id.refresh).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
refresh();
}
});
rv = findViewById(R.id.mDeviceGridView);
V7LinearLayoutManager layoutManager = new V7LinearLayoutManager(rv.getContext());
layoutManager.setOrientation(V7LinearLayoutManager.VERTICAL);
rv.setLayoutManager(layoutManager);
avTransportAdapter = new AVTransportAdapter(this);
rv.setAdapter(avTransportAdapter);
avTransportAdapter.setOnItemClickListener(new AVTransportAdapter.OnItemClickListener() {
@Override
public void onClick(AVTransport avTransport) {
Intent intent = new Intent(DlnaActivity.this, DlnaControllActivity.class);
intent.putExtra("AVT",avTransport);
startActivity(intent);
}
});
}
@Override
protected void onStart() {
super.onStart();
listen_thread.start();
refresh();
}
@Override
protected void onPause() {
super.onPause();
listen_thread.interrupt();
}
@Override
protected void onStop() {
super.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
/**
* 刷新设备
*/
private void refresh() {
new Thread(new Runnable() {
@Override
public void run() {
DatagramPacket packet = new DatagramPacket(NOTIFY_rootdevice, NOTIFY_rootdevice.length);
try {
packet.setAddress(InetAddress.getByName("239.255.255.250"));
packet.setPort(1900);
mSocket.send(packet);
packet.setData(NOTIFY_MediaRenderer);
mSocket.send(packet);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
private Runnable listen_Runnable = new Runnable() {
@Override
public void run() {
byte[] buff = new byte[1024];
DatagramPacket packet = new DatagramPacket(buff, buff.length);
while (mSocket != null) {
try {
mSocket.receive(packet);
String clientIP = packet.getAddress().getHostAddress();
int clientPort = packet.getPort();
String data = new String(packet.getData()).trim();
Log.d(TAG, "listen: " + clientIP + ":" + clientPort + "" + data);
ProgressNOTIFY(data);
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException(e);
}
}
handler.post(listen_thread);
}
};
public void ProgressNOTIFY(String data) throws IOException, XmlPullParserException {
String[] notify = data.split("\n");
boolean isav = false;
String location = "";
for (String n : notify) {
String[] ns = n.split(":", 2);
//Log.d(TAG, "ProgressNOTIFY: " + String.join(",",ns));
if (ns.length < 2) continue;
else if (ns[0].equals("Location")) location = ns[1];
else if (ns[0].equals("ST")) {
String nsnt = ns[1].trim();
if (nsnt.equals("upnp:rootdevice") || nsnt.indexOf("device:MediaRenderer") >= 0) {
isav = true;
}
}
}
if (isav && !location.equals("")) {
Log.d(TAG, "ProgressNOTIFY: " + location);
String finalLocation = location;
JfClient.SendGet(location,new JfClient.JJCallBack(){
@Override
public void onSuccess(String str) {
Log.d(TAG, "onSuccess: " + str);
findDevice(finalLocation,str);
}
},new JfClient.JJCallBack(){
@Override
public void onError(String str) {
ShowToask(str);
}
});
}
}
public void findDevice(String location,String xml){
DlnaDevice device;
try {
device = ParseXML(xml);
for (int i = 0; i < device.DlnaServices.size(); i++) {
DlnaService ds = device.DlnaServices.get(i);
if (ds.serviceType.indexOf("service:AVTransport") > -1) {
int si = location.indexOf("/", 8);
String url = si > -1 ? location.substring(0, si) : location;
String moduleName = device.friendlyName.equals("") ? device.modelName : device.friendlyName;
Bundle bundle = new Bundle();
bundle.putString("moduleName",moduleName);
bundle.putString("UDN", device.UDN);
bundle.putString("serviceId",ds.serviceId);
bundle.putString("controlURL",url + "/" + ds.controlURL);
bundle.putString("eventSubURL",url + "/" + ds.eventSubURL);
bundle.putString("iconurl",device.icon.size() > 0 ? url + "/" + device.icon.get(0) : "");
Message msg = new Message();
msg.what = 1;
msg.setData(bundle);
handler.sendMessage(msg);
}
}
} catch (XmlPullParserException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public DlnaDevice ParseXML(String xml) throws XmlPullParserException, IOException {
Log.d(TAG, "ParseXML: " + xml);
XmlPullParser xmlPullParser = Xml.newPullParser();
xmlPullParser.setInput(new StringReader(xml));
DlnaDevice device = new DlnaDevice();
int eventType = xmlPullParser.getEventType();
String tagName = "";
DlnaService service = null;
String icon = "";
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_TAG:
tagName = xmlPullParser.getName().toLowerCase();
if (tagName.equals("service")) service = new DlnaService();
if (tagName.equals("icon")) icon = "";
break;
case XmlPullParser.TEXT:
String value = xmlPullParser.getText();
value = value == null ? "" : value.trim();
if (tagName.equals("friendlyname")) device.friendlyName = value;
if (tagName.equals("devicetype")) device.deviceType = value;
if (tagName.equals("modelname")) device.modelName = value;
if (tagName.equals("udn")) device.UDN = value;
if (tagName.equals("url")) icon = value;
if (tagName.equals("servicetype")) service.serviceType = value;
if (tagName.equals("serviceid")) service.serviceId = value;
if (tagName.equals("controlurl")) service.controlURL = value;
if (tagName.equals("scpdurl")) service.SCPDURL = value;
if (tagName.equals("eventsuburl")) service.eventSubURL = value;
break;
case XmlPullParser.END_TAG:
if (xmlPullParser.getName().toLowerCase().equals("service")) device.DlnaServices.add(service);
if (xmlPullParser.getName().toLowerCase().equals("icon")) device.icon.add(icon);
break;
}
eventType = xmlPullParser.next();
}
return device;
}
private String getRspXML(String action, HashMap<String, String> map) {
String rsp = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"" +
" s:encodingstyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" +
"<s:Body>" +
"<u:" + action + "Response xmlns:u=\"urn:schemas-upnp-org:service:AVTransport:1\">";
if (map != null) {
for (String key : map.keySet()) {
rsp += "<" + key + ">" + map.get(key) + "</" + key + ">";
}
}
rsp += "</u:" + action + "Response>" +
"</s:Body>" +
"</s:Envelope>";
return rsp;
}
}

View File

@ -0,0 +1,25 @@
package org.sifacai.vlcjellyfin.Dlna;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import org.sifacai.vlcjellyfin.R;
import org.sifacai.vlcjellyfin.Ui.BaseActivity;
public class DlnaControllActivity extends BaseActivity {
String TAG = "DLNA控制器";
private AVTransport avTransport;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dlna_controll);
avTransport = getIntent().getParcelableExtra("AVT");
Log.d(TAG, "onCreate: " + avTransport.controlURL);
}
}

View File

@ -0,0 +1,13 @@
package org.sifacai.vlcjellyfin.Dlna;
import java.util.ArrayList;
public class DlnaDevice {
public String friendlyName;
public String modelName;
public String deviceType;
public String UDN;
public String location;
public ArrayList<String> icon = new ArrayList<>();
public ArrayList<DlnaService> DlnaServices = new ArrayList<>();
}

View File

@ -0,0 +1,9 @@
package org.sifacai.vlcjellyfin.Dlna;
public class DlnaService {
public String serviceType;
public String serviceId;
public String controlURL;
public String eventSubURL;
public String SCPDURL;
}

View File

@ -3,6 +3,7 @@ package org.sifacai.vlcjellyfin.Dlna;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
public class Utils {

View File

@ -24,6 +24,7 @@ import org.sifacai.vlcjellyfin.Bean.UserData;
import org.sifacai.vlcjellyfin.Component.JAdapter;
import org.sifacai.vlcjellyfin.Component.JRecyclerView;
import org.sifacai.vlcjellyfin.Component.JTAdapter;
import org.sifacai.vlcjellyfin.Dlna.DlnaActivity;
import org.sifacai.vlcjellyfin.Utils.JfClient;
import org.sifacai.vlcjellyfin.R;
import org.sifacai.vlcjellyfin.Utils.Utils;
@ -377,8 +378,7 @@ public class DetailActivity extends BaseActivity implements JAdapter.OnItemClick
public void toVlcPlayer() {
Intent intent;
if (JfClient.config.isDlnaPlayer()) {
ShowToask("投屏播放");
return;
intent = new Intent(this, DlnaActivity.class);
} else if (JfClient.config.isExtensionPlayer()) {
String videourl = JfClient.playList.get(JfClient.playIndex).Url;
Uri uri = Uri.parse(videourl);

View File

@ -666,6 +666,12 @@ public class JfClient {
return response;
}
public static String SendPost(String url,String body) throws IOException {
String response = "";
response = OkGo.<String>post(url).upBytes(body.getBytes()).execute().body().string();
return response;
}
public static class JJCallBack implements JCallBack {
@Override

View File

@ -0,0 +1,5 @@
<vector android:height="42dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="42dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
</vector>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".Dlna.DlnaActivity">
<LinearLayout
android:id="@+id/dlna_tool_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:text="投屏播放"
android:layout_width="0dp"
android:layout_weight="9"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/refresh"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="right"
android:focusable="true"
android:src="@drawable/baseline_sync_white_42dp" />
</LinearLayout>
<org.sifacai.vlcjellyfin.Component.JRecyclerView
android:id="@+id/mDeviceGridView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:padding="@dimen/padding_border"
app:tv_horizontalSpacingWithMargins="@dimen/vh_space_width"
app:tv_selectedItemIsCentered="true"
app:tv_verticalSpacingWithMargins="@dimen/vh_space_width" />
</LinearLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".Dlna.DlnaControllActivity">
</androidx.constraintlayout.widget.ConstraintLayout>