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 fn seek(target: &str) -> String {
146 format!(
147 r#"<?xml version="1.0" encoding="utf-8"?>
148<s:Envelope
149 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
150 s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
151 <s:Body>
152 <u:Seek xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
153 <InstanceID>0</InstanceID>
154 <Unit>REL_TIME</Unit>
155 <Target>{}</Target>
156 </u:Seek>
157 </s:Body>
158</s:Envelope>
159"#,
160 escape(target)
161 )
162}
163
164pub fn description(friendly_name: &str, uuid: &str) -> String {
166 format!(
167 r#"<?xml version="1.0" encoding="UTF-8"?>
168<root xmlns:dlna="urn:schemas-dlna-org:device-1-0" xmlns="urn:schemas-upnp-org:device-1-0">
169 <specVersion>
170 <major>1</major>
171 <minor>0</minor>
172 </specVersion>
173 <device>
174 <deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
175 <friendlyName>{}</friendlyName>
176 <UDN>uuid:{}</UDN>
177 <manufacturer>mitnk</manufacturer>
178 <modelName>Tiny-Render</modelName>
179 <modelDescription>AVTransport Media Renderer</modelDescription>
180 <dlna:X_DLNADOC xmlns:dlna="urn:schemas-dlna-org:device-1-0">DMR-1.50</dlna:X_DLNADOC>
181 <serviceList>
182 <service>
183 <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
184 <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
185 <controlURL>AVTransport/control</controlURL>
186 <SCPDURL>dlna/AVTransport.xml</SCPDURL>
187 </service>
188 <service>
189 <serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
190 <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
191 <controlURL>RenderingControl/action</controlURL>
192 <eventSubURL>RenderingControl/event</eventSubURL>
193 <SCPDURL>dlna/RenderingControl.xml</SCPDURL>
194 </service>
195 <service>
196 <serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
197 <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
198 <controlURL>ConnectionManager/action</controlURL>
199 <eventSubURL>ConnectionManager/event</eventSubURL>
200 <SCPDURL>dlna/ConnectionManager.xml</SCPDURL>
201 </service>
202 </serviceList>
203 </device>
204</root>"#,
205 escape(friendly_name),
206 escape(uuid),
207 )
208}
209
210pub const SET_AV_TRANSPORT_URI_RESPONSE: &str = r#"
211<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
212 <s:Body>
213 <u:SetAVTransportURIResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
214 </s:Body>
215</s:Envelope>
216"#;
217
218pub const PLAY_RESPONSE: &str = r#"
219<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
220 <s:Body>
221 <u:PlayResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
222 </s:Body>
223</s:Envelope>
224"#;
225
226pub const SEEK_RESPONSE: &str = r#"
227<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
228 <s:Body>
229 <u:SeekResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
230 </s:Body>
231</s:Envelope>
232"#;
233
234pub const STOP_RESPONSE: &str = r#"
235<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
236 <s:Body>
237 <u:StopResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
238 </s:Body>
239</s:Envelope>
240"#;
241
242pub const TRANSPORT_INFO_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:GetTransportInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
246 <CurrentTransportState>PLAYING</CurrentTransportState>
247 <CurrentTransportStatus>OK</CurrentTransportStatus>
248 <CurrentSpeed>1</CurrentSpeed>
249 </u:GetTransportInfoResponse>
250 </s:Body>
251</s:Envelope>
252"#;
253
254pub fn position_info_response(rel_time: &str) -> String {
255 format!(
256 r#"
257<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
258 <s:Body>
259 <u:GetPositionInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
260 <Track>0</Track>
261 <TrackDuration>01:23:45</TrackDuration>
262 <RelTime>{rel_time}</RelTime>
263 <AbsTime>{rel_time}</AbsTime>
264 <RelCount>2147483647</RelCount>
265 <AbsCount>2147483647</AbsCount>
266 </u:GetPositionInfoResponse>
267 </s:Body>
268</s:Envelope>
269"#
270 )
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn escape_roundtrip() {
279 let s = r#"a&b<c>"d"#;
280 assert_eq!(escape(s), "a&b<c>"d");
281 assert_eq!(unescape(&escape(s)), s);
282 }
283
284 #[test]
285 fn unescape_numeric_and_unknown() {
286 assert_eq!(unescape("&<&foo; & x"), "&<&foo; & x");
287 }
288
289 #[test]
290 fn static_documents_parse() {
291 for doc in [
292 AVTRANSPORT_SCPD,
293 RENDERING_CONTROL_SCPD,
294 CONNECTION_MANAGER_SCPD,
295 ] {
296 roxmltree::Document::parse(doc).unwrap();
297 }
298 roxmltree::Document::parse(&description("A & B <x>", "u-1")).unwrap();
299 let meta = didl_metadata("t&t", "http://h/v?a=1&b=2", Some("http://h/s.srt"));
300 roxmltree::Document::parse(&meta).unwrap();
301 roxmltree::Document::parse(&set_av_transport_uri("http://h/v?a=1&b=2", &meta)).unwrap();
302 }
303}