HTTP-Pseudostreaming von Videodateien

Eigentlich lassen sich MP4-Dateien mit H264-Codec nicht über das HTT(P)-Protokoll streamen. Stimmt irgendwie nur teilweise, denn man bekommt es mit viel Mühe und über einige Umwege doch hin!

Dies ist dann aber wiederum mit einigen Einschränkungen verbunden, wie z.B. der fehlenden Möglichkeit im ungebufferten Bereich zu „seeken“.

Doch wie macht Youtube das dann …


Generelle Informationen
Ich verwende folgende in der Tabelle aufgeführten Abkürzungen:

H264 Die für den Standard benutzten FourCCs sind „AVC1“, „DAVC“, „H264“, „X264“ und „VSSH.“ Die Matroska CodecID lautet „V_MPEG4/ISO/AVC“
AAC Steht für Advanced Audio Coding
FLV Ist eine Abkürzung für: Flash Video, ein Containerformat, das vornehmlich für die Internetübertragung von Videoinhalten genutzt wird
MP4 Ist eine Kurzform für den von der MPEG vorgesehenen Container für MPEG-4-Inhalte und wurde in ISO/IEC 14496-12 und -14 (MPEG-4 Teil 12 und 14) standardisiert
HTTP Das Hypertext Transfer Protocol (HTTP, dt. Hypertext-Übertragungsprotokoll) ist ein Protokoll zur Übertragung von Daten über ein Netzwerk. Es wird hauptsächlich eingesetzt, um Webseiten aus dem World Wide Web (WWW) in einen Webbrowser zu laden.


Warum habe ich diese Analyse durchgeführt?
Da im Internet so viele gänzlich unterschiedliche Aussagen zum HTTP-pseudostreaming von H264-kodierten MP4-Dateien zu finden sind und diese häufig darauf schließen lassen, dass dieses Vorhaben „unmöglich“ sei, bzw. möglich aber mit Einschränkungen behaftet (z.B. keine Möglichkeit zum „seeken“ in Dateien) musste ich der Sache selber auf den Grund gehen und ein wenig recherchieren.
Ich bin nämlich der festen Überzeugung, genau so etwas bei Youtube schon mal gesehen zu haben…


Material, Werkzeuge, Quellen
Als Material standen mir eine beliebige Videodatei von meinem Rechner, sowie zwei Videodateien von Youtube (es handelt sich hierbei um ein und dasselbe Video, einmal habe ich es komplett gespeichert und einmal habe ich es gespeichert, nachdem ich ein wenig zum Ende geseeked habe – um die Datei-Header analysieren zu können) zur Verfügung.

Als (Software-) Werkzeuge kamen

