1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
6 * Moonlight List (moonlight-list@lists.ximian.com)
8 * Copyright 2007 Novell, Inc. (http://www.novell.com)
10 * See the LICENSE file included with the distribution for details.
21 #include "mediaelement.h"
24 #include "mediaplayer.h"
27 * PlaylistParserInternal
30 PlaylistParserInternal::PlaylistParserInternal ()
32 parser
= XML_ParserCreate (NULL
);
37 PlaylistParserInternal::~PlaylistParserInternal ()
39 XML_ParserFree (parser
);
47 PlaylistNode::PlaylistNode (PlaylistEntry
*entry
) : List::Node ()
54 PlaylistNode::~PlaylistNode ()
66 PlaylistEntry::PlaylistEntry (Playlist
*parent
)
67 : EventObject (Type::PLAYLISTENTRY
, false)
69 LOG_PLAYLIST ("PlaylistEntry::PlaylistEntry (%p)\n", parent
);
72 g_return_if_fail (parent
!= NULL
); // should ge a g_warn..., but glib 2.10 doesn't have g_warn.
75 PlaylistEntry::PlaylistEntry (Type::Kind kind
, Playlist
*parent
)
76 : EventObject (kind
, false)
78 LOG_PLAYLIST ("PlaylistEntry::PlaylistEntry (%p)\n", parent
);
81 g_return_if_fail (parent
!= NULL
); // should ge a g_warn..., but glib 2.10 doesn't have g_warn.
85 PlaylistEntry::PlaylistEntry (Type::Kind kind
)
86 : EventObject (kind
, false)
88 LOG_PLAYLIST ("PlaylistEntry::PlaylistEntry ()\n");
94 PlaylistEntry::Dispose ()
96 LOG_PLAYLIST ("PlaylistEntry::Dispose () id: %i media: %i\n", GET_OBJ_ID (this), GET_OBJ_ID (media
));
101 tmp
->RemoveSafeHandlers (this);
102 tmp
->DisposeObject (tmp
);
108 g_free (full_source_name
);
109 full_source_name
= NULL
;
121 g_free (info_target
);
127 if (params
!= NULL
) {
128 g_hash_table_destroy (params
);
132 EventObject::Dispose ();
136 PlaylistEntry::Init (Playlist
*parent
)
138 // Parent might be null
139 this->parent
= parent
;
142 full_source_name
= NULL
;
145 play_when_available
= false;
155 set_values
= (PlaylistKind::Kind
) 0;
161 PlaylistEntry::Initialize (Media
*media
)
163 g_return_if_fail (media
!= NULL
);
164 g_return_if_fail (this->media
== NULL
);
166 media
->AddSafeHandler (Media::OpenCompletedEvent
, OpenCompletedCallback
, this);
167 media
->AddSafeHandler (Media::OpeningEvent
, OpeningCallback
, this);
168 media
->AddSafeHandler (Media::SeekingEvent
, SeekingCallback
, this);
169 media
->AddSafeHandler (Media::SeekCompletedEvent
, SeekCompletedCallback
, this);
170 media
->AddSafeHandler (Media::CurrentStateChangedEvent
, CurrentStateChangedCallback
, this);
171 media
->AddSafeHandler (Media::DownloadProgressChangedEvent
, DownloadProgressChangedCallback
, this);
172 media
->AddSafeHandler (Media::BufferingProgressChangedEvent
, BufferingProgressChangedCallback
, this);
173 media
->AddSafeHandler (Media::MediaErrorEvent
, MediaErrorCallback
, this);
180 PlaylistEntry::InitializeWithStream (ManagedStreamCallbacks
*callbacks
)
183 ManagedStreamSource
*source
;
184 PlaylistRoot
*root
= GetRoot ();
186 g_return_if_fail (callbacks
!= NULL
);
187 g_return_if_fail (root
!= NULL
);
189 media
= new Media (root
);
192 source
= new ManagedStreamSource (media
, callbacks
);
193 media
->Initialize (source
);
194 if (!media
->HasReportedError ())
201 PlaylistEntry::InitializeWithSource (IMediaSource
*source
)
204 PlaylistRoot
*root
= GetRoot ();
206 g_return_if_fail (source
!= NULL
);
207 g_return_if_fail (root
!= NULL
);
209 media
= source
->GetMediaReffed ();
211 g_return_if_fail (media
!= NULL
);
215 media
->Initialize (source
);
216 if (!media
->HasReportedError ())
222 PlaylistEntry::InitializeWithUri (const char *uri
)
225 PlaylistRoot
*root
= GetRoot ();
227 g_return_if_fail (uri
!= NULL
);
228 g_return_if_fail (root
!= NULL
);
230 media
= new Media (root
);
232 media
->Initialize (uri
);
233 if (!media
->HasReportedError ())
239 PlaylistEntry::InitializeWithDownloader (Downloader
*dl
, const char *PartName
)
242 PlaylistRoot
*root
= GetRoot ();
244 g_return_if_fail (dl
!= NULL
);
245 g_return_if_fail (root
!= NULL
);
247 media
= new Media (root
);
249 media
->Initialize (dl
, PartName
);
250 if (!media
->HasReportedError ())
256 PlaylistEntry::InitializeWithDemuxer (IMediaDemuxer
*demuxer
)
259 PlaylistRoot
*root
= GetRoot ();
261 g_return_if_fail (demuxer
!= NULL
);
262 g_return_if_fail (root
!= NULL
);
264 media
= demuxer
->GetMediaReffed ();
266 g_return_if_fail (media
!= NULL
);
269 media
->Initialize (demuxer
);
270 if (!media
->HasReportedError ())
276 PlaylistEntry::OpeningHandler (Media
*media
, EventArgs
*args
)
278 PlaylistRoot
*root
= GetRoot ();
280 LOG_PLAYLIST ("PlaylistEntry::OpeningHandler (%p, %p)\n", media
, args
);
282 g_return_if_fail (root
!= NULL
);
284 root
->Emit (PlaylistRoot::OpeningEvent
, args
);
288 PlaylistEntry::OpenMediaPlayer ()
290 PlaylistRoot
*root
= GetRoot ();
291 MediaPlayer
*mplayer
;
293 g_return_if_fail (opened
== true);
294 g_return_if_fail (root
!= NULL
);
296 mplayer
= GetMediaPlayer ();
297 g_return_if_fail (mplayer
!= NULL
);
299 mplayer
->Open (media
, this);
301 root
->Emit (PlaylistRoot::OpenCompletedEvent
, NULL
);
305 PlaylistEntry::OpenCompletedHandler (Media
*media
, EventArgs
*args
)
307 PlaylistRoot
*root
= GetRoot ();
308 IMediaDemuxer
*demuxer
;
311 LOG_PLAYLIST ("PlaylistEntry::OpenCompletedHandler (%p, %p)\n", media
, args
);
314 g_return_if_fail (media
!= NULL
);
315 g_return_if_fail (root
!= NULL
);
316 g_return_if_fail (parent
!= NULL
);
318 demuxer
= media
->GetDemuxer ();
320 g_return_if_fail (demuxer
!= NULL
);
322 LOG_PLAYLIST ("PlaylistEntry::OpenCompletedHandler (%p, %p) demuxer: %i %s\n", media
, args
, GET_OBJ_ID (demuxer
), demuxer
->GetTypeName ());
324 if (demuxer
->IsPlaylist ()) {
325 playlist
= demuxer
->GetPlaylist ();
327 g_return_if_fail (playlist
!= NULL
);
328 g_return_if_fail (parent
!= NULL
);
330 parent
->ReplaceCurrentEntry (playlist
);
333 if (parent
->GetCurrentEntry () == this) {
336 LOG_PLAYLIST ("PlaylistEntry::OpenCompletedHandler (%p, %p): opened entry in advance, waiting for current entry to finish.\n", media
, args
);
343 PlaylistEntry::SeekingHandler (Media
*media
, EventArgs
*args
)
345 LOG_PLAYLIST ("PlaylistEntry::SeekingHandler (%p, %p)\n", media
, args
);
349 PlaylistEntry::SeekCompletedHandler (Media
*media
, EventArgs
*args
)
351 PlaylistRoot
*root
= GetRoot ();
353 LOG_PLAYLIST ("PlaylistEntry::SeekCompletedHandler (%p, %p)\n", media
, args
);
355 g_return_if_fail (root
!= NULL
);
359 root
->Emit (PlaylistRoot::SeekCompletedEvent
, args
);
363 PlaylistEntry::CurrentStateChangedHandler (Media
*media
, EventArgs
*args
)
365 LOG_PLAYLIST ("PlaylistEntry::CurrentStateChangedHandler (%p, %p)\n", media
, args
);
369 PlaylistEntry::MediaErrorHandler (Media
*media
, ErrorEventArgs
*args
)
371 LOG_PLAYLIST ("PlaylistEntry::MediaErrorHandler (%p, %p): %s '%s'\n", media
, args
, GetFullSourceName (), args
? args
->GetErrorMessage() : "?");
373 g_return_if_fail (parent
!= NULL
);
375 parent
->OnEntryFailed (args
);
379 PlaylistEntry::DownloadProgressChangedHandler (Media
*media
, EventArgs
*args
)
383 LOG_PLAYLIST ("PlaylistEntry::DownloadProgressChanged (%p, %p %.2f). Disposed: %i\n", media
, args
, args
? ((ProgressEventArgs
*) args
)->progress
: -1.0, IsDisposed ());
390 g_return_if_fail (root
!= NULL
);
394 root
->Emit (PlaylistRoot::DownloadProgressChangedEvent
, args
);
398 PlaylistEntry::BufferingProgressChangedHandler (Media
*media
, EventArgs
*args
)
400 PlaylistRoot
*root
= GetRoot ();
402 LOG_PLAYLIST ("PlaylistEntry::BufferingProgressChanged (%p, %p) %.2f\n", media
, args
, args
? ((ProgressEventArgs
*) args
)->progress
: -1.0);
405 return; // this might happen if the media is still buffering and we're in the process of getting cleaned up
409 root
->Emit (PlaylistRoot::BufferingProgressChangedEvent
, args
);
413 PlaylistEntry::Seek (guint64 pts
)
415 LOG_PLAYLIST ("PlaylistEntry::Seek (%" G_GUINT64_FORMAT
")\n", pts
);
417 g_return_if_fail (media
!= NULL
);
419 media
->SeekAsync (pts
);
423 PlaylistEntry::AddParams (const char *name
, const char *value
)
425 char *uppername
= g_ascii_strup (name
, strlen (name
));
426 if (!strcmp (uppername
, "AUTHOR")) {
428 } else if (!strcmp (uppername
, "ABSTRACT")) {
430 } else if (!strcmp (uppername
, "TITLE")) {
432 } else if (!strcmp (uppername
, "COPYRIGHT")) {
433 SetCopyright (value
);
434 } else if (!strcmp (uppername
, "INFOTARGET")) {
435 SetInfoTarget (value
);
436 } else if (!strcmp (uppername
, "INFOURL")) {
440 params
= g_hash_table_new_full (g_str_hash
, g_str_equal
, g_free
, g_free
);
442 if (g_hash_table_lookup (params
, uppername
) == NULL
) {
443 g_hash_table_insert (params
, uppername
, g_strdup (value
));
451 PlaylistEntry::GetBase ()
457 PlaylistEntry::GetBaseInherited ()
462 return parent
->GetBaseInherited ();
467 PlaylistEntry::SetBase (Uri
*base
)
469 // TODO: Haven't been able to make BASE work with SL,
470 // which means that I haven't been able to confirm any behaviour.
471 if (!(set_values
& PlaylistKind::Base
)) {
473 set_values
= (PlaylistKind::Kind
) (set_values
| PlaylistKind::Base
);
480 PlaylistEntry::GetTitle ()
486 PlaylistEntry::SetTitle (const char *title
)
488 if (!(set_values
& PlaylistKind::Title
)) {
489 this->title
= g_strdup (title
);
490 set_values
= (PlaylistKind::Kind
) (set_values
| PlaylistKind::Title
);
495 PlaylistEntry::GetAuthor ()
500 void PlaylistEntry::SetAuthor (const char *author
)
502 if (!(set_values
& PlaylistKind::Author
)) {
503 this->author
= g_strdup (author
);
504 set_values
= (PlaylistKind::Kind
) (set_values
| PlaylistKind::Author
);
509 PlaylistEntry::GetAbstract ()
515 PlaylistEntry::SetAbstract (const char *abstract
)
517 if (!(set_values
& PlaylistKind::Abstract
)) {
518 this->abstract
= g_strdup (abstract
);
519 set_values
= (PlaylistKind::Kind
) (set_values
| PlaylistKind::Abstract
);
524 PlaylistEntry::GetCopyright ()
530 PlaylistEntry::SetCopyright (const char *copyright
)
532 if (!(set_values
& PlaylistKind::Copyright
)) {
533 this->copyright
= g_strdup (copyright
);
534 set_values
= (PlaylistKind::Kind
) (set_values
| PlaylistKind::Copyright
);
539 PlaylistEntry::GetSourceName ()
545 PlaylistEntry::SetSourceName (Uri
*source_name
)
547 if (this->source_name
)
548 delete this->source_name
;
549 this->source_name
= source_name
;
553 PlaylistEntry::GetStartTime ()
559 PlaylistEntry::SetStartTime (TimeSpan start_time
)
561 if (!(set_values
& PlaylistKind::StartTime
)) {
562 this->start_time
= start_time
;
563 set_values
= (PlaylistKind::Kind
) (set_values
| PlaylistKind::StartTime
);
568 PlaylistEntry::GetDuration ()
574 PlaylistEntry::GetInheritedDuration ()
576 if (HasDuration ()) {
577 return GetDuration ();
578 } else if (parent
!= NULL
) {
579 return parent
->GetInheritedDuration ();
586 PlaylistEntry::HasInheritedDuration ()
588 if (HasDuration ()) {
590 } else if (parent
!= NULL
) {
591 return parent
->HasInheritedDuration ();
598 PlaylistEntry::SetDuration (Duration
*duration
)
600 if (!(set_values
& PlaylistKind::Duration
)) {
601 this->duration
= duration
;
602 set_values
= (PlaylistKind::Kind
) (set_values
| PlaylistKind::Duration
);
607 PlaylistEntry::GetInfoTarget ()
613 PlaylistEntry::SetInfoTarget (const char *info_target
)
615 g_free (this->info_target
);
616 this->info_target
= g_strdup (info_target
);
620 PlaylistEntry::GetInfoURL ()
626 PlaylistEntry::SetInfoURL (const char *info_url
)
628 g_free (this->info_url
);
629 this->info_url
= g_strdup (info_url
);
633 PlaylistEntry::GetClientSkip ()
639 PlaylistEntry::SetClientSkip (bool value
)
645 PlaylistEntry::GetElement ()
647 g_return_val_if_fail (parent
!= NULL
, NULL
);
649 return parent
->GetElement ();
653 PlaylistEntry::ClearMedia ()
655 g_return_if_fail (media
!= NULL
);
661 PlaylistEntry::GetMediaPlayer ()
663 PlaylistRoot
*root
= GetRoot ();
665 g_return_val_if_fail (root
!= NULL
, NULL
);
667 return root
->GetMediaPlayer ();
671 add_attribute (MediaAttributeCollection
*attributes
, const char *name
, const char *attr
)
676 MediaAttribute
*attribute
= new MediaAttribute ();
677 attribute
->SetValue (attr
);
678 attribute
->SetName (name
);
680 attributes
->Add (attribute
);
685 add_attribute_glib (const char *name
, const char *value
, MediaAttributeCollection
*attributes
)
687 add_attribute (attributes
, name
, value
);
691 PlaylistEntry::PopulateMediaAttributes ()
693 LOG_PLAYLIST ("PlaylistEntry::PopulateMediaAttributes ()\n");
695 const char *abstract
= NULL
;
696 const char *author
= NULL
;
697 const char *copyright
= NULL
;
698 const char *title
= NULL
;
699 const char *infotarget
= NULL
;
700 const char *infourl
= NULL
;
701 const char *baseurl
= NULL
;
703 MediaElement
*element
= GetElement ();
704 PlaylistEntry
*current
= this;
705 MediaAttributeCollection
*attributes
;
707 g_return_if_fail (element
!= NULL
);
709 if (!(attributes
= element
->GetAttributes ())) {
710 attributes
= new MediaAttributeCollection ();
711 element
->SetAttributes (attributes
);
713 attributes
->Clear ();
716 while (current
!= NULL
) {
717 if (abstract
== NULL
)
718 abstract
= current
->GetAbstract ();
720 author
= current
->GetAuthor ();
721 if (copyright
== NULL
)
722 copyright
= current
->GetCopyright ();
724 title
= current
->GetTitle ();
725 if (infotarget
== NULL
)
726 infotarget
= current
->GetInfoTarget ();
728 infourl
= current
->GetInfoURL ();
729 if (baseurl
== NULL
&& current
->GetBase () != NULL
)
730 baseurl
= current
->GetBase ()->originalString
;
732 current
= current
->GetParent ();
735 add_attribute (attributes
, "ABSTRACT", abstract
);
736 add_attribute (attributes
, "AUTHOR", author
);
737 add_attribute (attributes
, "BaseURL", baseurl
);
738 add_attribute (attributes
, "COPYRIGHT", copyright
);
739 add_attribute (attributes
, "InfoTarget", infotarget
);
740 add_attribute (attributes
, "InfoURL", infourl
);
741 add_attribute (attributes
, "TITLE", title
);
744 while (current
!= NULL
) {
745 if (current
->params
!= NULL
)
746 g_hash_table_foreach (current
->params
, (GHFunc
) add_attribute_glib
, attributes
);
747 current
= current
->GetParent ();
752 PlaylistEntry::GetFullSourceName ()
755 * Now here we have some interesting semantics:
756 * - BASE has to be a complete url, with scheme and domain
757 * - BASE only matters up to the latest / (if no /, the entire BASE is used)
759 * Examples (numbered according to the test-playlist-with-base test in test/media/video)
761 * 01 localhost/dir/ + * = error
762 * 02 /dir/ + * = error
764 * 04 http://localhost/dir/ + somefile = http://localhost/dir/somefile
765 * 05 http://localhost/dir + somefile = http://localhost/somefile
766 * 06 http://localhost + somefile = http://localhost/somefile
767 * 07 http://localhost/dir/ + /somefile = http://localhost/somefile
768 * 08 http://localhost/dir/ + dir2/somefile = http://localhost/dir/dir2/somefile
769 * 09 rtsp://localhost/ + somefile = http://localhost/somefile
770 * 10 mms://localhost/dir/ + somefile = mms://localhost/dir/somefile
771 * 11 http://localhost/?huh + somefile = http://localhost/somefile
772 * 12 http://localhost/#huh + somefile = http://localhost/somefile
773 * 13 httP://localhost/ + somefile = http://localhost/somefile
777 // TODO: url validation, however it should probably happen inside MediaElement when we set the source
779 if (full_source_name
== NULL
) {
780 Uri
*base
= GetBaseInherited ();
781 Uri
*current
= GetSourceName ();
786 //printf ("PlaylistEntry::GetFullSourceName (), base: %s, current: %s\n", base ? base->ToString () : "NULL", current ? current->ToString () : "NULL");
788 if (current
== NULL
) {
790 } else if (current
->GetHost () != NULL
) {
791 //printf (" current host (%s) is something, scheme: %s\n", current->GetHost (), current->scheme);
793 } else if (base
!= NULL
) {
795 result
->scheme
= g_strdup (base
->GetScheme());
796 result
->user
= g_strdup (base
->GetUser());
797 result
->passwd
= g_strdup (base
->GetPasswd());
798 result
->host
= g_strdup (base
->GetHost());
799 result
->port
= base
->GetPort();
800 // we ignore the params, query and fragment values.
801 if (current
->GetPath() != NULL
&& current
->GetPath() [0] == '/') {
802 //printf (" current path is relative to root dir on host\n");
803 result
->path
= g_strdup (current
->GetPath());
804 } else if (base
->GetPath() == NULL
) {
805 //printf (" base path is root dir on host\n");
806 result
->path
= g_strdup (current
->GetPath());
808 pathsep
= strrchr (base
->GetPath(), '/');
809 if (pathsep
!= NULL
) {
810 if ((size_t) (pathsep
- base
->GetPath() + 1) == strlen (base
->GetPath())) {
811 //printf (" last character of base path (%s) is /\n", base->path);
812 result
->path
= g_strjoin (NULL
, base
->GetPath(), current
->GetPath(), NULL
);
814 //printf (" base path (%s) does not end with /, only copy path up to the last /\n", base->path);
815 base_path
= g_strndup (base
->GetPath(), pathsep
- base
->GetPath() + 1);
816 result
->path
= g_strjoin (NULL
, base_path
, current
->GetPath(), NULL
);
820 //printf (" base path (%s) does not contain a /\n", base->path);
821 result
->path
= g_strjoin (NULL
, base
->GetPath(), "/", current
->GetPath(), NULL
);
825 //printf (" there's no base\n");
829 full_source_name
= result
->ToString ();
831 //printf (" result: %s\n", full_source_name);
833 if (result
!= base
&& result
!= current
)
836 return full_source_name
;
840 PlaylistEntry::Open ()
842 LOG_PLAYLIST ("PlaylistEntry::Open (), media = %p, FullSourceName = %s\n", media
, GetFullSourceName ());
845 g_return_if_fail (GetFullSourceName () != NULL
);
846 InitializeWithUri (GetFullSourceName ());
855 PlaylistEntry::Play ()
857 MediaPlayer
*mplayer
= GetMediaPlayer ();
858 PlaylistRoot
*root
= GetRoot ();
860 LOG_PLAYLIST ("PlaylistEntry::Play (), play_when_available: %s, media: %p, source name: %s\n", play_when_available
? "true" : "false", media
, source_name
? source_name
->ToString () : "NULL");
862 g_return_if_fail (media
!= NULL
);
863 g_return_if_fail (mplayer
!= NULL
);
864 g_return_if_fail (root
!= NULL
);
869 root
->Emit (PlaylistRoot::PlayEvent
);
873 PlaylistEntry::Pause ()
875 MediaPlayer
*mplayer
= GetMediaPlayer ();
876 PlaylistRoot
*root
= GetRoot ();
878 LOG_PLAYLIST ("PlaylistEntry::Pause ()\n");
880 g_return_if_fail (media
!= NULL
);
881 g_return_if_fail (mplayer
!= NULL
);
882 g_return_if_fail (root
!= NULL
);
884 play_when_available
= false;
885 media
->PauseAsync ();
888 root
->Emit (PlaylistRoot::PauseEvent
);
892 PlaylistEntry::Stop ()
894 LOG_PLAYLIST ("PlaylistEntry::Stop ()\n");
896 play_when_available
= false;
902 PlaylistEntry::GetMedia ()
908 PlaylistEntry::IsSingleFile ()
910 return parent
? parent
->IsSingleFile () : false;
914 PlaylistEntry::GetRoot ()
921 if (parent
== NULL
) {
922 g_return_val_if_fail (GetObjectType () == Type::PLAYLISTROOT
, NULL
);
923 return (PlaylistRoot
*) this;
928 while (pl
->parent
!= NULL
)
931 g_return_val_if_fail (pl
->GetObjectType () == Type::PLAYLISTROOT
, NULL
);
933 return (PlaylistRoot
*) pl
;
940 Playlist::Playlist (Playlist
*parent
, IMediaSource
*source
)
941 : PlaylistEntry (Type::PLAYLIST
, parent
)
943 is_single_file
= false;
947 this->source
= source
;
948 this->source
->ref ();
951 Playlist::Playlist (Type::Kind kind
)
952 : PlaylistEntry (kind
)
954 LOG_PLAYLIST ("Playlist::Playlist ()\n");
955 is_single_file
= true;
958 AddEntry (new PlaylistEntry (this));
964 LOG_PLAYLIST ("Playlist::Init ()\n");
966 entries
= new List ();
975 PlaylistEntry
*entry
;
977 LOG_PLAYLIST ("Playlist::Dispose () id: %i\n", GET_OBJ_ID (this));
981 if (entries
!= NULL
) {
982 node
= (PlaylistNode
*) entries
->First ();
983 while (node
!= NULL
) {
984 entry
= node
->GetEntry ();
987 node
= (PlaylistNode
*) node
->next
;
998 PlaylistEntry::Dispose ();
1003 Playlist::IsCurrentEntryLastEntry ()
1005 PlaylistEntry
*entry
;
1008 if (entries
->Last () == NULL
)
1011 if (current_node
!= entries
->Last ())
1014 entry
= GetCurrentEntry ();
1016 if (!entry
->IsPlaylist ())
1019 pl
= (Playlist
*) entry
;
1021 return pl
->IsCurrentEntryLastEntry ();
1027 PlaylistEntry
*current_entry
;
1029 LOG_PLAYLIST ("Playlist::Open ()\n");
1031 current_node
= (PlaylistNode
*) entries
->First ();
1033 current_entry
= GetCurrentEntry ();
1035 while (current_entry
&& current_entry
->HasDuration () && current_entry
->GetDuration ()->HasTimeSpan() &&
1036 current_entry
->GetDuration ()->GetTimeSpan () == 0) {
1037 LOG_PLAYLIST ("Playlist::Open (), current entry (%s) has zero duration, skipping it.\n", current_entry
->GetSourceName ()->ToString ());
1038 current_node
= (PlaylistNode
*) current_node
->next
;
1039 current_entry
= GetCurrentEntry ();
1043 current_entry
->Open ();
1047 LOG_PLAYLIST ("Playlist::Open (): current node: %p, current entry: %p\n", current_entry
, GetCurrentEntry ());
1051 Playlist::PlayNext ()
1053 PlaylistEntry
*current_entry
;
1054 MediaElement
*element
= GetElement ();
1055 PlaylistRoot
*root
= GetRoot ();
1057 LOG_PLAYLIST ("Playlist::PlayNext () current_node: %p\n", current_node
);
1058 g_return_val_if_fail (root
!= NULL
, false);
1065 current_entry
= GetCurrentEntry ();
1067 if (current_entry
->HasDuration() && current_entry
->GetDuration()->IsForever ()) {
1068 element
->SetPlayRequested ();
1069 current_entry
->Play ();
1073 if (current_entry
->IsPlaylist ()) {
1074 Playlist
*current_playlist
= (Playlist
*) current_entry
;
1075 if (current_playlist
->PlayNext ())
1079 if (current_node
->next
) {
1080 current_node
= (PlaylistNode
*) current_node
->next
;
1082 current_entry
= GetCurrentEntry ();
1083 if (current_entry
) {
1084 LOG_PLAYLIST ("Playlist::PlayNext () playing entry: %p %s\n", current_entry
, current_entry
->GetFullSourceName ());
1085 element
->SetPlayRequested ();
1086 root
->Emit (PlaylistRoot::EntryChangedEvent
);
1087 current_entry
->Open ();
1092 LOG_PLAYLIST ("Playlist::PlayNext () current_node: %p, nothing to play (is root: %i)\n", current_node
, GetObjectType () == Type::PLAYLISTROOT
);
1094 if (GetObjectType () == Type::PLAYLISTROOT
)
1095 root
->Emit (PlaylistRoot::MediaEndedEvent
);
1101 Playlist::OnEntryEnded ()
1103 LOG_PLAYLIST ("Playlist::OnEntryEnded ()\n");
1108 Playlist::OnEntryFailed (ErrorEventArgs
*args
)
1111 PlaylistRoot
*root
= GetRoot ();
1113 LOG_PLAYLIST ("Playlist::OnEntryFailed () extended_code: %i is_single_file: %i\n", args
? args
->GetExtendedCode() : 0, is_single_file
);
1115 g_return_if_fail (root
!= NULL
);
1117 // media or playlist 404: fatal
1118 // invalid playlist (playlist parsing failed): fatal
1119 // invalid media (gif, swf): play next
1123 // check if we're in a playlist
1124 if (GetMedia () != NULL
&& GetMedia ()->GetDemuxer () != NULL
&& GetMedia ()->GetDemuxer ()->GetObjectType () == Type::ASXDEMUXER
) {
1126 if (args
->GetExtendedCode() == MEDIA_UNKNOWN_CODEC
) {
1132 // we're not a playlist
1140 root
->Emit (PlaylistRoot::MediaErrorEvent
, args
);
1147 Playlist::Seek (guint64 pts
)
1149 PlaylistEntry
*current_entry
;
1151 LOG_PLAYLIST ("Playlist::Seek (%" G_GUINT64_FORMAT
")\n", pts
);
1153 current_entry
= GetCurrentEntry ();
1155 g_return_if_fail (current_entry
!= NULL
);
1157 current_entry
->Seek (pts
);
1163 PlaylistEntry
*current_entry
;
1165 LOG_PLAYLIST ("Playlist::Play ()\n");
1167 current_entry
= GetCurrentEntry ();
1169 g_return_if_fail (current_entry
!= NULL
);
1171 if (current_entry
&& current_entry
->HasDuration () && current_entry
->GetDuration () == 0) {
1172 LOG_PLAYLIST ("Playlist::Open (), current entry (%s) has zero duration, skipping it.\n", current_entry
->GetSourceName ()->ToString ());
1176 current_entry
->Play ();
1183 PlaylistEntry
*current_entry
;
1185 LOG_PLAYLIST ("Playlist::Pause ()\n");
1187 current_entry
= GetCurrentEntry ();
1189 g_return_if_fail (current_entry
!= NULL
);
1191 current_entry
->Pause ();
1199 LOG_PLAYLIST ("Playlist::Stop ()\n");
1201 node
= (PlaylistNode
*) entries
->First ();
1202 current_node
= node
; // reset to first node
1203 while (node
!= NULL
) {
1204 node
->GetEntry ()->Stop ();
1205 node
= (PlaylistNode
*) node
->next
;
1210 Playlist::PopulateMediaAttributes ()
1212 PlaylistEntry
*current_entry
= GetCurrentEntry ();
1214 LOG_PLAYLIST ("Playlist::PopulateMediaAttributes ()\n");
1219 current_entry
->PopulateMediaAttributes ();
1223 Playlist::AddEntry (PlaylistEntry
*entry
)
1227 LOG_PLAYLIST ("Playlist::AddEntry (%p) Count: %i\n", entry
, entries
->Length ());
1229 node
= new PlaylistNode (entry
);
1230 entries
->Append (node
);
1233 if (entries
->Length () == 1) {
1234 g_return_if_fail (current_node
== NULL
);
1235 current_node
= node
;
1240 Playlist::ReplaceCurrentEntry (Playlist
*pl
)
1244 PlaylistEntry
*current_entry
= GetCurrentEntry ();
1246 LOG_PLAYLIST ("Playlist::ReplaceCurrentEntry (%p)\n", pl
);
1248 // check for too nested playlist
1250 PlaylistEntry
*e
= this;
1251 while (e
!= NULL
&& e
->IsPlaylist ()) {
1252 if (e
->GetObjectType () != Type::PLAYLISTROOT
&& e
->GetMedia () != NULL
&& e
->GetMedia ()->GetDemuxer () != NULL
&& e
->GetMedia ()->GetDemuxer ()->GetObjectType () == Type::ASXDEMUXER
)
1254 e
= e
->GetParent ();
1257 ErrorEventArgs
*args
= new ErrorEventArgs (MediaError
,
1258 MoonError (MoonError::EXCEPTION
, 4001, "AG_E_NETWORK_ERROR"));
1259 OnEntryFailed (args
);
1265 if (current_entry
->IsPlaylist ()) {
1266 result
= ((Playlist
*) current_entry
)->ReplaceCurrentEntry (pl
);
1268 PlaylistNode
*pln
= new PlaylistNode (pl
);
1269 pl
->MergeWith (current_entry
);
1270 entries
->InsertBefore (pln
, current_node
);
1271 entries
->Remove (current_node
);
1272 pl
->SetParent (this);
1277 LOG_PLAYLIST ("Playlist::ReplaceCurrentEntrY (%p) [DONE]\n", pl
);
1283 Playlist::MergeWith (PlaylistEntry
*entry
)
1285 LOG_PLAYLIST ("Playlist::MergeWith (%p)\n", entry
);
1287 SetBase (entry
->GetBase () ? new Uri (*entry
->GetBase ()) : NULL
);
1288 SetTitle (entry
->GetTitle ());
1289 SetAuthor (entry
->GetAuthor ());
1290 SetAbstract (entry
->GetAbstract ());
1291 SetCopyright (entry
->GetCopyright ());
1293 SetSourceName (entry
->GetSourceName () ? new Uri (*entry
->GetSourceName ()) : NULL
);
1294 if (entry
->HasDuration ())
1295 SetDuration (entry
->GetDuration ());
1296 Initialize (entry
->GetMedia ());
1297 entry
->ClearMedia ();
1301 Playlist::GetCurrentPlaylistEntry ()
1303 PlaylistEntry
*result
= NULL
;
1306 result
= current_node
->GetEntry () ->GetCurrentPlaylistEntry ();
1314 PlaylistRoot::PlaylistRoot (MediaElement
*element
)
1315 : Playlist (Type::PLAYLISTROOT
)
1317 this->element
= element
;
1319 mplayer
= element
->GetMediaPlayer ();
1320 mplayer
->AddHandler (MediaPlayer::MediaEndedEvent
, MediaEndedCallback
, this);
1321 mplayer
->AddHandler (MediaPlayer::BufferUnderflowEvent
, BufferUnderflowCallback
, this);
1326 PlaylistRoot::Dispose ()
1328 if (mplayer
!= NULL
) {
1329 mplayer
->RemoveAllHandlers (this);
1334 Playlist::Dispose ();
1338 PlaylistRoot::IsSingleFile ()
1340 PlaylistEntry
*entry
;
1342 if (GetCount () != 1)
1345 entry
= GetCurrentEntry ();
1349 if (entry
->GetObjectType () == Type::PLAYLISTENTRY
)
1352 return entry
->IsSingleFile ();
1357 PlaylistEntry::DumpInternal (int tabs
)
1359 printf ("%*s%s %i\n", tabs
, "", GetTypeName (), GET_OBJ_ID (this));
1361 printf ("%*sParent: %p %s\n", tabs
, "", parent
, parent
? parent
->GetTypeName () : NULL
);
1362 printf ("%*sFullSourceName: %s\n", tabs
, "", GetFullSourceName ());
1363 printf ("%*sDuration: %s %.2f seconds\n", tabs
, "", HasDuration () ? "yes" : "no", HasDuration () ? GetDuration ()->ToSecondsFloat () : 0.0);
1364 printf ("%*sMedia: %i %s\n", tabs
, "", GET_OBJ_ID (media
), media
? "" : "(null)");
1366 printf ("%*sUri: %s\n", tabs
, "", media
->GetUri ());
1367 printf ("%*sDemuxer: %i %s\n", tabs
, "", GET_OBJ_ID (media
->GetDemuxer ()), media
->GetDemuxer () ? media
->GetDemuxer ()->GetTypeName () : "N/A");
1368 printf ("%*sSource: %i %s\n", tabs
, "", GET_OBJ_ID (media
->GetSource ()), media
->GetSource () ? media
->GetSource ()->GetTypeName () : "N/A");
1374 Playlist::DumpInternal (int tabs
)
1378 PlaylistEntry::DumpInternal (tabs
);
1379 printf ("%*s %i entries:\n", tabs
, "", entries
->Length ());
1380 node
= (PlaylistNode
*) entries
->First ();
1381 while (node
!= NULL
) {
1382 if (node
== current_node
)
1383 printf ("*%*s * CURRENT NODE *\n", tabs
, "");
1384 node
->GetEntry ()->DumpInternal (tabs
+ 2);
1385 node
= (PlaylistNode
*) node
->next
;
1389 PlaylistRoot::Dump ()
1391 printf ("\n\nDUMP OF PLAYLIST\n\n");
1393 printf ("\n\nDUMP OF PLAYLIST DONE\n\n");
1398 PlaylistRoot::SeekCallback (EventObject
*obj
)
1400 PlaylistRoot
*playlist
= (PlaylistRoot
*) obj
;
1403 LOG_PLAYLIST ("PlaylistRoot::SeekCallback ()\n");
1405 if (playlist
->IsDisposed ())
1408 pts_node
= (PtsNode
*) playlist
->seeks
.First ();
1409 if (pts_node
!= NULL
) {
1410 playlist
->seeks
.Unlink (pts_node
);
1411 playlist
->Seek (pts_node
->pts
);
1417 PlaylistRoot::SeekAsync (guint64 pts
)
1419 LOG_PLAYLIST ("PlaylistRoot::SeekAsync (%" G_GUINT64_FORMAT
")\n", pts
);
1420 seeks
.Append (new PtsNode (pts
));
1421 AddTickCall (SeekCallback
);
1425 PlaylistRoot::PlayCallback (EventObject
*obj
)
1427 LOG_PLAYLIST ("Playlist::PlayCallback ()\n");
1429 PlaylistRoot
*root
= (PlaylistRoot
*) obj
;
1430 if (root
->IsDisposed ())
1436 PlaylistRoot::PlayAsync ()
1438 LOG_PLAYLIST ("Playlist::PlayAsync ()\n");
1439 AddTickCall (PlayCallback
);
1443 PlaylistRoot::PauseCallback (EventObject
*obj
)
1445 LOG_PLAYLIST ("Playlist::PauseCallback ()\n");
1447 PlaylistRoot
*root
= (PlaylistRoot
*) obj
;
1448 if (root
->IsDisposed ())
1454 PlaylistRoot::PauseAsync ()
1456 LOG_PLAYLIST ("Playlist::PauseAsync ()\n");
1457 AddTickCall (PauseCallback
);
1461 PlaylistRoot::OpenCallback (EventObject
*obj
)
1463 LOG_PLAYLIST ("Playlist::OpenCallback ()\n");
1465 PlaylistRoot
*root
= (PlaylistRoot
*) obj
;
1466 if (root
->IsDisposed ())
1472 PlaylistRoot::OpenAsync ()
1474 LOG_PLAYLIST ("Playlist::OpenAsync ()\n");
1475 AddTickCall (OpenCallback
);
1479 PlaylistRoot::StopCallback (EventObject
*obj
)
1481 LOG_PLAYLIST ("Playlist::StopCallback ()\n");
1483 PlaylistRoot
*root
= (PlaylistRoot
*) obj
;
1484 if (root
->IsDisposed ())
1490 PlaylistRoot::StopAsync ()
1492 LOG_PLAYLIST ("Playlist::StopAsync ()\n");
1493 AddTickCall (StopCallback
);
1497 PlaylistRoot::Stop ()
1499 MediaPlayer
*mplayer
;
1501 LOG_PLAYLIST ("PlaylistRoot::Stop ()\n");
1503 mplayer
= GetMediaPlayer ();
1506 if (mplayer
!= NULL
)
1508 // Stop is called async, and if we now emit Open async, we'd possibly not get events in the right order
1509 // example with user code:
1512 // would end up like:
1513 // StopAsync (); -> enqueue Stop
1514 // PlayAsync (); -> enqueue Play
1515 // Stop is called, enqueue Open
1517 Emit (StopEvent
); // we emit the event after enqueuing the Open request, do avoid funky side-effects of event emission.
1521 PlaylistRoot::EmitBufferUnderflowEvent (EventObject
*obj
)
1523 PlaylistRoot
*root
= (PlaylistRoot
*) obj
;
1524 root
->Emit (BufferUnderflowEvent
);
1528 PlaylistRoot::GetMediaPlayer ()
1534 PlaylistRoot::GetCurrentMedia ()
1536 PlaylistEntry
*entry
= GetCurrentEntry ();
1541 return entry
->GetMedia ();
1545 PlaylistRoot::GetElement ()
1551 PlaylistRoot::MediaEndedHandler (MediaPlayer
*mplayer
, EventArgs
*args
)
1553 LOG_PLAYLIST ("PlaylistRoot::MediaEndedHandler (%p, %p)\n", mplayer
, args
);
1557 // Emit (MediaEndedEvent, args);
1561 PlaylistRoot::BufferUnderflowHandler (MediaPlayer
*mplayer
, EventArgs
*args
)
1563 LOG_PLAYLIST ("PlaylistRoot::BufferUnderflowHandler (%p, %p)\n", mplayer
, args
);
1565 if (Surface::InMainThread ()) {
1566 EmitBufferUnderflowEvent (this);
1568 AddTickCall (EmitBufferUnderflowEvent
);
1576 PlaylistParser::PlaylistParser (PlaylistRoot
*root
, IMediaSource
*source
)
1579 this->source
= source
;
1580 this->internal
= NULL
;
1581 this->kind_stack
= NULL
;
1582 this->playlist
= NULL
;
1583 this->current_entry
= NULL
;
1584 this->current_text
= NULL
;
1585 this->error_args
= NULL
;
1589 PlaylistParser::SetSource (IMediaSource
*new_source
)
1593 source
= new_source
;
1599 PlaylistParser::Setup (XmlType type
)
1602 current_entry
= NULL
;
1603 current_text
= NULL
;
1605 was_playlist
= false;
1607 internal
= new PlaylistParserInternal ();
1608 kind_stack
= new List ();
1609 PushCurrentKind (PlaylistKind::Root
);
1611 if (type
== XML_TYPE_ASX3
) {
1612 XML_SetUserData (internal
->parser
, this);
1613 XML_SetElementHandler (internal
->parser
, on_asx_start_element
, on_asx_end_element
);
1614 XML_SetCharacterDataHandler (internal
->parser
, on_asx_text
);
1620 PlaylistParser::Cleanup ()
1623 kind_stack
->Clear (true);
1634 error_args
->unref ();
1639 PlaylistParser::~PlaylistParser ()
1645 str_match (const char *candidate
, const char *tag
)
1647 return g_ascii_strcasecmp (candidate
, tag
) == 0;
1651 PlaylistParser::on_asx_start_element (gpointer user_data
, const char *name
, const char **attrs
)
1653 ((PlaylistParser
*) user_data
)->OnASXStartElement (name
, attrs
);
1657 PlaylistParser::on_asx_end_element (gpointer user_data
, const char *name
)
1659 ((PlaylistParser
*) user_data
)->OnASXEndElement (name
);
1663 PlaylistParser::on_asx_text (gpointer user_data
, const char *data
, int len
)
1665 ((PlaylistParser
*) user_data
)->OnASXText (data
, len
);
1669 is_all_whitespace (const char *str
)
1674 for (int i
= 0; str
[i
] != 0; i
++) {
1689 // To make matters more interesting, the format of the VALUE attribute in the STARTTIME tag isn't
1690 // exactly the same as xaml's or javascript's TimeSpan format.
1692 // The time index, in hours, minutes, seconds, and hundredths of seconds.
1693 // [[hh]:mm]:ss.fract
1695 // The parser seems to stop if it finds a second dot, returnning whatever it had parsed
1698 // At most 4 digits of fract is read, the rest is ignored (even if it's not numbers).
1701 parse_int (const char **pp
, const char *end
, int *result
)
1703 const char *p
= *pp
;
1705 bool success
= false;
1707 while (p
<= end
&& g_ascii_isdigit (*p
)) {
1708 res
= res
* 10 + *p
- '0';
1721 duration_from_asx_str (PlaylistParser
*parser
, const char *str
, Duration
**res
)
1723 const char *end
= str
+ strlen (str
);
1726 int values
[] = {0, 0, 0};
1728 int hh
= 0, mm
= 0, ss
= 0;
1729 int milliseconds
= 0;
1734 if (!g_ascii_isdigit (*p
)) {
1735 parser
->ParsingError (new ErrorEventArgs (MediaError
, MoonError (MoonError::EXCEPTION
, 2210, "AG_E_INVALID_ARGUMENT")));
1739 for (int i
= 0; i
< 3; i
++) {
1740 if (!parse_int (&p
, end
, &values
[i
])) {
1741 parser
->ParsingError (new ErrorEventArgs (MediaError
,
1742 MoonError (MoonError::EXCEPTION
, 2210, "AG_E_INVALID_ARGUMENT")));
1753 while (digits
>= 0 && g_ascii_isdigit (*p
)) {
1754 milliseconds
+= pow (10.0f
, digits
) * (*p
- '0');
1758 if (counter
== 3 && *p
!= 0 && !g_ascii_isdigit (*p
)) {
1759 parser
->ParsingError (new ErrorEventArgs (MediaError
, MoonError (MoonError::EXCEPTION
, 2210, "AG_E_INVALID_ARGUMENT")));
1778 parser
->ParsingError (new ErrorEventArgs (MediaError
, MoonError (MoonError::EXCEPTION
, 2210, "AG_E_INVALID_ARGUMENT")));
1782 gint64 ms
= ((hh
* 3600) + (mm
* 60) + ss
) * 1000 + milliseconds
;
1783 TimeSpan result
= TimeSpan_FromPts (MilliSeconds_ToPts (ms
));
1784 Duration
*duration
= new Duration (result
);
1792 PlaylistParser::OnASXStartElement (const char *name
, const char **attrs
)
1794 PlaylistKind::Kind kind
= StringToKind (name
);
1798 LOG_PLAYLIST ("PlaylistParser::OnStartElement (%s, %p), kind = %d\n", name
, attrs
, kind
);
1800 g_free (current_text
);
1801 current_text
= NULL
;
1803 PushCurrentKind (kind
);
1806 case PlaylistKind::Abstract
:
1807 if (attrs
!= NULL
&& attrs
[0] != NULL
)
1808 ParsingError (new ErrorEventArgs (MediaError
, MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
1810 case PlaylistKind::Asx
:
1811 // Here the kind stack should be: Root+Asx
1812 if (kind_stack
->Length () != 2 || !AssertParentKind (PlaylistKind::Root
)) {
1813 ParsingError (new ErrorEventArgs (MediaError
, MoonError (MoonError::EXCEPTION
, 3008, "ASX parse error")));
1817 playlist
= new Playlist (root
, source
);
1819 for (int i
= 0; attrs
[i
] != NULL
; i
+= 2) {
1820 if (str_match (attrs
[i
], "VERSION")) {
1821 if (str_match (attrs
[i
+1], "3")) {
1822 playlist_version
= 3;
1823 } else if (str_match (attrs
[i
+1], "3.0")) {
1824 playlist_version
= 3;
1826 ParsingError (new ErrorEventArgs (MediaError
,
1827 MoonError (MoonError::EXCEPTION
, 3008, "ASX parse error")));
1829 } else if (str_match (attrs
[i
], "BANNERBAR")) {
1830 ParsingError (new ErrorEventArgs (MediaError
,
1831 MoonError (MoonError::EXCEPTION
, 3007, "Unsupported ASX attribute")));
1832 } else if (str_match (attrs
[i
], "PREVIEWMODE")) {
1833 ParsingError (new ErrorEventArgs (MediaError
,
1834 MoonError (MoonError::EXCEPTION
, 3007, "Unsupported ASX attribute")));
1836 ParsingError (new ErrorEventArgs (MediaError
,
1837 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
1841 case PlaylistKind::Author
:
1842 if (attrs
!= NULL
&& attrs
[0] != NULL
)
1843 ParsingError (new ErrorEventArgs (MediaError
,
1844 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
1846 case PlaylistKind::Banner
:
1847 if (attrs
!= NULL
&& attrs
[0] != NULL
)
1848 ParsingError (new ErrorEventArgs (MediaError
,
1849 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
1851 case PlaylistKind::Base
:
1852 if (!AssertParentKind (PlaylistKind::Asx
| PlaylistKind::Entry
))
1854 for (int i
= 0; attrs
[i
] != NULL
; i
+= 2) {
1855 if (str_match (attrs
[i
], "HREF")) {
1856 // TODO: What do we do with this value?
1857 if (GetCurrentContent () != NULL
) {
1860 if (!uri
->Parse (attrs
[i
+1], true)) {
1862 } else if (uri
->GetScheme() == NULL
) {
1864 } else if (uri
->IsScheme ("http") &&
1865 uri
->IsScheme ("https") &&
1866 uri
->IsScheme ("mms") &&
1867 uri
->IsScheme ("rtsp") &&
1868 uri
->IsScheme ("rstpt")) {
1873 GetCurrentContent ()->SetBase (uri
);
1876 ParsingError (new ErrorEventArgs (MediaError
,
1877 MoonError (MoonError::EXCEPTION
, 4001, "AG_E_NETWORK_ERROR")));
1882 ParsingError (new ErrorEventArgs (MediaError
,
1883 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
1888 case PlaylistKind::Copyright
:
1889 if (attrs
!= NULL
&& attrs
[0] != NULL
)
1890 ParsingError (new ErrorEventArgs (MediaError
,
1891 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
1893 case PlaylistKind::Duration
: {
1895 for (int i
= 0; attrs
[i
] != NULL
; i
+= 2) {
1896 if (str_match (attrs
[i
], "VALUE")) {
1897 if (duration_from_asx_str (this, attrs
[i
+1], &dur
)) {
1898 if (GetCurrentEntry () != NULL
&& GetParentKind () != PlaylistKind::Ref
) {
1899 LOG_PLAYLIST ("PlaylistParser::OnStartElement (%s, %p), found VALUE/timespan = %f s\n", name
, attrs
, TimeSpan_ToSecondsFloat (dur
->GetTimeSpan()));
1900 GetCurrentEntry ()->SetDuration (dur
);
1904 ParsingError (new ErrorEventArgs (MediaError
,
1905 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
1911 case PlaylistKind::Entry
: {
1912 bool client_skip
= true;
1913 for (int i
= 0; attrs
[i
] != NULL
; i
+= 2) {
1914 if (str_match (attrs
[i
], "CLIENTSKIP")) {
1915 // TODO: What do we do with this value?
1916 if (str_match (attrs
[i
+1], "YES")) {
1918 } else if (str_match (attrs
[i
+1], "NO")) {
1919 client_skip
= false;
1921 ParsingError (new ErrorEventArgs (MediaError
,
1922 MoonError (MoonError::EXCEPTION
, 3008, "ASX parse error")));
1925 } else if (str_match (attrs
[i
], "SKIPIFREF")) {
1926 ParsingError (new ErrorEventArgs (MediaError
,
1927 MoonError (MoonError::EXCEPTION
, 3007, "Unsupported ASX attribute")));
1930 ParsingError (new ErrorEventArgs (MediaError
,
1931 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
1935 PlaylistEntry
*entry
= new PlaylistEntry (playlist
);
1936 entry
->SetClientSkip (client_skip
);
1937 playlist
->AddEntry (entry
);
1938 current_entry
= entry
;
1941 case PlaylistKind::EntryRef
: {
1943 for (int i
= 0; attrs
[i
] != NULL
; i
+= 2) {
1944 if (str_match (attrs
[i
], "HREF")) {
1946 href
= g_strdup (attrs
[i
+1]);
1947 // Docs says this attribute isn't unsupported, but an error is emitted.
1948 //} else if (str_match (attrs [i], "CLIENTBIND")) {
1949 // // TODO: What do we do with this value?
1951 ParsingError (new ErrorEventArgs (MediaError
,
1952 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
1959 if (!uri
->Parse (href
)) {
1962 ParsingError (new ErrorEventArgs (MediaError
,
1963 MoonError (MoonError::EXCEPTION
, 1001, "AG_E_UNKNOWN_ERROR")));
1967 PlaylistEntry
*entry
= new PlaylistEntry (playlist
);
1969 entry
->SetSourceName (uri
);
1971 playlist
->AddEntry (entry
);
1972 current_entry
= entry
;
1975 case PlaylistKind::LogUrl
:
1976 if (attrs
!= NULL
&& attrs
[0] != NULL
)
1977 ParsingError (new ErrorEventArgs (MediaError
,
1978 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
1980 case PlaylistKind::MoreInfo
:
1981 for (int i
= 0; attrs
[i
] != NULL
; i
+= 2) {
1982 if (str_match (attrs
[i
], "HREF")) {
1983 if (GetCurrentEntry () != NULL
)
1984 GetCurrentEntry ()->SetInfoURL (attrs
[i
+1]);
1985 } else if (str_match (attrs
[i
], "TARGET")) {
1986 if (GetCurrentEntry () != NULL
)
1987 GetCurrentEntry ()->SetInfoTarget (attrs
[i
+1]);
1989 ParsingError (new ErrorEventArgs (MediaError
,
1990 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
1995 case PlaylistKind::StartTime
: {
1997 for (int i
= 0; attrs
[i
] != NULL
; i
+= 2) {
1998 if (str_match (attrs
[i
], "VALUE")) {
1999 if (duration_from_asx_str (this, attrs
[i
+1], &dur
) && dur
->HasTimeSpan ()) {
2000 if (GetCurrentEntry () != NULL
&& GetParentKind () != PlaylistKind::Ref
) {
2001 LOG_PLAYLIST ("PlaylistParser::OnStartElement (%s, %p), found VALUE/timespan = %f s\n", name
, attrs
, TimeSpan_ToSecondsFloat (dur
->GetTimeSpan()));
2002 GetCurrentEntry ()->SetStartTime (dur
->GetTimeSpan ());
2006 ParsingError (new ErrorEventArgs (MediaError
,
2007 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
2014 case PlaylistKind::Ref
: {
2015 for (int i
= 0; attrs
[i
] != NULL
; i
+= 2) {
2016 if (str_match (attrs
[i
], "HREF")) {
2017 if (GetCurrentEntry () != NULL
&& GetCurrentEntry ()->GetSourceName () == NULL
) {
2019 if (uri
->Parse (attrs
[i
+1])) {
2020 GetCurrentEntry ()->SetSourceName (uri
);
2023 ParsingError (new ErrorEventArgs (MediaError
,
2024 MoonError (MoonError::EXCEPTION
, 1001, "AG_E_UNKNOWN_ERROR")));
2029 ParsingError (new ErrorEventArgs (MediaError
,
2030 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
2036 case PlaylistKind::Param
: {
2037 const char *name
= NULL
;
2038 const char *value
= NULL
;
2040 for (int i
= 0; attrs
[i
] != NULL
; i
+= 2) {
2041 if (str_match (attrs
[i
], "name")) {
2042 name
= attrs
[i
+ 1];
2043 } else if (str_match (attrs
[i
], "value")) {
2044 value
= attrs
[i
+ 1];
2046 ParsingError (new ErrorEventArgs (MediaError
,
2047 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
2051 if (value
!= NULL
&& value
[0] != 0 && name
!= NULL
&& name
[0] != 0) {
2052 PlaylistEntry
*entry
= GetCurrentEntry ();
2057 entry
->AddParams (name
, value
);
2063 case PlaylistKind::Title
:
2064 if (attrs
!= NULL
&& attrs
[0] != NULL
)
2065 ParsingError (new ErrorEventArgs (MediaError
,
2066 MoonError (MoonError::EXCEPTION
, 3005, "Invalid ASX attribute")));
2068 case PlaylistKind::StartMarker
:
2069 case PlaylistKind::EndMarker
:
2070 case PlaylistKind::Repeat
:
2071 case PlaylistKind::Event
:
2072 ParsingError (new ErrorEventArgs (MediaError
,
2073 MoonError (MoonError::EXCEPTION
, 3006, "Unsupported ASX element")));
2075 case PlaylistKind::Root
:
2076 case PlaylistKind::Unknown
:
2078 LOG_PLAYLIST ("PlaylistParser::OnStartElement ('%s', %p): Unknown kind: %d\n", name
, attrs
, kind
);
2079 ParsingError (new ErrorEventArgs (MediaError
,
2080 MoonError (MoonError::EXCEPTION
, 3004, "Invalid ASX element")));
2086 PlaylistParser::OnASXEndElement (const char *name
)
2088 PlaylistKind::Kind kind
= GetCurrentKind ();
2091 LOG_PLAYLIST ("PlaylistParser::OnEndElement (%s), GetCurrentKind (): %d, GetCurrentKind () to string: %s\n", name
, kind
, KindToString (kind
));
2094 case PlaylistKind::Abstract
:
2095 if (!AssertParentKind (PlaylistKind::Asx
| PlaylistKind::Entry
))
2097 if (GetCurrentContent () != NULL
)
2098 GetCurrentContent ()->SetAbstract (current_text
);
2100 case PlaylistKind::Author
:
2101 if (!AssertParentKind (PlaylistKind::Asx
| PlaylistKind::Entry
))
2103 if (GetCurrentContent () != NULL
)
2104 GetCurrentContent ()->SetAuthor (current_text
);
2106 case PlaylistKind::Base
:
2107 if (!AssertParentKind (PlaylistKind::Asx
| PlaylistKind::Entry
))
2110 case PlaylistKind::Copyright
:
2111 if (!AssertParentKind (PlaylistKind::Asx
| PlaylistKind::Entry
))
2113 if (GetCurrentContent () != NULL
)
2114 GetCurrentContent ()->SetCopyright (current_text
);
2116 case PlaylistKind::Duration
:
2117 if (!AssertParentKind (PlaylistKind::Entry
| PlaylistKind::Ref
))
2119 if (current_text
== NULL
)
2121 duration_from_asx_str (this, current_text
, &dur
);
2122 if (GetCurrentEntry () != NULL
)
2123 GetCurrentEntry ()->SetDuration (dur
);
2125 case PlaylistKind::Entry
:
2126 if (!AssertParentKind (PlaylistKind::Asx
))
2128 if (!is_all_whitespace (current_text
)) {
2129 ParsingError (new ErrorEventArgs (MediaError
,
2130 MoonError (MoonError::EXCEPTION
, 3008, "ASX parse error")));
2133 case PlaylistKind::EntryRef
:
2134 if (!AssertParentKind (PlaylistKind::Asx
))
2137 case PlaylistKind::StartTime
:
2138 if (!AssertParentKind (PlaylistKind::Entry
| PlaylistKind::Ref
))
2140 if (!is_all_whitespace (current_text
)) {
2141 ParsingError (new ErrorEventArgs (MediaError
,
2142 MoonError (MoonError::EXCEPTION
, 3008, "ASX parse error")));
2145 case PlaylistKind::Title
:
2146 if (!AssertParentKind (PlaylistKind::Asx
| PlaylistKind::Entry
))
2148 if (GetCurrentContent () != NULL
)
2149 GetCurrentContent ()->SetTitle (current_text
);
2151 case PlaylistKind::Asx
:
2152 if (playlist_version
== 3)
2153 was_playlist
= true;
2154 if (!AssertParentKind (PlaylistKind::Root
))
2157 case PlaylistKind::Ref
:
2158 if (!AssertParentKind (PlaylistKind::Entry
))
2160 if (!is_all_whitespace (current_text
)) {
2161 ParsingError (new ErrorEventArgs (MediaError
,
2162 MoonError (MoonError::EXCEPTION
, 3008, "ASX parse error")));
2165 case PlaylistKind::MoreInfo
:
2166 if (!AssertParentKind (PlaylistKind::Asx
| PlaylistKind::Entry
))
2168 if (!is_all_whitespace (current_text
)) {
2169 ParsingError (new ErrorEventArgs (MediaError
,
2170 MoonError (MoonError::EXCEPTION
, 3008, "ASX parse error")));
2173 case PlaylistKind::Param
:
2174 if (!AssertParentKind (PlaylistKind::Asx
| PlaylistKind::Entry
))
2176 if (!is_all_whitespace (current_text
)) {
2177 ParsingError (new ErrorEventArgs (MediaError
,
2178 MoonError (MoonError::EXCEPTION
, 3008, "ASX parse error")));
2182 LOG_PLAYLIST ("PlaylistParser::OnEndElement ('%s'): Unknown kind %d.\n", name
, kind
);
2183 ParsingError (new ErrorEventArgs (MediaError
,
2184 MoonError (MoonError::EXCEPTION
, 3004, "Invalid ASX element")));
2188 if (current_text
!= NULL
) {
2189 g_free (current_text
);
2190 current_text
= NULL
;
2193 switch (GetCurrentKind ()) {
2194 case PlaylistKind::Entry
:
2204 PlaylistParser::OnASXText (const char *text
, int len
)
2206 char *a
= g_strndup (text
, len
);
2209 char *p
= g_strndup (text
, len
);
2210 for (int i
= 0; p
[i
] != 0; i
++)
2211 if (p
[i
] == 10 || p
[i
] == 13)
2214 LOG_PLAYLIST ("PlaylistParser::OnText (%s, %d)\n", p
, len
);
2218 if (current_text
== NULL
) {
2221 char *b
= g_strconcat (current_text
, a
, NULL
);
2222 g_free (current_text
);
2228 PlaylistParser::Is (IMediaSource
*source
, const char *asx_header
)
2230 bool result
= false;
2231 int asx_header_length
= strlen (asx_header
);
2232 unsigned char buffer
[20];
2235 result
= source
->Peek ((guint8
*) buffer
, asx_header_length
);
2239 // skip any whitespace
2240 unsigned char c
= buffer
[0];
2246 result
= source
->ReadAll ((guint8
*) buffer
, 1);
2252 if (buffer
[1] == 0xbb && buffer
[2] == 0xbf) { // UTF-8 BOM: EF BB BF
2253 result
= source
->ReadAll ((guint8
*) buffer
, 3);
2258 // TODO: there might be other BOMs we should handle too
2262 result
= !g_ascii_strncasecmp ((const char *) buffer
, asx_header
, asx_header_length
);
2269 source
->Seek (0, SEEK_SET
);
2275 PlaylistParser::IsASX3 (IMediaSource
*source
)
2277 return Is (source
, "<ASX");
2281 PlaylistParser::IsASX2 (IMediaSource
*source
)
2283 return Is (source
, "[Reference]");
2287 PlaylistParser::ParseASX2 ()
2289 const int BUFFER_SIZE
= 1024;
2291 char buffer
[BUFFER_SIZE
];
2297 playlist_version
= 2;
2299 bytes_read
= source
->ReadSome (buffer
, BUFFER_SIZE
);
2300 if (bytes_read
< 0) {
2301 LOG_PLAYLIST_WARN ("Could not read asx document for parsing.\n");
2305 key_file
= g_key_file_new ();
2306 if (!g_key_file_load_from_data (key_file
, buffer
, bytes_read
,
2307 G_KEY_FILE_NONE
, NULL
)) {
2308 LOG_PLAYLIST_WARN ("Invalid asx2 document.\n");
2309 g_key_file_free (key_file
);
2313 ref
= g_key_file_get_value (key_file
, "Reference", "Ref1", NULL
);
2315 LOG_PLAYLIST_WARN ("Could not find Ref1 entry in asx2 document.\n");
2316 g_key_file_free (key_file
);
2320 if (!g_str_has_prefix (ref
, "http://") || !g_str_has_suffix (ref
, "MSWMExt=.asf")) {
2321 LOG_PLAYLIST_WARN ("Could not find a valid uri within Ref1 entry in asx2 document.\n");
2323 g_key_file_free (key_file
);
2327 mms_uri
= g_strdup_printf ("mms://%s", strstr (ref
, "http://") + strlen ("http://"));
2329 g_key_file_free (key_file
);
2332 playlist
= new Playlist (root
, source
);
2334 PlaylistEntry
*entry
= new PlaylistEntry (playlist
);
2336 if (uri
->Parse (mms_uri
)) {
2337 entry
->SetSourceName (uri
);
2341 playlist
->AddEntry (entry
);
2342 current_entry
= entry
;
2348 PlaylistParser::TryFixError (gint8
*current_buffer
, int bytes_read
)
2352 if (XML_GetErrorCode (internal
->parser
) != XML_ERROR_INVALID_TOKEN
)
2355 int index
= XML_GetErrorByteIndex (internal
->parser
);
2357 if (index
> bytes_read
)
2360 LOG_PLAYLIST ("Attempting to fix invalid token error index: %d\n", index
);
2362 // OK, so we are going to guess that we are in an attribute here and walk back
2363 // until we hit a control char that should be escaped.
2364 char * escape
= NULL
;
2365 while (index
>= 0) {
2366 switch (current_buffer
[index
]) {
2368 escape
= g_strdup ("&");
2371 escape
= g_strdup ("<");
2374 escape
= g_strdup (">");
2385 LOG_PLAYLIST_WARN ("Unable to find an invalid escape character to fix in ASX: %s.\n", current_buffer
);
2390 int escape_len
= strlen (escape
);
2391 int new_size
= source
->GetSize () + escape_len
- 1;
2392 int patched_size
= internal
->bytes_read
+ bytes_read
+ escape_len
- 1;
2393 gint8
* new_buffer
= (gint8
*) g_malloc (new_size
);
2395 source
->Seek (0, SEEK_SET
);
2396 source
->ReadSome (new_buffer
, internal
->bytes_read
);
2398 memcpy (new_buffer
+ internal
->bytes_read
, current_buffer
, index
);
2399 memcpy (new_buffer
+ internal
->bytes_read
+ index
, escape
, escape_len
);
2400 memcpy (new_buffer
+ internal
->bytes_read
+ index
+ escape_len
, current_buffer
+ index
+ 1, bytes_read
- index
- 1);
2402 source
->Seek (internal
->bytes_read
+ bytes_read
, SEEK_SET
);
2403 source
->ReadSome (new_buffer
+ patched_size
, new_size
- patched_size
);
2405 media
= source
->GetMediaReffed ();
2407 MemorySource
*reparse_source
= new MemorySource (media
, new_buffer
, new_size
);
2408 SetSource (reparse_source
);
2409 reparse_source
->unref ();
2411 internal
->reparse
= true;
2414 // Clear out errors in the old buffer
2415 error_args
->unref ();
2429 PlaylistParser::Parse ()
2432 gint64 last_available_pos
;
2435 LOG_PLAYLIST ("PlaylistParser::Parse ()\n");
2438 // Don't try to parse anything until we have all the data.
2439 if (internal
!= NULL
)
2440 internal
->reparse
= false;
2441 size
= source
->GetSize ();
2442 last_available_pos
= source
->GetLastAvailablePosition ();
2443 if (size
!= -1 && last_available_pos
!= -1 && size
!= last_available_pos
)
2444 return MEDIA_NOT_ENOUGH_DATA
;
2446 if (this->IsASX2 (source
)) {
2447 /* Parse as a asx2 mms file */
2448 Setup (XML_TYPE_NONE
);
2449 result
= this->ParseASX2 ();
2450 } else if (this->IsASX3 (source
)) {
2451 Setup (XML_TYPE_ASX3
);
2452 result
= this->ParseASX3 ();
2456 } while (result
&& internal
->reparse
);
2458 return result
? MEDIA_SUCCESS
: MEDIA_FAIL
;
2462 PlaylistParser::ParseASX3 ()
2467 // asx documents don't tend to be very big, so there's no need for a big buffer
2468 const int BUFFER_SIZE
= 1024;
2471 buffer
= XML_GetBuffer(internal
->parser
, BUFFER_SIZE
);
2472 if (buffer
== NULL
) {
2473 fprintf (stderr
, "Could not allocate memory for asx document parsing.\n");
2477 bytes_read
= source
->ReadSome (buffer
, BUFFER_SIZE
);
2478 if (bytes_read
< 0) {
2479 fprintf (stderr
, "Could not read asx document for parsing.\n");
2483 if (!XML_ParseBuffer (internal
->parser
, bytes_read
, bytes_read
== 0)) {
2484 if (error_args
!= NULL
)
2487 switch (XML_GetErrorCode (internal
->parser
)) {
2488 case XML_ERROR_NO_ELEMENTS
:
2489 ParsingError (new ErrorEventArgs (MediaError
,
2490 MoonError (MoonError::EXCEPTION
, 7000, "unexpected end of input")));
2492 case XML_ERROR_DUPLICATE_ATTRIBUTE
:
2493 ParsingError (new ErrorEventArgs (MediaError
,
2494 MoonError (MoonError::EXCEPTION
, 7031, "wfc: unique attribute spec")));
2496 case XML_ERROR_INVALID_TOKEN
:
2497 // save error args in case the error fixing fails (in which case we want this error, not the error the error fixing caused)
2498 error_args
= new ErrorEventArgs (MediaError
,
2499 MoonError (MoonError::EXCEPTION
, 7007, "quote expected"));
2500 if (TryFixError ((gint8
*) buffer
, bytes_read
))
2504 char *msg
= g_strdup_printf ("%s %d (%d, %d)",
2505 XML_ErrorString (XML_GetErrorCode (internal
->parser
)), (int) XML_GetErrorCode (internal
->parser
),
2506 (int) XML_GetCurrentLineNumber (internal
->parser
), (int) XML_GetCurrentColumnNumber (internal
->parser
));
2507 ParsingError (new ErrorEventArgs (MediaError
,
2508 MoonError (MoonError::EXCEPTION
, 3000, msg
)));
2514 if (bytes_read
== 0)
2517 internal
->bytes_read
+= bytes_read
;
2520 return playlist
!= NULL
;
2524 PlaylistParser::GetCurrentContent ()
2526 if (current_entry
!= NULL
)
2527 return current_entry
;
2533 PlaylistParser::GetCurrentEntry ()
2535 return current_entry
;
2539 PlaylistParser::EndEntry ()
2541 this->current_entry
= NULL
;
2545 PlaylistParser::PushCurrentKind (PlaylistKind::Kind kind
)
2547 kind_stack
->Append (new KindNode (kind
));
2548 LOG_PLAYLIST ("PlaylistParser::Push (%d)\n", kind
);
2552 PlaylistParser::PopCurrentKind ()
2554 LOG_PLAYLIST ("PlaylistParser::PopCurrentKind (), current: %d\n", ((KindNode
*)kind_stack
->Last ())->kind
);
2555 kind_stack
->Remove (kind_stack
->Last ());
2559 PlaylistParser::GetCurrentKind ()
2561 KindNode
*node
= (KindNode
*) kind_stack
->Last ();
2566 PlaylistParser::GetParentKind ()
2568 KindNode
*node
= (KindNode
*) kind_stack
->Last ()->prev
;
2573 PlaylistParser::AssertParentKind (int kind
)
2575 LOG_PLAYLIST ("PlaylistParser::AssertParentKind (%d), GetParentKind: %d, result: %d\n", kind
, GetParentKind (), GetParentKind () & kind
);
2577 if (GetParentKind () & kind
)
2580 ParsingError (new ErrorEventArgs (MediaError
,
2581 MoonError (MoonError::EXCEPTION
, 3008, "ASX parse error")));
2587 PlaylistParser::ParsingError (ErrorEventArgs
*args
)
2589 LOG_PLAYLIST ("PlaylistParser::ParsingError (%s)\n", args
->GetErrorMessage());
2591 XML_StopParser (internal
->parser
, false);
2595 return; // don't overwrite any previous errors.
2597 error_args
= args
; // don't ref, this method is called like this: ParsingError (new ErrorEventArgs (...));, so the caller gives us the ref he has
2601 PlaylistKind
PlaylistParser::playlist_kinds
[] = {
2603 PlaylistKind ("ABSTRACT", PlaylistKind::Abstract
),
2604 PlaylistKind ("ASX", PlaylistKind::Asx
),
2605 PlaylistKind ("ROOT", PlaylistKind::Root
),
2606 PlaylistKind ("AUTHOR", PlaylistKind::Author
),
2607 PlaylistKind ("BANNER", PlaylistKind::Banner
),
2608 PlaylistKind ("BASE", PlaylistKind::Base
),
2609 PlaylistKind ("COPYRIGHT", PlaylistKind::Copyright
),
2610 PlaylistKind ("DURATION", PlaylistKind::Duration
),
2611 PlaylistKind ("ENTRY", PlaylistKind::Entry
),
2612 PlaylistKind ("ENTRYREF", PlaylistKind::EntryRef
),
2613 PlaylistKind ("LOGURL", PlaylistKind::LogUrl
),
2614 PlaylistKind ("MOREINFO", PlaylistKind::MoreInfo
),
2615 PlaylistKind ("REF", PlaylistKind::Ref
),
2616 PlaylistKind ("STARTTIME", PlaylistKind::StartTime
),
2617 PlaylistKind ("TITLE", PlaylistKind::Title
),
2618 PlaylistKind ("STARTMARKER", PlaylistKind::StartMarker
),
2619 PlaylistKind ("REPEAT", PlaylistKind::Repeat
),
2620 PlaylistKind ("ENDMARKER", PlaylistKind::EndMarker
),
2621 PlaylistKind ("PARAM", PlaylistKind::Param
),
2622 PlaylistKind ("EVENT", PlaylistKind::Event
),
2624 PlaylistKind (NULL
, PlaylistKind::Unknown
)
2628 PlaylistParser::StringToKind (const char *str
)
2630 PlaylistKind::Kind kind
= PlaylistKind::Unknown
;
2632 for (int i
= 0; playlist_kinds
[i
].str
!= NULL
; i
++) {
2633 if (str_match (str
, playlist_kinds
[i
].str
)) {
2634 kind
= playlist_kinds
[i
].kind
;
2639 LOG_PLAYLIST ("PlaylistParser::StringToKind ('%s') = %d\n", str
, kind
);
2645 PlaylistParser::KindToString (PlaylistKind::Kind kind
)
2647 const char *result
= NULL
;
2649 for (int i
= 0; playlist_kinds
[i
].str
!= NULL
; i
++) {
2650 if (playlist_kinds
[i
].kind
== kind
) {
2651 result
= playlist_kinds
[i
].str
;
2656 LOG_PLAYLIST ("PlaylistParser::KindToString (%d) = '%s'\n", kind
, result
);