The Wiert Corner – irregular stream of stuff

Jeroen W. Pluimers on .NET, C#, Delphi, databases, and personal interests

  • My badges

  • Twitter Updates

  • My Flickr Stream

  • Pages

  • All categories

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 1,497 other followers

Some ASX information in order to create a partical XSD schema of it

Posted by jpluimers on 2010/11/03

I tried searching for an XSD or other schema that describes ASX (Advanced Stream Redirector) files, but somehow could not find them.

The ASX files can be used to generate a playlist, but they are a bit richer in content than the WPL (Windows Media Player Playlist) file format.

So here is a post with some references to ASX files, examples, documentation and an XSD you could use for ASX files, and why ASX is not valid XML, it is just well-formed.

Microsoft calls ASX “Windows Media Metafile”, and it is documented in the Windows Media Player 11 SDK documentation.
It can have different extensions (.WAX, .WVX and .ASX) depending on the streams it is supposed to encapsulate (.WMA, .WMV and .ASF), but in practice, .ASX can be used for any stream, including .ASF which now means “Advanced Systems Format” in stead of only “Advanced Streams Format”.
Probably, .WAX and .WVX can be used for other formats as well, I just haven’t tried: according to the extensions list, .ASX just seems to be able to encapsulate the broadest set of formats.

Microsoft documents on how to create a metafile playlist, which basically looks like this:

<ASX version = "3.0">
<!--A simple playlist with entries to be played in sequence.-->
    <Title>The Show Title</Title>
    <Entry>
        <Ref href = "mms://adventure-works.com/Path/title1.wma" />
    </Entry>
    <Entry>
        <Ref href = "mms://adventure-works.com/Path/title2.wma" />
    </Entry>
    <Entry>
        <Ref href = "mms://adventure-works.com/Path/title3.wma" />
    </Entry>
</ASX>

So, an .ASX files contains an ASX element containing a list of ENTRY elements which in turn contains a TITLE and REF element and optionally some more elements like AUHTOR.

From the Microsoft documentation, it is totally unclear if the element names are case sensitive.
Experimenting shows they are not, but the opening and closing tags must be have the same case.
At first sight, that is probably the reason why there is no XSD for it: that would “fix” the case to be sensitive.

But for generating .ASX files it is still convenient to have an XSD to start with, so I generated one.

The <XML for ASP.NET/> site has a handy online XML Schema Generator that accepts XML and infers XSD for it.

It can generate both “Russian Doll Style” XSD (that has all the types inlined) or generate the XSD with “Separate Complex Types” (which often are easier to grasp).

This is some workable XSD that can serve as a base for ASX files that Windows Media Player understands:

<?xml version="1.0" encoding="utf-8"?>
<xsd:schema attributeFormDefault="unqualified" elementFormDefault="qualified" version="1.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsd:element name="asx" type="asxType" />
  <xsd:complexType name="asxType">
    <xsd:sequence>
      <xsd:element name="title" type="xsd:string" />
      <xsd:element maxOccurs="unbounded" name="entry" type="entryType" />
    </xsd:sequence>
    <xsd:attribute name="version" type="xsd:decimal" />
  </xsd:complexType>
  <xsd:complexType name="entryType">
    <xsd:sequence>
      <xsd:element name="title" type="xsd:string" />
      <xsd:element name="ref" type="refType" />
      <xsd:element name="author" type="xsd:string" />
    </xsd:sequence>
  </xsd:complexType>
  <xsd:complexType name="refType">
    <xsd:attribute name="href" type="xsd:string" />
  </xsd:complexType>
</xsd:schema>

Notes:

  1. The XSD omits the XML declaration <?xml version=”1.0″ encoding=”utf-8″?> because of a wordpress bug; some of the XSD might also be gone because of another wordpress bug.
    (Note to self: to circumvent these bugs, save your blog post in “HTML” mode, then do not switch to “Visual” mode after saving).
  2. It could be refined (for instance by limiting the href to xsd:anyURI instead of xsd:string – both definitions from the excellent Relax NG book by Eric van der Vlist), but it gives you a start.