• FFmpeg – zur Videokonvertierung
(http://ffmpeg.zeranoe.com/builds/)

• MediaInfo – zur Videoanalyse
(http://mediainfo.sourceforge.net/de)

• mp4UI – zum analysieren von MP4-Dateien
(http://mp4ui.sourceforge.net/)

• VideoLAN – als ultimativer Player für Videodateien
(http://www.videolan.org/)

• flvtool2 – zur Analyse und zum updaten der FLV-Metadaten
(https://github.com/unnu/flvtool2)

• HxD – als Hexeditor für die Dateiheaderanalyse
(http://mh-nexus.de)

• GSpot – ebenfalls zur Analyse der Videodateien
(http://www.headbands.com/gspot/)

• QTIndexSwapper – AIR App – MOOV-Atom relocate in H.264 files
(http://renaun.com/blog/2007/08/22/234/)

zum Einsatz . Alle Arbeiten erledigte ich unter Windows.
Die Quellen aller wichtigen Informationen findet ihr am Ende dieses Artikels.


Ausgangssituation
Ich muss sagen, dass ich von Anfang an sicher war, dass es irgendwie funktionieren MUSS. Warum ich so sicher war? Nach dem ich PHP-Klassen zum injizieren (inject) von FLV-Metadaten (metainject) und zum verschieben (relocate) des MOOV-Atom (moovrelocate) von MP4-Dateien (vom Ende zum Anfang einer Datei) geschrieben habe und somit ein wenig Grundkenntnisse zu den Formaten und deren Aufbau besitze, war mir klar, dass es schon irgendwie funktionieren würde.

Zu dem ersten Weg (der ja direkt erfolgreich war!) gibt es ja einen noch viel besseren, aber vor allem „generischeren“ Weg, den ich in Kürze ein wenig weiter ausbauen möchte – diesen stelle ich dann direkt mit dem PHP-Streamingserver (den ich geschrieben habe) zur Verfügung. Wer jetzt schon gerne mehr dazu erfahren möchte, (dem sei übrigens folgendes schon mal entgegengeworfen: Umrechnung des Sekunden-Offsets in eine Byteposition, „on-the-fly“ generierter MP4-Header inkl. dynamisch generiertem MP4-Metadaten + angefragte Byterange – hier darf man mir auch gerne helfen) darf mich gerne auch per Email (phpfluesterer [ät] googlemail [dott] com) kontaktieren.

In Kürze werde ich übrigens die o.g. Klassen die ich geschrieben habe hier zum Download anbieten. Dabei handelt es sich zum einen um „moovrelocate“ (verschieben des MOOV-Atoms an den Anfang der Datei) und zum anderen um „metainject“ (einfügen von Metadaten in FLV-Dateien). Sollte jemand ganz dringendes Interesse an diesen Klassen haben, kann er mich auch gerne umgehend per Email (phpfluesterer [ät] googlemail [dott] com) kontaktieren.


Die Analyse
Laut Spezifikation sollte ein H264-kodiertes Video in einen MP4-Container eingebettet sein (Audiocodec sollte laut Spezifikation AAC sein, kann aber auch MP3 oder MP4[A] sein). Jetzt kann man entweder davon ausgehen, dass alles andere nicht funktioniert, bzw. es ggf. nur eine vorübergehende oder proprietäre Lösung darstellt.

Also benötigte ich einen Ausgangspunkt um mit der Analyse starten zu können. Als Referenzplattform für Videos und deren Auslieferung kam für mich spontan nur Youtube in Frage und wer könnte glaubwürdigere Informationen zu den unterstützten FLV-Media-Formaten liefern als Adobe?

Also besorgte ich mir die o.g. Videodateien von Youtube und die folgende Spezifikationen von Adobe:

Video File Format Specification Version 9
(http://www.adobe.com/devnet/flv/pdf/video_file_format_spec_v9.pdf)

Überraschend: Laut dieser Spezifikation besteht ganz legitim, ohne irgendwelche Umwege die Möglichkeit folgende „media types“ in einem FLV-Container zu verwenden:

Supported media types (Seite 31)
The following table describes the media types that Flash Player plays back when the media is encapsulated inside an F4V file.

Media type

Comments

GIF A media type of gif (0x67696620) denotes a still frame of video datacompressed using the CompuServe GIF format. The space character,hex0x20, is included to make a complete four-character code.
PNG A media type of png (0x706E6720) denotes a still frame of video datacompressed using the standard PNG format. The space character,hex0x20, is included to make a complete four-character code.
JPEG A media type of jpeg (0x6A706567) denotes a still frame of video datacompressed using the standard JPEG format.
Text A media type of either text (0x74657874) or tx3g (0x74783367)ndicates that the track contains textual data that is made available viaActionScript.
AMF0 A media type of amf0 (0x616D6630) indicates that the track containsdata corresponding to the original version of the ActionScript MessageFormat (AMF)
AMF3 A media type of amf3 (0x616D6633) indicates that the track containsdata corresponding to the ActionScript Message Format (AMF)version 3.
H264 A media type of H264 (0x48323634), h264 (0x68323634), or avc1 (0x61766331) indicates that the track is encoded with H.264 video. Flash Player supports the following H.264 video profiles:

• 0: supported for older media that neglects to set profile

• 66: baseline

• 77: extended

• 88: main

• 100: YUV 4:2:0, 8 bits/sample; a.k.a. “High”

• 110: YUV 4:2:0, 10 bits/sample; a.k.a. “High 10”

• 122: YUV 4:2:2, 10 bits/sample; a.k.a. “High 4:2:2”

• 144: YUV 4:4:4, 12 bits/sample; a.k.a. “High 4:4:4”

MP3 A media type of .mp3 (0x2E6D7033) indicates that the track containsMP3 audio data. The dot character, hex0x2E, is included to make acomplete four-character code.
AAC A media type of mp4a (0x6D703461) indicates that the track isencoded with AAC audio. Flash Player supports the following Aprofiles, denoted by their object types:

• 1: main profile

• 2: low complexity, a.k.a. LC

• 5: high efficiency/scale band replication, a.k.a. HE/SBR


Das könnte also bedeuten, dass diese Formate selbst dann unterstützt werden, wenn diese statt in einem MP4 in einem FLV-Container ausgeliefert werden. Darauf stützt sich somit auch unser Ansatz. Bei den Youtube-Videos die ich daraufhin analysiert habe, bestätigte sich dann auch diese Vermutung. Youtube liefert die HD-Videos ebenfalls in einem FLV-Container aus, der aber bei genauerer Analyse H264 (als Videocodec) und MP4A (als Audiocodec) beinhaltet. Wie wir oben in der Tabelle (orange markiert) sehen konnten, ist MP4A nicht anderes als AAC “…A media type of mp4a (0x6D703461) indicates that the track is encoded with AAC audio…“ Also haben wir hier unsere, dieser Analyse zugrundeliegende, „H264-kodierte (H264-Video + AAC-Audio) Videodatei“.

Öffnet man diese Datei jetzt mit VideoLAN und sieht sich die Codecdetails an, sieht man folgendes:

Abb. 1 VideoLAN Medien-Information

Abb. 1 VideoLAN Medien-Information


Basierend auf diesen Informationen habe ich die von mir verwendete default-commandline (FFmpeg) zur Konvertierung zu MP4 (H264 + AAC) einfach einmal abgeändert, eine Datei konvertiert und bei der Analyse des Ergebnisses folgendes festgestellt:

• GSpot streikt bei der Analyse der generierten Videodatei – genau wie bei den Youtube-Videos (die Weiterentwicklung ist ja auch 2007 eingestellt worden)

• Abändern der Dateiendung der Ausgabedatei von „.mp4“ auf „.flv“ veranlasst FFmpeg dazu das gesamte Material (Video-Stream in H264 und Audio-Stream in AAC) in einen FLV-Container zu schreiben (siehe nächster Punkt).

• Nach einer Analyse dieses Materials mit „flvtool2“ lieferte das folgende Ergebnis:

Abb. 2 flvtool2 Ausgabe

Abb. 2 flvtool2 Ausgabe

• Das sagt mir, dass „flvtool2“ einen FLV-Container gefunden hat und diesen auch lesen kann. Mit den korrekten Werten (siehe rote Markierung) für die verwendeten Codecs (http://help.adobe.com/en_US/FlashMediaServer/3.5_Deving/WS5b3ccc516d4fbf351e63e3d11a0773d56e-7ff6.html)

• Wenn ich das Video in VideoLAN öffne erhalte ich unter Codecdetails zudem die folgenden Informationen:

Abb. 3 VideoLAN Medien-Information (eigene Datei)

Abb. 3 VideoLAN Medien-Information (eigene Datei)

• Nach einem update der MetaDaten mit „flvtool2“ zeigen sich folgende Metadaten/Veränderungen:

Abb. 4 flvtool2 Ausgabe nach Metadaten-Update

Abb. 4 flvtool2 Ausgabe nach Metadaten-Update


Damit man diese ein wenig besser versteht, folgen nun einige grundlegende Informationen zu (Flash/FLV) MetaDaten:

Metadata property name Data type Description
lastkeyframetimestamp Number The timestamp of the last video keyframe recorded.
width Number The width of the video, in pixels.
height Number The height of the video, in pixels.
videodatarate Number The video bit rate.
audiodatarate Number The audio bit rate.
framerate Number The frames per second at which the video was recorded.
creationdate String The creation date of the file.
createdby String The creator of the file.
audiocodecid Number The audio codec ID used in the file. Values are:0 Uncompressed1 ADPCM

2 MP3

5 Nellymoser 8 kHz Mono

6 Nellymoser

10 HE-AAC

11 Speex

videocodecid Number The video codec ID used in the file. Values are:2 Sorenson H.2633 Screen video

4 On2 VP6

5 On2 VP6 with transparency

7 H.264

audiodelay Number The delay introduced by the audio codec, in seconds.
duration Number The length of the file, in seconds.
canSeekToEnd Boolean Whether the last video frame is a keyframe (true if yes, false if no).


Info-Objekte für onMetaData
In der folgenden Tabelle sind die möglichen Werte für Video-Metadaten aufgeführt:

Parameter

Beschreibung

audiocodecid Eine Zahl, die den verwendeten Audio-Codec (Kodier-/Dekodiertechnik) angibt.
audiodatarate Eine Zahl, die die Abtastrate angibt, mit der das Audio kodiert wurde, in Kilobyte pro Sekunde.
audiodelay Eine Zahl, die angibt, bei welcher Zeit in der FLV-Datei der „Zeitpunkt 0″ der ursprünglichen FLV-Datei vorhanden ist. Der Videoinhalt muss leicht verzögert werden, um korrekt mit dem Audio synchronisiert zu werden.
canSeekToEnd Ein boolescher Wert. Die Einstellung true bedeutet, dass die FLV-Datei mit einem Schlüsselbild im letzten Bild kodiert wurde, das den Suchlauf bis zum Ende eines progressiv heruntergeladenen Movieclips ermöglicht. Der Wert lautet false, wenn die FLV-Datei nicht mit einem Schlüsselbild im letzten Bild kodiert wurde.
cuePoints Ein Array von Objekten, eines für jeden in die FLV-Datei eingebetteten Cue-Point. Der Wert ist nicht definiert, wenn die FLV-Datei keine Cue-Points enthält. Jedes Objekt hat die folgenden Eigenschaften:

  • type: Ein String, der den Typ des Cue-Points als „navigation“ oder als „event“ angibt.
  • name: Ein String, der den Namen des Cue-Points angibt.
  • time: Eine Zahl, die die Zeit des Cue-Points in Sekunden mit einer Genauigkeit von drei Dezimalstellen (Millisekunden) angibt.
  • parameters: Ein optionales Objekt mit Name-Wert-Paaren, die vom Benutzer beim Erstellen des Cue-Points festgelegt wurden.
duration Eine Zahl, die die Dauer der FLV-Quelldatei in Sekunden angibt.
framerate Eine Zahl, die die Bildrate der FLV-Datei angibt.
height Eine Zahl, die die Höhe der FLV-Datei in Pixel angibt.
videocodecid Eine Zahl, die die Codec-Version angibt, mit der das Video kodiert wurde.
videodatarate Eine Zahl, die die Video-Datenrate der FLV-Datei angibt.
width Eine Zahl, die die Breite der FLV-Datei in Pixel angibt.


Ein Auszug von FLVMDI (http://www.buraks.com/flvmdi/):

What info does the injected onMetaData event have?
We tried to have all the info that is added by Flash Video Exporter and Flash 8 Video Encoder and some more… The data is added at the start of the file with a timestamp of 00:00:00. If there’s existing metadata, some standard (known) fields will be changed as stated below. If there is Flash 8 cue point information in the metadata, it will be preserved along with any non-standard values (Note that these values are not exported to the XML file). With version 2.9 and the /v switch, you can save the existing onMetaData data present as an XML file.

• duration: (Number) Length of the FLV in seconds. FLVMDI computes this value.

• lasttimestamp: (Number) TimeStamp of the last tag in the FLV file.

(You may think as ‚duration = lasttimestamp + estimated or calculated duration of the last tag‘. This is not exactly true though; FLVMDI calculates/estimates duration for last Audio and Video tags separately and uses the appropriate value for the duration. With version 2.0, FLVMDI is more conservative and duration will be equal to lasttimestamp more than 1.x version).

NetStream.Time will not return the duration, or the actual time as one might expect. It returns the position of the PlayHead which can only be a timestamp in the FLV file (see Position of the PlayHead). So, lasttimestamp value is useful for detecting the end of the FLV more than the duration value.

• lastkeyframetimestamp: (Number) TimeStamp of the last video tag which is a key frame. This info might be needed because seeking a frame after this time usually does not work.

• width: (Number) Width of the video in pixels. (Flash exporter 1.1 sets this to 0).

• height: (Number) Height of the video in pixels. (Flash exporter 1.1 sets this to 0).

• videodatarate: (Number) FLVMDI does not compute this value and imports it if present. (Defaults to 0).

• audiodatarate: (Number) FLVMDI does not compute this value and imports it if present. (Defaults to 0).

• framerate: (Number) FLVMDI computes this value, but uses imported value if not 0.

• creationdate: (String) FLVMDI cannot compute this value and imports it if present. (Defaults to ‚unknown‘).

• filesize: (Number) Filesize in bytes (including the injected data).

• videosize: (Number) Total size of video tags in the file in bytes.

• audiosize: (Number) Total size of audio tags in the file in bytes.

• datasize: (Number) Total size of data tags in the file in bytes.

• metadatacreator: (String) Will be set to ‚Manitu Group FLV MetaData Injector 2‘.

• metadatadate: (Date) Date and time metadata added. (Note that this is not of type string like ‚creationdate‘).

• xtradata: (string) Additional string data if specified.

• videocodecid: (Number) Video codec ID number used in the FLV. (Sorenson H.263 =2, Screen Video =3, On2 VP6 = 4 and 5, Screen Video V2 = 6).

• audiocodecid: (Number) Audio codec ID number used in the FLV. (Uncompressed = 0, ADPCM = 1, MP3 = 2, NellyMoser = 5 and 6).

• audiodelay: (Number) Audio delay in seconds. Flash 8 encoder delays the video for better synch with audio (Audio and video does not start both at time 0, Video starts a bit later). This value is also important for Flash 8 Video Encoder injected Cue Points, because logical time of the cue points does not correspond to physical time they are inserted in the FLV. (Cue points are injected before encoding, when the video is shifted by ‚audio delay‘ seconds, cue points are also shifted and their physical time in the FLV changes).

• canSeekToEnd: (Boolean) True if the last video tag is a key frame and hence can be ’seek’ed.

• keyframes: (Object) This object is added only if you specify the /k switch. ‚keyframes‘ is known to FLVMDI and if /k switch is not specified, ‚keyframes‘ object will be deleted. ‚keyframes‘ object has 2 arrays: ‚filepositions‘ and ‚times‘. Both arrays have the same number of elements, which is equal to the number of key frames in the FLV. Values in times array are in ’seconds‘. Each correspond to the timestamp of the n’th key frame. Values in filepositions array are in ‚bytes‘. Each correspond to the fileposition of the nth key frame video tag (which starts with byte tag type 9).

Analyse der Youtube-Videodateien
Die Youtube-Videos zeigten die gleichen Merkmale, bis auf einige wenige Unterschiede (mal abgesehen von Länge, Bitrate usw.). Die Videodateien haben einige unbekannte Metadaten, welche ich den Spezifikationen nicht finden konnte (z.B. „purl“, „starttime“ usw…). Doch diese werde ich beizeiten mal analysieren.

Vollständige Videodatei:

Abb. 5 flvtool2 Ausgabe Youtube-Datei Nr. 1

Abb. 5 flvtool2 Ausgabe Youtube-Datei Nr. 1

Teil der Videodatei (nach seek):

Abb. 6 flvtool2 Ausgabe Youtube-Datei Nr. 2

Abb. 6 flvtool2 Ausgabe Youtube-Datei Nr. 2

Hier kommt die Vermutung auf, dass die Metadaten „on-the-fly“ geschrieben werden. Diese Fehler sind schließlich in der ersten analysierten Datei nicht vorhanden! Wie sonst sollen diese auf einmal in die Datei kommen?
Und wie man sieht ist „starttime“ auf 178.678 gesetzt. Das entspricht in etwa der Sekundenzahl zu der ich geskipped habe. Auch dieses werde ich beizeiten mal analysieren.


Ergebnis der Analyse
Nach dem ich die Informationen zusammengetragen und die von mir erstellte FLV-Datei mit den Dateien von Youtube verglichen habe, bin ich zu dem Ergebnis gekommen, dass es durchaus möglich ist, einen H264-Videostream kombiniert mit einem AAC-Audiostream in einen FLV-Container einzubetten, welcher dann sogar über das HTT(P)-Protokoll „pseudo“ gestreamed werden kann. Selbst das seeken zu noch nicht geladenen (gebufferten) Bereichen eines Videos ist möglich (für diesen Test habe ich den FlowPlayer [http://flowplayer.org/download/] mit dem Pseudostreaming-Plugin verwendet [http://flowplayer.org/plugins/streaming/pseudostreaming.html]). Damit ist das vor der Analyse gesteckte Ziel definitiv erreicht. Wir sind in der Lage nur mit PHP ohne einen gesonderten Streamingserver Videos in hoher Qualität bei kleiner Dateigröße auszuliefern.


Quelle / Referenzen
Folgende Quellen haben mir bei der Analyse geholfen:
http://xhelmboyx.tripod.com/formats/mp4-layout.txt
http://code.google.com/p/php-reader/
http://atomicparsley.sourceforge.net/mpeg-4files.html
https://raw.githubusercontent.com/bharathwaaj/sandbox/master/Programming%20books/TrickPlay%20related%20ISO%20Docs/H264/c041828_ISO_IEC_14496-12_2005(E).pdf
http://help.adobe.com/en_US/FlashMediaServer/3.5_Deving/WS5b3ccc516d4fbf351e63e3d11a0773d56e-7ff6.html

Software:
FFmpeg (http://ffmpeg.zeranoe.com/builds/)
MediaInfo (http://mediainfo.sourceforge.net/de)
mp4UI (http://mp4ui.sourceforge.net/)
VideoLAN (http://www.videolan.org/)
flvtool2 (https://github.com/unnu/flvtool2)
HxD (http://mh-nexus.de)
GSpot (http://www.headbands.com/gspot/)

Download
Download dieses Artikels im PDF-Format

Tweet about this on TwitterShare on LinkedInShare on FacebookShare on Google+Email this to someone

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.