2 * Copyright (C) 2010 Nokia Corporation.
4 * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
5 * <zeeshan.ali@nokia.com>
7 * This file is part of Rygel.
9 * Rygel is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU Lesser General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
14 * Rygel is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Lesser General Public License for more details.
19 * You should have received a copy of the GNU Lesser General Public License
20 * along with this program; if not, write to the Free Software Foundation,
21 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27 public errordomain Rygel.TestError {
32 public errordomain Rygel.ClientHacksError {
36 public class Rygel.ClientHacks {
37 public static ClientHacks create (Message? message) throws Error {
38 var headers = message.request_headers;
39 if (headers.get_one ("clienthacks.test.rygel") != null) {
40 return new ClientHacks ();
42 throw new ClientHacksError.NA ("");
46 public void apply (MediaObject? item) {
49 public bool force_seek () {
54 public class Rygel.TestRequestFactory {
55 public Soup.Message msg;
56 public Soup.Status expected_code;
58 public TestRequestFactory (Soup.Message msg,
59 Soup.Status expected_code) {
61 this.expected_code = expected_code;
64 internal HTTPGet create_get (HTTPServer http_server,
67 HTTPGet request = new HTTPGet (http_server, server, msg);
68 request.handler = null;
74 public class Rygel.HTTPGetTest : GLib.Object {
75 protected HTTPServer server;
76 protected HTTPClient client;
78 private bool server_done;
79 private bool client_done;
81 private MainLoop main_loop;
85 private ArrayList<TestRequestFactory> requests;
86 private TestRequestFactory current_request;
88 public static int main (string[] args) {
90 var test = new HTTPGetTest ();
93 } catch (TestError.SKIP error) {
95 } catch (Error error) {
96 critical ("%s", error.message);
101 /* Avoid some warnings about unused methods: */
102 var item = new VideoItem();
103 assert (!item.is_live_stream());
104 assert (!item.streamable());
109 public HTTPGetTest () throws Error {
110 this.server = new HTTPServer ();
111 this.client = new HTTPClient (this.server.context);
112 this.main_loop = new MainLoop (null, false);
113 this.create_test_messages();
116 public virtual void run () throws Error {
117 Timeout.add_seconds (3, this.on_timeout);
118 this.server.message_received.connect (this.on_message_received);
119 this.client.completed.connect (this.on_client_completed);
121 this.start_next_test_request ();
123 this.main_loop.run ();
125 if (this.error != null) {
130 private void create_test_messages () {
131 requests = new ArrayList<TestRequestFactory> ();
133 Soup.Message request = new Soup.Message ("POST", this.server.uri);
134 requests.add (new TestRequestFactory (request,
135 Soup.Status.BAD_REQUEST));
137 request = new Soup.Message ("HEAD", this.server.uri);
138 requests.add (new TestRequestFactory (request, Soup.Status.OK));
140 request = new Soup.Message ("GET", this.server.uri);
141 requests.add (new TestRequestFactory (request, Soup.Status.OK));
143 string uri = this.server.create_uri ("VideoItem");
144 uri = uri + "/tr/MP3";
145 request = new Soup.Message ("HEAD", uri);
146 requests.add (new TestRequestFactory (request, Soup.Status.OK));
148 request = new Soup.Message ("GET", this.server.uri);
149 request.request_headers.append ("transferMode.dlna.org", "Streaming");
150 requests.add (new TestRequestFactory (request, Soup.Status.OK));
152 request = new Soup.Message ("GET", this.server.uri);
153 request.request_headers.append ("transferMode.dlna.org", "Interactive");
154 requests.add (new TestRequestFactory (request,
155 Soup.Status.NOT_ACCEPTABLE));
157 request = new Soup.Message ("GET", this.server.uri);
158 request.request_headers.append ("Range", "bytes=1-2");
159 requests.add (new TestRequestFactory (request,
162 uri = this.server.create_uri ("AudioItem");
165 request = new Soup.Message ("GET", uri);
166 requests.add (new TestRequestFactory (request,
167 Soup.Status.NOT_FOUND));
169 request = new Soup.Message ("GET", this.server.uri);
170 request.request_headers.append ("TimeSeekRange.dlna.org", "0");
171 requests.add (new TestRequestFactory (request,
172 Soup.Status.NOT_ACCEPTABLE));
174 uri = this.server.create_uri ("AudioItem");
175 request = new Soup.Message ("GET", uri);
176 request.request_headers.append ("TimeSeekRange.dlna.org", "0");
177 requests.add (new TestRequestFactory (request,
178 Soup.Status.BAD_REQUEST));
180 uri = this.server.create_uri ("AudioItem");
181 request = new Soup.Message ("GET", uri);
182 request.request_headers.append ("TimeSeekRange.dlna.org", "npt=1-2049");
183 requests.add (new TestRequestFactory (request,
184 Soup.Status.REQUESTED_RANGE_NOT_SATISFIABLE));
186 request = new Soup.Message ("GET", this.server.uri);
187 request.request_headers.append ("clienthacks.test.rygel", "f");
188 requests.add (new TestRequestFactory (request,
191 request = new Soup.Message ("GET", this.server.uri);
192 request.request_headers.append ("clienthacks.test.rygel", "t");
193 requests.add (new TestRequestFactory (request,
197 private HTTPGet create_request (Soup.Message msg) throws Error {
198 HTTPGet request = this.current_request.create_get (this.server,
199 this.server.context.server, msg);
203 private void on_client_completed (StateMachine client) {
204 if (requests.size > 0) {
205 this.start_next_test_request ();
207 this.main_loop.quit ();
208 this.client_done = true;
212 private void start_next_test_request() {
213 this.current_request = requests.remove_at (0);
214 this.client.msg = this.current_request.msg;
215 this.client.run.begin ();
218 private void on_message_received (HTTPServer server,
220 this.handle_client_message.begin (msg);
223 private async void handle_client_message (Soup.Message msg) {
225 var request = this.create_request (msg);
227 yield request.run ();
229 assert ((request as HTTPGet).object != null);
231 debug ("status.code: %d", (int) msg.status_code);
232 assert (msg.status_code == this.current_request.expected_code);
234 if (this.client_done) {
235 this.main_loop.quit ();
238 this.server_done = true;
239 } catch (Error error) {
241 this.main_loop.quit ();
247 private bool on_timeout () {
248 this.error = new TestError.TIMEOUT ("Timeout");
249 this.main_loop.quit ();
255 public class Rygel.HTTPServer : GLib.Object {
256 private const string SERVER_PATH = "/RygelHTTPServer/Rygel/Test";
257 public string path_root {
263 public MediaContainer root_container;
264 public GUPnP.Context context;
268 return create_uri("VideoItem");
272 public string create_uri (string item_id) {
273 var item = new VideoItem ();
276 var item_uri = new HTTPItemURI (item, this);
278 return item_uri.to_string ();
281 public signal void message_received (Soup.Message message);
283 public HTTPServer () throws TestError {
285 this.context = new GUPnP.Context (null, "lo", 0);
286 } catch (Error error) {
287 throw new TestError.SKIP ("Network context not available");
290 assert (this.context != null);
291 assert (this.context.host_ip != null);
292 assert (this.context.port > 0);
294 this.context.server.add_handler (SERVER_PATH, this.server_cb);
296 this.root_container = new MediaContainer ();
299 private void server_cb (Server server,
303 ClientContext client) {
304 this.context.server.pause_message (msg);
305 this.message_received (msg);
308 public Transcoder get_transcoder (string target) throws Error {
309 if (target == "MP3") {
310 return new Transcoder ("mp3");
312 throw new HTTPRequestError.NOT_FOUND (
313 "No transcoder available for target format '%s'",
318 public class Rygel.HTTPClient : GLib.Object, StateMachine {
319 public GUPnP.Context context;
320 public Soup.Message msg;
322 public Cancellable cancellable { get; set; }
324 public HTTPClient (GUPnP.Context context) {
325 this.context = context;
328 public async void run () {
329 SourceFunc run_continue = run.callback;
331 this.context.session.queue_message (this.msg, (session, msg) => {
341 public class Rygel.MediaContainer : Rygel.MediaObject {
343 public async MediaObject? find_object (string item_id,
344 Cancellable? cancellable)
346 SourceFunc find_object_continue = find_object.callback;
348 find_object_continue ();
355 debug ("item id: %s", item_id);
356 if (item_id == "VideoItem") {
357 return new VideoItem ();
358 } else if (item_id == "AudioItem") {
359 return new AudioItem ();
366 internal abstract class Rygel.HTTPGetHandler {
367 public HTTPResponse render_body (HTTPGet get_request) {
368 return new HTTPResponse (get_request);
371 public void add_response_headers (HTTPGet get_request) {}
373 public bool knows_size (HTTPGet request) { return false; }
376 internal class Rygel.HTTPTranscodeHandler : Rygel.HTTPGetHandler {
377 public HTTPTranscodeHandler (Transcoder transcoder,
378 Cancellable cancellable) {}
381 internal class Rygel.HTTPIdentityHandler : Rygel.HTTPGetHandler {
382 public HTTPIdentityHandler (Cancellable cancellable) {}
385 internal class Rygel.HTTPPlaylistHandler : Rygel.HTTPGetHandler {
386 public HTTPPlaylistHandler (string? arg, Cancellable cancellable) {}
388 public static bool is_supported (string? arg) { return true; }
391 public abstract class Rygel.MediaItem : Rygel.MediaObject {
392 public long size = 1024;
393 public ArrayList<string> uris = new ArrayList<string> ();
395 public bool place_holder = false;
397 public bool is_live_stream () {
398 if (this.id == "VideoItem")
404 public bool streamable () {
409 private class Rygel.AudioItem : MediaItem {
410 public int64 duration = 2048;
412 public AudioItem () {
413 this.id = "AudioItem";
417 private interface Rygel.VisualItem : MediaItem {
418 public abstract int width { get; set; }
419 public abstract int height { get; set; }
420 public abstract int color_depth { get; set; }
422 public abstract ArrayList<Thumbnail> thumbnails { get; protected set; }
424 public bool is_live_stream () {
428 public bool streamable () {
433 private class Rygel.VideoItem : AudioItem, VisualItem {
434 public int width { get; set; default = -1; }
435 public int height { get; set; default = -1; }
436 public int color_depth { get; set; default = -1; }
438 private ArrayList<Thumbnail> ts;
440 public VideoItem () {
441 this.id = "VideoItem";
444 public ArrayList<Thumbnail> thumbnails {
446 this.ts = new ArrayList<Thumbnail>();
447 ts.add(new Rygel.Thumbnail());
454 public ArrayList<Subtitle> subtitles = new ArrayList<Subtitle> ();
457 private class Rygel.MusicItem : AudioItem {
458 public Thumbnail album_art;
461 public class Rygel.Thumbnail {
462 public long size = 1024;
463 public string file_extension;
466 public class Rygel.Subtitle {
467 public long size = 1024;
468 public string caption_type;
471 internal class Rygel.HTTPResponse : Rygel.StateMachine, GLib.Object {
472 public Cancellable cancellable { get; set; }
474 private Soup.Message msg;
475 private Soup.Server server;
477 public HTTPResponse (HTTPGet get_request) {
478 this.msg = get_request.msg;
479 this.msg.response_headers.set_encoding (Soup.Encoding.CONTENT_LENGTH);
480 this.server = get_request.server;
483 public async void run () {
484 SourceFunc run_continue = run.callback;
494 this.msg.set_status (Soup.Status.OK);
495 this.server.unpause_message (msg);
501 public class Rygel.MediaObject {
503 public string mime_type = "";
506 public class Rygel.Transcoder : GLib.Object {
507 public string extension { get; protected set; }
509 public Transcoder (string extension) {
510 this.extension = extension;