After generating the ASX based on this XSD, Windows Media Player kept complaining that it could not play the XML file.
In the end there were 2 reasons the ASX file was invalid

  1. The ASX file cannot have an XML declaration like <?xml version=”1.0″ encoding=”utf-8″?>.
    So ASX looks like XML, but it is not, and hence this is the true reason there is also no XSD for it: ASX are not XML :-)
    This was easy to fix: just make sure that when you write out the ASX, the XML declaration is not written (alternatively: write out the ASX, then strip the XML declaration)
  2. All ENTRY elements must also contain a REF element.
    Also easy to fix, and caused by an incomplete stream list (not all stream URLs were yet filled in).
    Easy fix too: only write out the records to the ASX that have a REF.

Finally some Delphi XE code, based on the above XSD, that generates the ASX file I wanted.
It converts from a media list in JSON format to an ASX file.

unit NLFMJSON2ASXUnit;

interface

uses
  DBXJSON, ASXUnit, SysUtils, XMLIntf;

type
  TNLFMJSON2ASX = class(TObject)
  private
    function GetJSON(JSONText: string): string;
    procedure ParseInnerJSONObject(JSONObject: TJSONObject; Builder: TStringBuilder; Entries: IXMLEntryTypeList);
    procedure ParseItems(Pair: TJSONPair; Builder: TStringBuilder; Entries: IXMLEntryTypeList);
    procedure SaveASX(ASX: IXMLAsxType; Builder: TStringBuilder);
    function SetOwnerDocumentOptions(const ASX: IXMLAsxType): IXMLDocument;
  public
    function ParseJSON(JSONText: string): TJSONValue;
    function ParseJSONValue(const JSONValue: TJSONValue): string;
  end;

implementation

uses
  Variants, Classes;

function TNLFMJSON2ASX.GetJSON(JSONText: string): string;
const
  ValidStartCharacters = '{'; // '"0123456789{[tfn';
var
  ResultLength: Integer;
  StartPos: Integer;
begin
  Result := JSONText;
  StartPos := 0;
  while True do
  begin
    Inc(StartPos);
    ResultLength := Length(Result);
    if StartPos > ResultLength then
      Exit(NullAsStringValue);
    if Pos(Result[StartPos], ValidStartCharacters) > 0 then
      Exit(Copy(Result, StartPos, ResultLength));
  end;
end;

procedure TNLFMJSON2ASX.ParseInnerJSONObject(JSONObject: TJSONObject; Builder: TStringBuilder; Entries: IXMLEntryTypeList);
var
  Author: string;
  Entry: IXMLEntryType;
  Href: string;
  AlternativeHref: string;
  PairIndex: Integer;
  Pair: TJSONPair;
  PairJsonString: TJSONString;
  PairJSONValue: TJSONValue;
  Title: string;
begin
  for PairIndex := 0 to JSONObject.Size - 1 do
  begin
    Pair := JSONObject.Get(PairIndex);
    PairJSONValue := Pair.JsonValue;
    PairJsonString := Pair.JsonString;

    if PairJSONValue is TJSONString then
    begin
      if PairJsonString.Value = 'o' then
        Title := PairJSONValue.Value
      else if PairJsonString.Value = 'm' then
        Href := PairJSONValue.Value
      else if PairJsonString.Value = 'z' then
        Author := PairJSONValue.Value
      else if PairJsonString.Value = 's' then
        AlternativeHref := PairJSONValue.Value;
    end;
  end;
  Entry := nil;
  if Href = NullAsStringValue then
    Href := AlternativeHref;
  if Title <> NullAsStringValue then
    if Href <> NullAsStringValue then
      if Author <> NullAsStringValue then
      begin
        Entry := Entries.Add();
        Entry.Title := Title;
        Entry.Ref.Href := Href;
        Entry.Author := Author;
      end;
  if not Assigned(Entry) then
  begin
    Builder.Append(JSONObject);
    Builder.AppendLine;
  end;
