From ea0af079f2beb51e361c47a8ba7e0a09a9d27008 Mon Sep 17 00:00:00 2001 From: sifacaii Date: Tue, 16 May 2023 16:56:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=8A=95=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 16 +- .../sifacai/vlcjellyfin/Dlna/AVTransport.java | 47 +++ .../vlcjellyfin/Dlna/AVTransportAdapter.java | 71 ++++ .../vlcjellyfin/Dlna/DlnaActivity.java | 303 ++++++++++++++++++ .../Dlna/DlnaControllActivity.java | 25 ++ .../sifacai/vlcjellyfin/Dlna/DlnaDevice.java | 13 + .../sifacai/vlcjellyfin/Dlna/DlnaService.java | 9 + .../org/sifacai/vlcjellyfin/Dlna/Utils.java | 1 + .../vlcjellyfin/Ui/DetailActivity.java | 4 +- .../sifacai/vlcjellyfin/Utils/JfClient.java | 6 + .../res/drawable/baseline_sync_white_42dp.xml | 5 + app/src/main/res/layout/activity_dlna.xml | 43 +++ .../res/layout/activity_dlna_controll.xml | 9 + 14 files changed, 546 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/org/sifacai/vlcjellyfin/Dlna/AVTransport.java create mode 100644 app/src/main/java/org/sifacai/vlcjellyfin/Dlna/AVTransportAdapter.java create mode 100644 app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaActivity.java create mode 100644 app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaControllActivity.java create mode 100644 app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaDevice.java create mode 100644 app/src/main/java/org/sifacai/vlcjellyfin/Dlna/DlnaService.java create mode 100644 app/src/main/res/drawable/baseline_sync_white_42dp.xml create mode 100644 app/src/main/res/layout/activity_dlna.xml create mode 100644 app/src/main/res/layout/activity_dlna_controll.xml 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) + ""; + } + } + + 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