1pub const AVTRANSPORT_SCPD: &str = include_str!("../xml/AVTransport.xml");
4pub const RENDERING_CONTROL_SCPD: &str = include_str!("../xml/RenderingControl.xml");
5pub const CONNECTION_MANAGER_SCPD: &str = include_str!("../xml/ConnectionManager.xml");
6
7pub fn escape(s: &str) -> String {
9 let mut out = String::with_capacity(s.len());
10 for c in s.chars() {
11 match c {
12 '&' => out.push_str("&"),
13 '<' => out.push_str("<"),
14 '>' => out.push_str(">"),
15 '"' => out.push_str("""),
16 _ => out.push(c),
17 }
18 }
19 out
20}
21
22pub fn unescape(s: &str) -> String {
25 let mut out = String::with_capacity(s.len());
26 let mut rest = s;
27 while let Some(pos) = rest.find('&') {
28 out.push_str(&rest[..pos]);
29 rest = &rest[pos..];
30 let decoded = rest[1..].find(';').filter(|&n| n <= 10).and_then(|n| {
31 let name = &rest[1..n + 1];
32 let c = match name {
33 "amp" => Some('&'),
34 "lt" => Some('<'),
35 "gt" => Some('>'),
36 "quot" => Some('"'),
37 "apos" => Some('\''),
38 "nbsp" => Some('\u{a0}'),
39 _ => {
40 let code =
41 if let Some(hex) = name.strip_prefix("#x").or(name.strip_prefix("#X")) {
42 u32::from_str_radix(hex, 16).ok()
43 } else if let Some(dec) = name.strip_prefix('#') {
44 dec.parse().ok()
45 } else {
46 None
47 };
48 code.and_then(char::from_u32)
49 }
50 };
51 c.map(|c| (c, n + 2))
52 });
53 match decoded {
54 Some((c, len)) => {
55 out.push(c);
56 rest = &rest[len..];
57 }
58 None => {
59 out.push('&');
60 rest = &rest[1..];
61 }
62 }
63 }
64 out.push_str(rest);
65 out
66}
67
68pub fn didl_metadata(title: &str, url_video: &str, url_srt: Option<&str>) -> String {
70 let subtitle = match url_srt {
71 Some(srt) => {
72 let srt = escape(srt);
73 format!(
74 r#"
75 <res protocolInfo="http-get:*:text/srt:*">{srt}</res>
76 <sec:CaptionInfo sec:type="srt">{srt}</sec:CaptionInfo>
77 <sec:CaptionInfoEx sec:type="srt">{srt}</sec:CaptionInfoEx>
78 <pv:subtitleFileUri>{srt}</pv:subtitleFileUri>"#
79 )
80 }
81 None => String::new(),
82 };
83 format!(
84 r#"<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
85 xmlns:dc="http://purl.org/dc/elements/1.1/"
86 xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
87 xmlns:sec="http://www.sec.co.kr/"
88 xmlns:pv="http://www.pv.com/pvns/">
89 <item id="0" parentID="-1" restricted="false">
90 <dc:title>{title}</dc:title>
91 <upnp:class>object.item.videoItem</upnp:class>
92 <res protocolInfo="http-get:*:video/mp4:DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000">{url_video}</res>{subtitle}
93 </item>
94</DIDL-Lite>"#,
95 title = escape(title),
96 url_video = escape(url_video),
97 )
98}
99
100pub fn set_av_transport_uri(url_video: &str, metadata: &str) -> String {
101 format!(
102 r#"<?xml version='1.0' encoding='utf-8'?>
103<s:Envelope
104 s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
105 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
106 <s:Body>
107 <u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
108 <InstanceID>0</InstanceID>
109 <CurrentURI>{}</CurrentURI>
110 <CurrentURIMetaData>{}</CurrentURIMetaData>
111 </u:SetAVTransportURI>
112 </s:Body>
113</s:Envelope>
114"#,
115 escape(url_video),
116 escape(metadata),
117 )
118}
119
120pub const PLAY: &str = r#"<?xml version='1.0' encoding='utf-8'?>
121<s:Envelope
122 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
123 s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
124 <s:Body>
125 <u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
126 <InstanceID>0</InstanceID>
127 <Speed>1</Speed>
128 </u:Play>
129 </s:Body>
130</s:Envelope>
131"#;
132
133pub const STOP: &str = r#"<?xml version='1.0' encoding='utf-8'?>
134<s:Envelope
135 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
136 s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
137 <s:Body>
138 <u:Stop xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
139 <InstanceID>0</InstanceID>
140 </u:Stop>
141 </s:Body>
142</s:Envelope>
143"#;
144
145pub const GET_TRANSPORT_INFO: &str = r#"<?xml version='1.0' encoding='utf-8'?>
146<s:Envelope
147 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
148 s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
149 <s:Body>
150 <u:GetTransportInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
151 <InstanceID>0</InstanceID>
152 </u:GetTransportInfo>
153 </s:Body>
154</s:Envelope>
155"#;
156
157pub const GET_MEDIA_INFO: &str = r#"<?xml version='1.0' encoding='utf-8'?>
158<s:Envelope
159 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
160 s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
161 <s:Body>
162 <u:GetMediaInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
163 <InstanceID>0</InstanceID>
164 </u:GetMediaInfo>
165 </s:Body>
166</s:Envelope>
167"#;
168
169pub fn seek(target: &str) -> String {
170 format!(
171 r#"<?xml version="1.0" encoding="utf-8"?>
172<s:Envelope
173 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
174 s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
175 <s:Body>
176 <u:Seek xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
177 <InstanceID>0</InstanceID>
178 <Unit>REL_TIME</Unit>
179 <Target>{}</Target>
180 </u:Seek>
181 </s:Body>
182</s:Envelope>
183"#,
184 escape(target)
185 )
186}
187
188pub fn description(friendly_name: &str, uuid: &str) -> String {
190 format!(
191 r#"<?xml version="1.0" encoding="UTF-8"?>
192<root xmlns:dlna="urn:schemas-dlna-org:device-1-0" xmlns="urn:schemas-upnp-org:device-1-0">
193 <specVersion>
194 <major>1</major>
195 <minor>0</minor>
196 </specVersion>
197 <device>
198 <deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
199 <friendlyName>{}</friendlyName>
200 <UDN>uuid:{}</UDN>
201 <manufacturer>mitnk</manufacturer>
202 <modelName>Tiny-Render</modelName>
203 <modelDescription>AVTransport Media Renderer</modelDescription>
204 <dlna:X_DLNADOC xmlns:dlna="urn:schemas-dlna-org:device-1-0">DMR-1.50</dlna:X_DLNADOC>
205 <serviceList>
206 <service>
207 <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
208 <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
209 <controlURL>AVTransport/control</controlURL>
210 <SCPDURL>dlna/AVTransport.xml</SCPDURL>
211 </service>
212 <service>
213 <serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
214 <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
215 <controlURL>RenderingControl/action</controlURL>
216 <eventSubURL>RenderingControl/event</eventSubURL>
217 <SCPDURL>dlna/RenderingControl.xml</SCPDURL>
218 </service>
219 <service>
220 <serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
221 <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
222 <controlURL>ConnectionManager/action</controlURL>
223 <eventSubURL>ConnectionManager/event</eventSubURL>
224 <SCPDURL>dlna/ConnectionManager.xml</SCPDURL>
225 </service>
226 </serviceList>
227 </device>
228</root>"#,
229 escape(friendly_name),
230 escape(uuid),
231 )
232}
233
234pub const SET_AV_TRANSPORT_URI_RESPONSE: &str = r#"
235<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
236 <s:Body>
237 <u:SetAVTransportURIResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
238 </s:Body>
239</s:Envelope>
240"#;
241
242pub const PLAY_RESPONSE: &str = r#"
243<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
244 <s:Body>
245 <u:PlayResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
246 </s:Body>
247</s:Envelope>
248"#;
249
250pub const SEEK_RESPONSE: &str = r#"
251<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
252 <s:Body>
253 <u:SeekResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
254 </s:Body>
255</s:Envelope>
256"#;
257
258pub const STOP_RESPONSE: &str = r#"
259<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
260 <s:Body>
261 <u:StopResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
262 </s:Body>
263</s:Envelope>
264"#;
265
266pub fn transport_info_response(state: &str) -> String {
268 format!(
269 r#"
270<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
271 <s:Body>
272 <u:GetTransportInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
273 <CurrentTransportState>{state}</CurrentTransportState>
274 <CurrentTransportStatus>OK</CurrentTransportStatus>
275 <CurrentSpeed>1</CurrentSpeed>
276 </u:GetTransportInfoResponse>
277 </s:Body>
278</s:Envelope>
279"#
280 )
281}
282
283pub fn media_info_response(current_uri: &str) -> String {
284 format!(
285 r#"
286<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
287 <s:Body>
288 <u:GetMediaInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
289 <NrTracks>{tracks}</NrTracks>
290 <MediaDuration>0:00:00</MediaDuration>
291 <CurrentURI>{uri}</CurrentURI>
292 <CurrentURIMetaData></CurrentURIMetaData>
293 <NextURI></NextURI>
294 <NextURIMetaData></NextURIMetaData>
295 <PlayMedium>NETWORK</PlayMedium>
296 <RecordMedium>NOT_IMPLEMENTED</RecordMedium>
297 <WriteStatus>NOT_IMPLEMENTED</WriteStatus>
298 </u:GetMediaInfoResponse>
299 </s:Body>
300</s:Envelope>
301"#,
302 tracks = u8::from(!current_uri.is_empty()),
303 uri = escape(current_uri),
304 )
305}
306
307pub fn position_info_response(rel_time: &str) -> String {
308 format!(
309 r#"
310<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
311 <s:Body>
312 <u:GetPositionInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
313 <Track>0</Track>
314 <TrackDuration>01:23:45</TrackDuration>
315 <RelTime>{rel_time}</RelTime>
316 <AbsTime>{rel_time}</AbsTime>
317 <RelCount>2147483647</RelCount>
318 <AbsCount>2147483647</AbsCount>
319 </u:GetPositionInfoResponse>
320 </s:Body>
321</s:Envelope>
322"#
323 )
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn escape_roundtrip() {
332 let s = r#"a&b<c>"d"#;
333 assert_eq!(escape(s), "a&b<c>"d");
334 assert_eq!(unescape(&escape(s)), s);
335 }
336
337 #[test]
338 fn unescape_numeric_and_unknown() {
339 assert_eq!(unescape("&<&foo; & x"), "&<&foo; & x");
340 }
341
342 #[test]
343 fn static_documents_parse() {
344 for doc in [
345 AVTRANSPORT_SCPD,
346 RENDERING_CONTROL_SCPD,
347 CONNECTION_MANAGER_SCPD,
348 ] {
349 roxmltree::Document::parse(doc).unwrap();
350 }
351 roxmltree::Document::parse(&description("A & B <x>", "u-1")).unwrap();
352 roxmltree::Document::parse(GET_TRANSPORT_INFO).unwrap();
353 roxmltree::Document::parse(GET_MEDIA_INFO).unwrap();
354 roxmltree::Document::parse(&transport_info_response("STOPPED")).unwrap();
355 roxmltree::Document::parse(&media_info_response("http://h/v?a=1&b=2")).unwrap();
356 let meta = didl_metadata("t&t", "http://h/v?a=1&b=2", Some("http://h/s.srt"));
357 roxmltree::Document::parse(&meta).unwrap();
358 roxmltree::Document::parse(&set_av_transport_uri("http://h/v?a=1&b=2", &meta)).unwrap();
359 }
360}