end;

procedure TNLFMJSON2ASX.ParseItems(Pair: TJSONPair; Builder: TStringBuilder; Entries: IXMLEntryTypeList);
var
  ArrayIndex: Integer;
  JSONArray: TJSONArray;
  PairJSONValue: TJSONValue;
  ArrayJSONValue: TJSONValue;
  JSONObject: TJSONObject;
begin
  PairJSONValue := Pair.JsonValue;
  if PairJSONValue is TJSONArray then
  begin
    JSONArray := TJSONArray(PairJSONValue);
    for ArrayIndex := 0 to JSONArray.Size-1 do
    begin
      ArrayJSONValue := JSONArray.Get(ArrayIndex);
      if ArrayJSONValue is TJSONObject then
      begin
        JSONObject := TJSONObject(ArrayJSONValue);
        ParseInnerJSONObject(JSONObject, Builder, Entries);
      end;
    end;
    Builder.Append(Pair.JsonValue);
  end;
end;

function TNLFMJSON2ASX.ParseJSON(JSONText: string): TJSONValue;
var
  JSONString: string;
begin
  JSONString := GetJSON(JSONText);
  Result := TJSONObject.ParseJSONValue(JSONString);
end;

function TNLFMJSON2ASX.ParseJSONValue(const JSONValue: TJSONValue): string;
var
  Builder: TStringBuilder;
  JSONObject: TJSONObject;
  Pair: TJSONPair;
  PairIndex: Integer;
  ASX: IXMLAsxType;
begin
  Builder := TStringBuilder.Create();
  try
    if JSONValue is TJSONObject then
    begin
      ASX := NewASX;
      SetOwnerDocumentOptions(ASX);
      ASX.Title := 'From NLFM JSON';
      JSONObject := TJSONObject(JSONValue);
      for PairIndex := 0 to JSONObject.Size-1 do
      begin
        Pair := JSONObject.Get(PairIndex);
        Builder.Append(Pair.JsonString);
        Builder.AppendLine;
        if Pair.JsonString.Value = 'items' then
        begin
          ParseItems(Pair, Builder, ASX.Entry);
        end;
      end;
      SaveASX(ASX, Builder);
      Result := Builder.ToString();
    end
    else
      Result := JSONValue.ToString;
  finally
    Builder.Free;
  end;
end;

procedure TNLFMJSON2ASX.SaveASX(ASX: IXMLAsxType; Builder: TStringBuilder);
var
  OwnerDocument: IXMLDocument;
  XMLLines: TStrings;
begin
  Builder.AppendLine;
  XMLLines := TStringList.Create();
  try
    OwnerDocument := SetOwnerDocumentOptions(ASX);
    XMLLines.Assign(OwnerDocument.XML);
    XMLLines.Delete(0); // delete the XML declaration
    Builder.Append(XMLLines.Text);
    XMLLines.SaveToFile('..\..\..\doc\NLFM.ASX');
  finally
    XMLLines.Free;
  end;
end;

function TNLFMJSON2ASX.SetOwnerDocumentOptions(const ASX: IXMLAsxType): IXMLDocument;
begin
  Result := ASX.OwnerDocument;
  Result.Options := Result.Options + [doNodeAutoIndent];
end;

end.

I hope this sheds some light on ASX files and how to write them.

–jeroen

2 Responses to “Some ASX information in order to create a partical XSD schema of it”

  1. “So ASX looks like XML, but it is not, and hence this is the
    true reason there is also no XSD for it: ASX are not XML”

    Not quite. ASX is well-formed XML, it just isn’t *valid* XML. But it is still very definitely XML.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

 
%d bloggers like this: