diff --git a/app/build.gradle b/app/build.gradle
index 0356cf2..eb7c863 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5796d29..2c2e13a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,23 +3,29 @@
xmlns:tools="http://schemas.android.com/tools"
package="org.sifacai.vlcjellyfin">
-
-
-
+
+
+
+
+
@@ -41,7 +47,7 @@
+ android:theme="@style/Theme.AppCompat.NoActionBar" />
CREATOR = new Parcelable.Creator(){
+ @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];
+ }
+ };
+}
diff --git a/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/AVTransportAdapter.java b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/AVTransportAdapter.java
new file mode 100644
index 0000000..fa39b27
--- /dev/null
+++ b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/AVTransportAdapter.java
@@ -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 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);
+ }
+}
diff --git a/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaActivity.java b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaActivity.java
new file mode 100644
index 0000000..e3cd2a5
--- /dev/null
+++ b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaActivity.java
@@ -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 map) {
+ String rsp = "" +
+ "" +
+ "" +
+ "";
+
+ if (map != null) {
+ for (String key : map.keySet()) {
+ rsp += "<" + key + ">" + map.get(key) + "" + key + ">";
+ }
+ }
+
+ rsp += "" +
+ "" +
+ "";
+
+ return rsp;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaControllActivity.java b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaControllActivity.java
new file mode 100644
index 0000000..418fe4f
--- /dev/null
+++ b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaControllActivity.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaDevice.java b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaDevice.java
new file mode 100644
index 0000000..706840a
--- /dev/null
+++ b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaDevice.java
@@ -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 icon = new ArrayList<>();
+ public ArrayList DlnaServices = new ArrayList<>();
+}
diff --git a/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaService.java b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaService.java
new file mode 100644
index 0000000..2020a75
--- /dev/null
+++ b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaService.java
@@ -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;
+}
diff --git a/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/Utils.java b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/Utils.java
index 0718905..b9a318e 100644
--- a/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/Utils.java
+++ b/app/src/main/java/org/sifacai/vlcjellyfin/Dlna/Utils.java
@@ -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 {
diff --git a/app/src/main/java/org/sifacai/vlcjellyfin/Ui/DetailActivity.java b/app/src/main/java/org/sifacai/vlcjellyfin/Ui/DetailActivity.java
index 7cf8ede..573a9c8 100644
--- a/app/src/main/java/org/sifacai/vlcjellyfin/Ui/DetailActivity.java
+++ b/app/src/main/java/org/sifacai/vlcjellyfin/Ui/DetailActivity.java
@@ -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);
diff --git a/app/src/main/java/org/sifacai/vlcjellyfin/Utils/JfClient.java b/app/src/main/java/org/sifacai/vlcjellyfin/Utils/JfClient.java
index 061e080..6b44e93 100644
--- a/app/src/main/java/org/sifacai/vlcjellyfin/Utils/JfClient.java
+++ b/app/src/main/java/org/sifacai/vlcjellyfin/Utils/JfClient.java
@@ -666,6 +666,12 @@ public class JfClient {
return response;
}
+ public static String SendPost(String url,String body) throws IOException {
+ String response = "";
+ response = OkGo.post(url).upBytes(body.getBytes()).execute().body().string();
+ return response;
+ }
+
public static class JJCallBack implements JCallBack {
@Override
diff --git a/app/src/main/res/drawable/baseline_sync_white_42dp.xml b/app/src/main/res/drawable/baseline_sync_white_42dp.xml
new file mode 100644
index 0000000..7c4b0b4
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_sync_white_42dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_dlna.xml b/app/src/main/res/layout/activity_dlna.xml
new file mode 100644
index 0000000..14290d1
--- /dev/null
+++ b/app/src/main/res/layout/activity_dlna.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_dlna_controll.xml b/app/src/main/res/layout/activity_dlna_controll.xml
new file mode 100644
index 0000000..0122739
--- /dev/null
+++ b/app/src/main/res/layout/activity_dlna_controll.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file