FLV-Cache-Catch-Converter

FLV-CCC-Tutorial von Daniel Schwamm (20.07.2009)

Inhalt

1. FLV-Format offline

Eigentlich kann ich es ja nicht leiden, das dreckige FLV-Format (Flash Video).

1.1. CPU-Fresserei

Das Format an sich, der Codec oder was auch immer (?) ist ... anspruchsvoll. Ohne ordentlich CPU-Power ruckeln die Videos. Navigation darin ist ein Geduldsspiel. Und endet oft im Nirvana - meine Tools fliegen jedenfalls regelmässig ab, wenn viel innerhalb von FLVs herumgesprungen wird.

1.2. Streaming & Online-Navigation

Aber: Die Dinger sind auch klein. Und über in Webseiten integrierte Player (Web-Player) werden FLVs unmittelbar abgespielt, ohne dass man sie zuvor komplett herunterladen muss. Manchmal kann man sogar online in ihnen navigieren. Also z.B. direkt zum Filmende springen. Prima Sache, man kann so gleich zum Wesentlichen kommen.

FLV-Cache-Catch-Converter - YouTube, die Mutter aller FLVs

YouTube: Eine schier unerschöpfliche Quelle an FLV-Movies. Im Gegensatz zu MPGs und AVIs werden Movie-Streams bereits abgespielt, so wie ein kleiner Teil vom Server geladen ist. Die Web-Player erlauben häufig eine Online-Navigation. Bei obigem Video könnte man so gleich schauen, ob Homer es schafft, sich mit der Waffe selbst zu verletzen.

1.3. Wo ist der Speicher-Button?

Dummerweise - und sicher nicht zufällig - erlauben es die wenigsten Web-Player, die FLVs auch auf Festplatte zu speichern. Online gucken ist also okay, aber offline, das sollen wir nicht?

Nö, ich mag offline. Also schaute ich mich nach einem Mittelchen um ...

1.4. Catch from Cache as Cache can

Alles, was man sich so mit dem Browser aus dem Web zieht, wird in dessen Cache zwischengelagert. Glücklicherweise ist dieser temporäre Speicherhort auf Festplatte ausgelagert, also keine reine Memory-Geschichte. Wodurch man ungleich einfacher an ihn dran kommt.

FLV-Cache-Catch-Converter - Cache-Dateien des Internet Explorer

IE-Cache: Zwischenspeicher auf der Festplatte für alle Downloads des Browsers. Egal, ob Movies, Bilder, JavaScript oder HTML-Seiten, hier sammelt sich alles an, was den Weg vom Server zum Client geschafft hat.

Okay, im Cache kann man sie dann auch meistens tatsächlich finden, die FLVs, die man sich (gerade) online betrachtet.

Cache ist aber temporär und dynamisch; da ändert sich ständig etwas, kommt dazu und verschwindet wieder. Files, auf die der Browser aktuell zugreift, deren Download also noch läuft, erkennt man darüber hinaus nicht als solche. Und was in so mancher Cache-Datei steckt, ob nun Bild, Movie oder HTML-Seite, ist auch nicht immer gleich zu erkennen - insbesondere beim Firefox, dessen Cache-Files gänzlich ohne Extensions daherkommen.

Naheliegend also, ein kleines Tool zu basteln, welches einem die FLVs quasi automatisch aus dem Cache fischt. Klar, die Idee ist so trivial, dass schon viele Programmierer darauf gekommen sind. An Cache-Browsern herrscht kein Mangel.

Mein bescheidener Beitrag dazu, der "FLV-Cache-Catch-Converter", hat jedoch ein paar Gimmicks zusätzlich drauf. Wie der Name schon sagt, schnappt er sich nicht nur die FLVs im Cache, sondern konvertiert sie auch gleich noch in ein "besseres" Format, nämlich MPGs (sofern man dies möchte). Zudem erkennt das Tool (teilweise) den Start-Time-Code von etappenweise geladenen FLV-Parts und baut diesen in den Dateinamen ein. Dadurch lassen sich die einzelnen Movie-Stücke später wieder relativ leicht zeitlich korrekt zusammensetzen.

Ach ja, trotz des Namens "FLV-CCC" erkennt das Tool neben dem FLV-Format auch noch zwei weitere Movie-Stream-Formate, nämlich WMV und MP4. Auch diese Datei-Typen werden also im Cache identifiziert und gegebenenfalls in MPGs konvertiert.

1.5. Open Source FFMPEG.EXE

MPGs wurden auf dieser Homepage bereits in einem früheren Tutorial behandelt: siehe Video-Splitter. Die Manipulation oder Generierung von MPGs ist nicht eben einfach. Ebensolches gilt für FLVs; zu deren innerem Aufbau habe ich im Web sogar noch weniger konkrete Information gefunden wie seiner Zeit zu den MPGs. Die Programmierung einer Konvertierungsroutine von FLV nach MPG liess also kein Zuckerschlecken erahnen.

Glücklicherweise gibt es aber Open Source. Darunter auch das Tool FFMPEG.EXE, mit dem man sehr viele verschiedene Movie-Formate bearbeiten kann. Es offeriert dazu eine leistungsfähige API mittels Übergabeparametern. Und da Delphi die Möglichkeit bietet, beliebige EXEs mit Übergabeparametern aufzurufen ...

Wir machen es uns also einfach: Wir konvertieren nicht selbst, sondern lassen konvertieren. Besser als die fähigen Jungs und Mädels vom FFMPEG hätte ich es sowieso nie hinbekommen.

FLV-Cache-Catch-Converter - FFMPEG, der Open Source Movie Converter

2. Mein FLV-Cache-Catch-Converter (FLV-CCC)

2.1. Übersicht

Der Delphi-Source von FLV-CCC gliedert sich in drei Units:

  • "main_u.pas" mit TForm "main_f": Hauptfenster der Applikation. Hierüber werden die Benutzer- und Timer-Ereignisse verwaltet. Ein PageControl bietet dem Anwender zwei Registerseiten an, eine für das Cache-, und eine für das Konvertierungsmanagement.
  • "cache_u.pas": unit zur Kapselung aller cache-bezogenen Funktionen.
  • "convert_u.pas": unit zur Kapselung aller konvertierungsbezogenen Funktionen.

Im folgenden sehen wir uns die drei Units der Reihenfolge nach an.

2.2. Unit "main_u.pas"

2.2.1. Deklarationen

An die unit "main_u.pas" ist die TForm "main_f" gebunden. Sie stellt das einzige Fenster der Applikation dar. Alle allgemeinen Ereignisse werden hier verarbeitet bzw. an die zuständigen Units "cache_u.pas" und "convert_u.pas" durchgereicht.

Die im Programm verwendeten Konstanten sind:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
const
  _cap='FLV-CacheCatchConverter V1.0';
  _inifn='flvccc.ini';

  _dir_work='work\';

  _postfix='+';
  _postfix_work='-';

  _flv_org='#org.flv';
  _flv_err='#err.flv';

  _color_on=clwindowtext;
  _color_off=cl3DDkShadow;

Hier werden der Anwendungstitel, die Initialisierungsdatei und der Arbeitsordner vorgegeben. Die "_postfix"-Konstanten kommen bei der Konvertierung zum Einsatz, um fertige FLVs von gerade in Arbeit befindlichen FLVs unterscheiden zu können. Je nach Ergebnis der Konvertierung werden die "_flv_"-Konstanten verwendet, um an den Originalnamen angehängt zu werden. Aktive Elemente erhalten die Farbe "_color_on", inaktive die Farbe "_color_off".

Die Klassendeklaration des Hauptfensters "main_f" sieht folgendermassen aus:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
type
  Tmain_f = class(TForm)
    pctrl: TPageControl;
    cache_ts: TTabSheet;
    convert_ts: TTabSheet;

    [...]

    procedure FormCreate(Sender: TObject);
    procedure pctrlChange(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure timer_tTimer(Sender: TObject);

    procedure cache_refresh_bClick(Sender: TObject);
    procedure cache_catch_bClick(Sender: TObject);
    procedure cache_delete_bClick(Sender: TObject);

    [...]

    procedure convert_bClick(Sender: TObject);
  private
    { private-Deklarationen }
  public
    { public-Deklarationen }
    homedir:string;

    cache_catch_c:integer;
    cache_check_c:integer;

    convert_err_c,
    convert_mpg_c,
    convert_m1v_c:integer;

    procedure error(s:string);
  end;

Wie man sieht, kommt das Hauptfenster mit sehr wenigen "globalen" Variablen und Funktionen aus. In "homedir" wird der Applikationspfad gesichert, "cache_catch_c" ist ein Zähler für die Anzahl kopierter bzw. verschobener Cache-Files, "cache_check_c" gibt an, wie viele Cache-Files auf "Movie-Stream-Artigkeit" getestet wurden, und die "convert_"-Variablen sind Zähler, die angeben, wie viele MPGs bzw. M1Vs erfolgreich generiert wurden bzw. wie viele Konvertierungen letztlich in die Hose gingen.

2.2.2. Nach dem Start von "FLV-CCC" ...

Schauen wir uns nun den Konstruktor der Hauptform "mainf_" an:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
procedure Tmain_f.FormCreate(Sender: TObject);
var
  inif:tinifile;
  obj:tobject;
begin
  formstyle:=fsstayontop;
  randomize;
  caption:=_cap;
  homedir:=extractfilepath(application.exename);

  //create work directory
  if not directoryexists(homedir+_dir_work) then
    mkdir(homedir+_dir_work);

  //read parameters
  inif:=tinifile.create(homedir+_inifn);
  try
    top:=inif.Readinteger('param','pos_top',100);
    left:=inif.Readinteger('param','pos_left',100);

    [...]
  finally
    inif.free;
  end;

  //set TabSheets
  pctrl.align:=alclient;
  pctrl.ActivePage:=cache_ts;

  cache_catch_c:=0;
  cache_check_c:=0;
  cache_u.init(cache_sg);
  cache_target_chbClick(Sender);

  convert_err_c:=0;
  convert_mpg_c:=0;
  convert_m1v_c:=0;
  convert_flb.align:=alclient;

  convert_flb.mask:='*.flv'+_postfix;
  convert_flb.Directory:=homedir+_dir_work;
  convert_working_p.ParentBackground:=false;
  convert_working_p.color:=clred;

  //autostart for converter?
  obj:=nil;
  if convert_auto_chb.Checked then obj:=sender;
  convert_bClick(obj);

  timer_t.Tag:=-1;
  timer_t.Enabled:=true;
end;

Wir geben der Form den Style "fsstayontop". Dadurch bleibt sie nach dem Start stets im Vordergrund "schweben", über allen anderen Anwendungen. Man kann im Browser also das anvisierte FLV-Movie betrachten und gleichzeitig den Status des zugehörigen Cache-Files im "FLV-CCC" beobachten.

FLV-Cache-Catch-Converter - Das Hauptfenster von FLV-CCC schwebt über dem IE

FLV-CCC stay-on-top: Das Fenster von FLV-CCC "schwebt" über allen anderen Anwendungen. Hier ist es der Internet Explorer, in dem gerade ein FLV-Movie von Rihanna abgespielt wird. Wie man erkennt, wurden ca. 1.6 MB des Films geladen. Der Status ist "BLOCKED", d.h., der Download ist noch nicht abgeschlossen.

Falls der Ordner "_dir_work" nicht im aktuellen Verzeichnis der Applikation existiert, wird er angelegt. In diesem Ordner werden alle aufgespürten FLV-Movies abgelegt. Auch die bereits konvertierten Filme bleiben in diesem Verzeichnis lokalisiert (bekommen jedoch eine andere Extension zugewiesen).

Anschliessend wird das INI-File mit diversen Programmparametern eingelesen. Dort wird z.B. die Position des Fensters sowie seine Breite und Höhe gesichert.

Jetzt folgt die Initialisierung der beiden verwendeten Tab-Sheets - "cache_ts" und "convert_ts" - mit ihren darauf befindlichen Komponenten. So wird z.B. die StringGrid "cache_sg" mit dem aktuellen Cache-Inhalt gefüllt sowie eventuell zu konvertierende FLV-Movies in die zugehörigen FileListBox "convert_flb" eingeladen.

Am Schluss wird noch der Timer "timer_t" aktiviert.

2.2.3. Ein Timer regelt alles

Um den Erstaufruf des Timer-Ereignisses zu kennzeichnen, bekommt das Timer-Tag im Konstruktor den Wert "-1" zugewiesen. Der Timer feuert jede Sekunde sein Ereignis ab und bewirkt dadurch jedes Mal den Aufruf der Prozedur "timer_tTimer":

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
//timer-event every second
procedure Tmain_f.timer_tTimer(Sender: TObject);
var
  s:string;
begin
  //first call?
  if timer_t.Tag=-1 then begin
    //yes, refresh cache
    timer_t.Tag:=0;
    cache_refresh_bClick(Sender);
  end;

  //change timer.tag every second 0<->1
  timer_t.tag:=timer_t.tag+1;
  if timer_t.tag>1 then timer_t.tag:=0;

  //build form.caption
  s:='';
  if convert_working_p.color=cllime then begin
    //converter runs
    if timer_t.tag=0 then s:=s+'Working'
                     else s:=s+' ... ';
  end
  else begin
    //converters inactive: show statistics
    s:=
      s+
      'MPG: '+inttostr(convert_mpg_c)+ ' '+
      'M1V: '+inttostr(convert_m1v_c)+' '+
      'ERR: '+inttostr(convert_err_c);
  end;

  //und form.caption setzen
  caption:=
    _cap+' | '+
    'CHECK: '+inttostr(cache_check_c)+
    ' CATCH: '+inttostr(cache_catch_c)+
    ' | '+
    s;

  //update spooler of converter
  convert_flb.update;

  //converter-spooler active? if not get out!
  if convert_b.caption<>'STOP' then exit;

  //converter running? Get out!
  if convert_working_p.color=cllime then exit;

  //okay, next flv in spooler ready to convert
  thread_spooler:=tthread_spooler.Create;
end;

Am Anfang wird geprüft, ob die Prozedur zum ersten Mal aufgerufen wird. Dies ist direkt nach Programmstart der Fall. Die Cache-StringGrid wird daraufhin aktualisiert. Das kann, je nach Grösse des Caches, recht lange dauern (und wurde alleine aus diesem Grund in das Timer-Ereignis gepackt, denn zu diesem Zeitpunkt ist wenigstens das Hauptfenster von "FLV-CCC" bereits zu sehen, anders als etwa noch im "FormCreate"-Ereignis).

Dann wird das Timer-Tag erhöht - und, sowie es grösser als 1 ist, wieder auf 0 gesetzt. Es wechselt also jede Sekunden seinen Wert von 0 nach 1 nach 0 nach 1 usw.

Findet gerade ein Konvertierungsprozess statt, so nutzen wir dieses alternierende Timer-Tag, um im Fensterkopf von FLV-CCC einen "blinkenden" Text auszugeben. Das signalisiert dem Anwender unmittelbar, dass das Tool gerade (noch) am Schuften ist.

Ist der Konvertierungsspooler dagegen im Standby-Modus, so wird im Fensterkopf stattdessen eine kleine Statistik angezeigt, die Auskunft darüber gibt, wie viele MPGs, M1Vs seit Programmstart bereits erfolgreich generiert wurden - und wie oft die Konvertierung misslang.

Anschliessend wird der Konvertierungsspooler (repräsentiert durch die FileListBox "convert_flb") aktualisiert. Ist er zu diesem Zeitpunkt nicht "scharf Geschaltet", werden keine Konvertierungen durchgeführt und wir verlassen wir die Prozedur wieder.

Ansonsten wird geprüft, ob aktuell ein FLV-Movie in Arbeit ist. Ist dem so, machen wir weiter keinen Stress und beenden die Prozedur ebenso.

Theoretisch wäre es ja möglich, mehrere Movies gleichzeitig zu konvertieren. Solche parallelen Prozesse bergen jedoch mannigfaltige Probleme, etwa konkurrierende Ressourcenzugriffe, was u.U. Geschwindigkeitseinbussen gegenüber sequenziellen Abläufen zu Folge haben kann. Das brauchen wir nicht, das wollen wir nicht, also lassen wir es.

Ist der Konvertierungsspooler frei, dann wird ein neuer Thread erzeugt, der den eventuell vorliegenden Spooler-Inhalt abarbeiten wird. Dazu gleich mehr bei der unit "convert_u.pas".

Damit hätten wir das Hauptfenster auch schon weitgehend abgearbeitet. Die restlichen Funktionen und Ereignisbehandlungen des Hauptfensters sind recht trivial, reichen ihre Arbeit auch weitgehend nur an die Units "cache_u.pas" und "convert_u.pas" durch, daher ersparen wir uns hier eine weitere Analyse.

2.3. Unit "cache_u.pas"

2.3.1. Deklarationen

FLV-Cache-Catch-Converter - TabSheet-register zur Cache-unit

TabSheet-register zur Cache-unit: Hier kann bestimmt werden, welcher Cache verwendet werden soll, der vom IE oder der vom Firefox. Der Cache kann per "Refresh" neu geladen werden. Einträge lassen sich per "Catch" in den Konvertierungsspooler kopieren/verschieben. Über "Delete" können selektierte Cache-Movie-Streams gelöscht werden. Die Checkbox "Use Target Name" erlaubt die Vorgabe eines bestimmten Ziel-Datei-Namens, statt die oft kryptischen Cache-Bezeichnung der originalen Quell-Datei zu verwenden.

In der unit "cache_u.pas" sind alle den Browser-Cache (Internet Explorer und/oder Firefox) Betreffenden Funktionen implementiert. Der Deklarationsteil sieht folgendermassen aus:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
unit cache_u;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
  Forms, StdCtrls, ExtCtrls, Grids, registry;

const
  _registry_shell_folder=
    'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\';

type
  Tc_inx=(
    _cache_file,
    _cache_size,
    _cache_size_format,
    _cache_datetime,
    _cache_state,
    _cache_path,
    _cache_c
  );

Die Konstante "_registry_shell_folder" ist der Basisordner, über den wir später in der Registry die physikalischen Ordnernamen für die Caches von IE und Firefox finden können. Der Typ "tc_inx" dient als index für die Spalten der StringGrid "cache_sg", in die der Cache-Inhalt geladen wird.

Doch beginnen wir von vorne, mit der Initialisierung der Cache-StringGrid:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
procedure init(sg:tstringgrid);
var
  c:integer;
begin
  sg.align:=alclient;
  sg.colcount:=ord(_cache_c);
  sg.rowcount:=1;
  sg.color:=_color_off;
  for c:=0 to sg.colcount-1 do begin
    sg.cells[c,0]:='';
    sg.colwidths[c]:=40;
  end;

  //pfad-spalte verstecken
  sg.colcount:=ord(_cache_c)-1;
end;

Zu Beginn erhält die Cache-StringGrid nur eine Zeile mit einer bestimmten Anzahl Spalten ("sg.colcount:=ord(_cache_c);") einer bestimmten Breite ("sg.colwidths[c]:=40;"), die keinerlei Inhalt enthält.

Die letzte Spalte, die (später) den kompletten Cache-Pfad enthält, wird für den Benutzer ausgeblendet; diese Information verwirrt eher, als dass sie Transparenz schafft.

2.3.2. Füllung der Cache-StringGrid

Um die StringGrid "cache_sg" mit den Namen der Movie-Stream-Inhalte zu füllen, die sich aktuell in einem der beiden Browser-Cache-Ordner befinden, verwenden wir die Funktion "refresh":

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
//Cache-StringGrid mit FLV-Dateien fuellen
//je nach Auswahl: IE-Cache oder Firefox-Cache
function refresh(
  sg:tstringgrid;
  cache_lb,cache_work_lb:tlistbox;
  firefox_ok,sort_size:bool
):integer;
var
  row:integer;
  sg_sl:tstringlist;
  check_c:integer;

  //FLV-Datei-Eintrag in temporaere ListBox vornehmen
  procedure addentry(sr:tsearchrec;dir:string);
  [...]

  //Ordner und Unterordner rekursiv nach Dateien scannen
  procedure dir_scan(dir:string);
  [...]

  //liefer string bis zum naechsten '|' zurueck
  //modifiziere s, sodass es den Rest enthaelt
  function column_next(var s:string):string;
  [..]


var
  s:string;
  c,r:integer;
  dir,fn:string;
  colwidths:array[0..ord(_cache_c)]of integer;
  w:integer;
begin
  screen.cursor:=crhourglass;

  //clean cache
  cache_work_lb.clear;

  //save selected cache-entry
  fn:=
    sg.cells[ord(_cache_path),sg.Row]+
    sg.cells[ord(_cache_file),sg.Row];

  //get cache-directory from browser out of Registry
  if firefox_ok then dir:=reg_firefox_cache_path
                else dir:=reg_ie_cache_path;

  //hide stringgrid
  sg.Visible:=false;

  //stringgrid leeren
  init(sg);

  //counter for checked files
  check_c:=0;

  //create temporary stringlist
  sg_sl:=tstringlist.Create;
  try
    //sort active
    sg_sl.Sorted:=true;

    //fill stringlist with cache-filename plus infos
    dir_scan(dir);

    //put stringliste to stringgrid
    //do it backyard - new or big files are first
    row:=0;
    for r:=sg_sl.count-1 downto 0 do begin

      //Eintrag aus Stringliste holen
      //z.B. 00005000|Test|5000|5,00 kb|090627 16:05|c:\...
      s:=sg_sl[r];

      //ignoriere erste 'Spalte' (Text bis Delimiter '|')
      //Diese Spalte ist nur fuer die korrekte Sortierung
      //noetig gewesen
      column_next(s);

      //filtere Dateinamen aus
      sg.cells[ord(_cache_file),row]:=column_next(s);

      //Filter Groesse in Bytes aus
      sg.cells[ord(_cache_size),row]:=column_next(s);

      [...]

      //erhoehe Zeilenanzahl der StringGrid
      inc(row);
    end;

    //Zeilenanzahl der StringGrid setzen
    if row<1 then row:=1;
    sg.rowcount:=row;

    //noetige breite der spalten berechnen
    //Canvas-Font muss dazu identisch mit StringGrid-Font sein
    sg.Canvas.Font:=sg.Font;

    //Breitentabelle loeschen
    for c:=0 to sg.colCount-1 do colwidths[c]:=0;

    //jede Zelle der StringGrid wird betrachtet
    for r:=0 to sg.RowCount-1 do begin
      for c:=0 to sg.colCount-1 do begin

        //Inhalt der Zelle
        s:=sg.cells[c,r];

        //Breite in Pixel berechnen (plus etwas Puffer)
        w:=sg.Canvas.TextWidth(s)+5;

        //Maximum auf 300 Pixel beschraenken
        if w>300 then w:=300;

        //Breiteste Spalte merken
        if w>colwidths[c] then colwidths[c]:=w;
      end;
    end;

    //Ermittelte Spalten-Breiten setzen
    for c:=0 to sg.colCount-1 do sg.ColWidths[c]:=colwidths[c];

    //Das zu Beginn selektierte File erneut selektieren
    //(es kann ja inzwischen Position gewechselt haben)
    row:=0;
    for r:=1 to sg.RowCount-1 do begin
      if
        sg.cells[ord(_cache_path),r]+
        sg.cells[ord(_cache_file),r]
        <>
        fn
      then continue;
      row:=r;
      break;
    end;
    sg.row:=row;
  finally
    //Aufraeumen
    sg.Visible:=true;
    sg_sl.free;

    //gepruefte Cache-Files merken
    cache_lb.clear;
    for r:=0 to cache_work_lb.items.count-1 do begin
      cache_lb.items.add(cache_work_lb.items[r]);
    end;

    result:=check_c;

    screen.cursor:=crdefault;
  end;
end;

An die Prozedur wird die Cache-StringGrid übergeben, in die die gefundenen Cache-Inhalte dann eingetragen werden. Ausserdem zwei ListBoxen, die zur Optimierung des Scan-Vorgangs eingesetzt werden. Die boolesche Variable "firefox_ok" gibt an, ob wir den Cache des Firefox-Browsers oder den des Internet Explorers analysieren. "sort_size" teilt der Funktion mit, nach welchem Kriterium die StringGrid sortiert werden soll: Nach der Grösse oder nach dem Dateidatum.

Das Füllen der StringGrid läuft so ab:
  • Die ListBox "cache_work_lb" wird gelöscht
  • Wir merken uns den gerade selektierten Eintrag in der StringGrid in "fn"
  • In "dir" wird der Pfad zum gewählten Browser-Cache festgehalten
  • Es wird eine temporäre Stringlist "sg_sl" erzeugt.
  • Über die interne Prozedur "scan_dir" wird die "sg_sl" mit bestimmten Inhalten aus dem gewähltem Cache gefüllt - eben mit diverse Infos (Name, Grösse, Datum ...) zu den darin befindlichen Movie-Streams.
  • Die "sg_sl" wird automatisch in gewünschter Weise sortiert, indem die erste "Spalte" eines jeden Eintrags das Sortierkriterium enthält.
  • Die "sg_sl" wird in die Cache-StringGrid "sg" übertragen.
  • Jetzt wird "sg" durchlaufen und dabei jeweils der breiteste Eintrag je Spalte im array "colwidths" ermittelt.
  • Die Spaltenbreiten werden gemäss "colwidths" gesetzt, sodass die Spalten möglichst den kompletten Zellen-Text anzeigen können. Um keine zu breiten Spalten zu bekommen, liegt hier aber das Maximum bei 300 Pixeln.
  • Die "sg" wird nach dem zuvor gemerkten markierten Cache-Eintrag "fn" durchsucht. Ist er noch vorhanden, so wird er erneut selektiert.
  • Die "sg_sl" wird freigeben.
  • Die ListBox "cache_work_sl" wurde während des Scan-Vorgangs mit den Namen aller geprüften Cache-Dateien gefüllt. Ihr Inhalt wird nun auf die "cache_sl" übertragen - all diese Files müssen beim nächsten Durchgang nicht noch einmal überprüft werden.

FLV-Cache-Catch-Converter - Alle Movies im Cache von Firefox in der StringGrid

Alle Movies vom Firefox in der Cache-StringGrid: Hier wurden im Cache des Firefox fünf Movie-Streams gefunden. Der erste wurde zu Testzwecken manuell hineinkopiert und besitzt daher als einziges Movie einen halbwegs sinnvollen Namen plus Extension. Die StringGrid ist nach Datum sortiert, sodass die neuesten Einträge vorne stehen. Alle Movies haben den Status "READY", ihr Download ist also komplett abgeschlossen. Im Fenstertitel sieht man anhand der Zahl hinter "Check", dass beim letzten Refresh insgesamt 98 Cache-Dateien auf Movie-Stream-Artigkeit überprüft wurden.

2.3.3. Wo sind die Browser-Cache-Ordner?

Die physikalischen Pfad auf die Cache-Ordner von IE und Firefox liefern uns die beiden folgenden Funktionen:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
//liefert registry path auf IE-Cache zurueck
function reg_ie_cache_path:string;
var
  reg:TRegistry;
  s:string;
begin
  result:='';
  reg:=TRegistry.Create;
  try
    reg.RootKey:=HKEY_CURRENT_USER;
    if not reg.OpenKey(_registry_shell_folder,false) then exit;
    s:=reg.ReadString('Cache');
    if copy(s,Length(s)-1,1)<>'\' then s:=s+'\';
    result:=s;
  finally
    reg.Free;
  end;
end;

//liefert registry path auf Firefox-Cache zurueck
function reg_firefox_cache_path:string;
var
  reg:TRegistry;
  s:string;
begin
  result:='';
  reg:=TRegistry.Create;
  try
    reg.RootKey:=HKEY_CURRENT_USER;
    if not reg.OpenKey(_registry_shell_folder,false) then exit;
    s:=reg.ReadString('Local AppData');
    if copy(s,Length(s)-1,1)<>'\' then s:=s+'\';
    s:=s+'mozilla/firefox/profiles/';
    result:=s;
  finally
    reg.Free;
  end;
end;

Insbesondere das Aufspüren des FF-Caches auf der Festplatte war nicht ganz einfach für mich. Obige Prozedur funktioniert dann auch (vermutlich) nur bei dessen Normal-Version, nicht jedoch beim Firefox Portable. Habe ich aber nicht ausprobiert.

2.3.4. Der Scan-Vorgang

Das eigentliche Auffinden aller Cache-Files geschieht über die interne Prozedur "scan_dir", die in "refresh" aufgerufen wird, der wir den eben ermittelten "Cache-Root-Ordner" übergeben:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
//Ordner und Unterordner rekursiv nach Dateien scannen
procedure dir_scan(dir:string);
var
  sr:tsearchrec;
  fn,dir_chk:string;
begin
  application.processmessages;
  if dir[length(dir)]<>'\' then dir:=dir+'\';

  //ordner rekursiv
  if FindFirst(dir+'*.*',faanyfile,sr)=0 then begin
    repeat
      if (sr.Attr and fadirectory)>0 then begin
        if(sr.Name<>'.')and(sr.name<>'..')then begin
          dir_scan(dir+sr.Name+'\');
        end;
      end;
    until FindNext(sr)<>0;
    FindClose(sr);
  end;

  //Ordnernamen normiert
  dir_chk:=ansilowercase(copy(dir,1,length(dir)-1));

  //bin ich in einem korrekten Ordner?
  if firefox_ok then begin
    //Firefox-Cache
    //bin ich im richtigen Ordner gelandet?
    if pos('\cache',dir_chk)=0 then exit;
  end
  else begin
    //Internet Explorer-Cache
    //bin ich unterhalb vom Content-IE-Ordner?
    if pos('content.ie5',dir_chk)=0 then exit;
    if extractfilename(dir_chk)='content.ie5' then exit;
  end;

  //dateien
  if FindFirst(dir+'*',faanyfile,sr)=0 then begin
    repeat
      application.processmessages;
      if (sr.Attr and fadirectory)<=0 then begin

        //bereits geprueft?
        fn:=ansilowercase(dir+sr.name);
        if cache_lb.Items.IndexOf(fn)>-1 then begin
          //ja, dann ignoriere cache-datei
          cache_work_lb.items.add(fn);
        end
        else begin
          //nein, pruefe die Cache-Datei ob Movie-Stream
          addentry(sr,dir);
        end;

      end;
    until FindNext(sr)<>0;
    FindClose(sr);
  end;
end;

Zunächst wird dafür gesorgt, dass der übergebene string "dir", der den aktuell zu scannenden Ordnernamen enthält, mit einem Slash endet.

Anschliessend füllen wir mittels der Borland-"Find"-Funktionen die Struktur "sr" vom Typ "tsearchrec" für jedes gefundene File bzw. jeden ermittelten Ordner.

Beim ersten Durchlauf interessieren uns nur die Unterordner von "dir"; Dateien werden ignoriert. Wurde ein Unterordner ermittelt, so wird mit diesem als neuem "dir" die Prozedur "scan_dir" rekursiv wiederholt. Auf diese Art und Weise durchlaufen wir nach und nach alle Unterordner des ersten "dir", also des anfangs übergebenen Cache-Ordners.

Wurden alle Ordner einer "dir"-Ebene abgeklappert, folgt nun der zweite Durchlauf mit den "Find"-Kommandos. Diesmal interessieren uns nur die Dateien im aktuell betrachteten Ordner.

Zuvor wird jedoch anhand des des Ordnernamens "dir" geprüft, ob wir uns überhaupt in einem sinnvollen Verzeichnis befinden, also einem, welches Cache-Dateien enthält. IE und FF benutzen den Cache-Ordner nämlich auch noch zur Verwaltung anderer Dateien und Ordner, die uns hier aber nicht weiter interessieren.

Haben wir schliesslich eine echte Cache-Datei ermittelt, wird anhand der Eintragungen in der ListBox "cache_lb" gecheckt, ob wir diese nicht bereits bei einer früheren "Refresh"-Aktion auf Movie-Stream-Artigkeit überprüft haben. Falls ja, dann ignorieren wir die Datei - sie muss ja nicht unnötigerweise noch einmal analysiert werden.

FLV-Cache-Catch-Converter - IE-Cache aus Sicht des Window-Explorers

IE-Cache aus Sicht des Window-Explorers: Betrachtet man sich den IE-Cache des IE mit dem Explorer, dann sieht es so aus, als seien alle Files in nur einem Ordner untergebracht. Dem ist aber nicht so. Windows verbirgt (leider) häufig die wahre Natur seiner internen Organisation vor dem Benutzer. Die API-Funktionen zum Auslesen des Caches (auf die der Explorer vermutlich zurückgreift) sind darüber hinaus fehlerhaft, wie mir scheint. Denn teilweise werden damit vorhandene Files nicht angezeigt bzw. Files, die bereits gelöscht sind, immer noch als existent ausgegeben.

FLV-Cache-Catch-Converter - Die wahre Ordner-Struktur des IE-Caches

Die wahre Ordner-Struktur des IE-Caches: Mein eigener Dateibrowser, der "ComCen", zeigt uns hier die wahre Ordner-Struktur des IE-Caches: Er besteht nämlich aus zahlreichen Unterordnern, index-Dateien und Systemfiles. Und nur in einigen der Unterordner liegen auch tatsächlich die gesuchten Cache-Dateien drin.

2.3.5. Movie-Stream-Detection

Sollte eine gefundene Cache-Datei noch nicht überprüft worden sein, etwa, weil sie neu im Cache hinzugekommen ist, so wird sie jetzt an eine weitere interne Prozedur von "refresh" übergeben, nämlich "addentry":

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
//FLV-Datei-Eintrag in temporaere ListBox vornehmen
procedure addentry(sr:tsearchrec;dir:string);
var
  dt:tdatetime;
  s,typ,fn_state:string;
  check_ok:bool;
begin
  inc(check_c);

  //Stream-Datei? Wenn nicht, ignoriere sie
  if not file_is_movie_stream(dir+sr.Name,typ,check_ok) then begin
    //wenn Pruefung erfolgreich, markiere Datei als geprueft
    if check_ok then
      cache_work_lb.items.add(ansilowercase(dir+sr.Name));

    //downloade mehr, koennte eventuell doch ein Stream sein
    exit;
  end;

  //Dateidatum sichern
  dt:=FileDateToDateTime(sr.Time);

  //Sortierungsspalte normieren
  if sort_size then begin
    //Sortierung nach Groesse
    s:=inttostr(sr.size);
    while length(s)<10 do s:='0'+s;
  end
  else begin
    //Sortierung nach Dateidatum
    s:=formatdatetime('yymmdd hh:nn:ss',dt);
  end;

  //Datei blockiert?
  fn_state:=state(dir+sr.name);

  //Eintrag aufbauen, Sortierspalte nach vorne
  s:=
    s+'|'+
    sr.Name+'|'+
    inttostr(sr.size)+'|'+
    size_format(sr.size)+'|'+
    formatdatetime('yymmdd hh:nn:ss',dt)+'|'+
    fn_state+'|'+
    dir+'|';

  //und Eintrag in temporaere ListBox
  sg_sl.add(s);
end;

Zuerst wird hier der Cache-Check-Zähler "check_c" für die Anzahl geprüfter Files erhöht. Anschliessend liefert uns die Funktion "file_is_movie_stream" die wesentliche Information zurück, ob es sich bei der gefundenen Datei um ein Movie-Stream handelt oder nicht.

Ist sie - wie sicher in den weitaus meisten Fällen - kein Movie-Stream, so landet ihr Dateinamen in der ListBox der neu geprüften Dateien "cache_work_lb", sofern auch das "check_ok"-Flag auf "true" gesetzt ist - doch dazu gleich mehr.

Wurde die aktuelle Datei dagegen tatsächlich als Movie-Stream erkannt, dann wird sie nun in die temporäre Stringlist "sg_sl" eingetragen. Zu beachten ist, dass dabei das Sortierkriterium (Grösse oder Datum) an den Anfang gestellt wird, wodurch bei der späteren Übertragung der "sg_sl" in die Cache-StringGrid automatisch die gewünschte Sortierreihenfolge gewährleistet ist.

FLV-Cache-Catch-Converter - Welche Firefox-Cache-Files sind hier Movie-Streams?

Movie-Stream-Detection: Der im Bild gezeigte Cache des Firefox-Browsers beherbergt eine Unzahl von Dateien. Doch bei welchen dieser Dateien handelt es sich um Movie-Streams? Ihre Bezeichnung verrät nichts, Grösse und Datum sind irrelevant, eine Extension liegt nicht vor. Doch unser Tool "FLV-CCC.EXE" weiss dennoch die Antwort (zumindest meistens).

Die Funktion "file_is_movie_stream" ist eine der Kernfunktionen von "FLV-CCC". Sie ist folgendermassen implementiert worden:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
function file_is_movie_stream(
  fn:string;
  var typ:string;
  var check_ok:bool
):bool;
const
  _bufsz=50;
var
  buf:array[0.._bufsz]of char;
  fhi:thandle;
  rd:cardinal;
begin
  typ:='';
  check_ok:=false;
  result:=false;

  //Datei zum Lesen oeffnen
  fhi:=FileOpen(fn,fmOpenREAD or fmShareDenyNone);

  //nicht geklappt? Dann raus
  if fhi=INVALID_HANDLE_VALUE then exit;

  try
    //Buffer einlesen
    if not ReadFile(fhi,buf,_bufsz,rd,nilthen exit;
  finally
    closehandle(fhi);
  end;

  //zu wenig Infos fuer Entscheidung
  if rd<_bufsz then begin
    //Status ready? Dann Minifile, keine weitere Pruefung nötig
    check_ok:=(state(fn)='READY');
    exit;
  end;

  //Prüfung okay
  check_ok:=true;

  //FLV?
  typ:='flv';
  result:=((buf[0]='F')and(buf[1]='L')and(buf[2]='V'));
  if result then exit;

  //wmv?
  typ:='wmv';
  result:=((buf[0]='0')and(buf[1]='&')and(buf[2]=chr($b2)));
  if result then exit;

  //mp4?
  typ:='mp4';
  result:=(pos_buffer('ftyp',buf,rd)>-1);

  //erweitere Prüfung auf mp4
  if result then
    result:=
      (pos_buffer('ftypisom',buf,rd)>-1)or
      (pos_buffer('mp4',     buf,rd)>-1);
  if result then exit;

  //keiner der obigen Streams
  typ:='';
end;

Die übergebene Cache-Datei "fn" wird zum Lesen geöffnet. Dann wird wird versucht, die ersten 50 Bytes der Datei in den Puffer "buf" zu speichern. Gelingt dies nicht, kann das zwei Gründen haben: Entweder ist das Cache-File einfach kleiner als 50 Bytes oder der Download-Prozess des Browsers ist noch nicht abgeschlossen.

Daher wird in diesem Fall über die Funktion "state" geprüft, ob die Datei gerade in Benutzung durch einen anderen Prozess (Browser) ist. Falls nicht, ist der Download offenbar abgeschlossen und die Datei schlicht zu klein, um ein Movie-Stream sein zu können - "check_ok" wird auf "true" gesetzt. Ansonsten behält "check_ok" den Wert "false", wodurch sichergestellt bleibt, dass die Datei beim nächsten Refresh der Cache-StringGrid erneut auf Stream-Artigkeit überprüft wird.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
//Status einer Datei im Cache ermitteln-----------------------
//result:
// '**ERROR*' wenn Datei fehlt
// 'READY' wenn Download der Datei fertig ist
// 'BLOCKED' wenn Download der Datei noch nicht fertig ist
function state(fn:string):string;
var
  HF:HFile;
  b:byte;
begin
  fn:=ansilowercase(fn);
  b:=2;
  if fileexists(fn) then begin
    HF:=CreateFile(
      PChar(fn),
      GENERIC_READ or GENERIC_WRITE,
      0,nil,OPEN_EXISTING,0,0
    );
    try
      b:=ord(HF=INVALID_HANDLE_VALUE);
    finally
      CloseHandle(HF);
    end;
  end;

  if      b=2 then result:='*ERROR*'
  else if b=0 then result:='READY'
  else             result:='BLOCKED';
end;

Zurück zur Funktion "file_is_movie_stream": Wurden die ersten 50 Bytes der Datei erfolgreich in "buf" abgelegt, prüfen wir nun weiter, ob darin die nötigen "Signaturen" für das FLV-, WMV- oder MP4-Format enthalten sind.

FLVs und WMVs sind anhand ihres "magischen Codes" - die ersten drei Bytes der Datei mit standardisiertem Inhalt - leicht zu erkennen.

FLV-Cache-Catch-Converter - Magic-Code von FLV-Movies

Magic-Code von FLV-Movies: FLV-Movies sind so nett, und verraten gleich zu Beginn ihrer Datei in grossen Lettern, was ihr Inhalt ist. Natürlich kann auch eine Nicht-FLV-Datei diese Zeichenfolge (zufällig) besitzen, was aber selten vorkommen dürfte. "FLV-CCC" würde jedenfalls auf eine solche "Fake"-Datei hereinfallen. Nicht jedoch das Proggy "ffmpeg.exe" ...

FLV-Cache-Catch-Converter - Magic-Code von WMV-Movies

Magic-Code von WMV-Movies: Nicht ganz so sprechend wie bei FLVs, aber auch WMVs besitzen eine genormte Kennzeichnung in den ersten drei Bytes ihrer Datei. Das macht es "FLV-CCC" relativ leicht, solche Dateien zu identifizieren. Sollte eine WMV-Datei jedoch ohne diese Signatur daherkommen, würde "FLV-CCC" sie nicht aufspüren können ...

MP4s scheinen dagegen keinen Magic Code zu besitzen (?) und treten auch vielfältiger auf, was ihr "Inneres" angeht. Keine Ahnung also, ob meine obige Prüfungsroutine letztlich alle Arten von MP4s erkennt. In den von mir durchgespielten Fällen klappte es aber eigentlich immer.

FLV-Cache-Catch-Converter - Hex-View des MP4-Headers

Hex-View des MP4-Headers: In obigem Beispiel tauchen die Zeichenfolgen "ftypsiom" und "mp41" in der (Cache-)Datei "smooth.mp4" auf. Mit ziemlicher Sicherheit handelt es sich hierbei also um ein MP4-Movie - zumal dies natürlich bereits die Extension ".mp4" nahegelegt hat.

Um den Inhalt des 50-Byte-Puffers zu analysieren, nehmen wir übrigens die Funktion "pos_buffer" zu Hilfe, die eine leicht abgewandelte Version des Pos-Befehls von Delphi ist:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
function pos_buffer(s:string;buf:array of char;buf_len:integer):integer;
var
  c,cc:integer;
  s_chk:string;
begin
  for c:=0 to buf_len-length(s)-1 do begin
    s_chk:='';for cc:=0 to length(s)-1 do s_chk:=s_chk+buf[c+cc];
    if s=s_chk then begin
      result:=c;
      exit;
    end;
  end;
  result:=-1;
end;

Hier wird der übergebene Char-Puffer "buf" zeichenweise durchlaufen und jedes Mal darauf getestet, ob ab der aktuellen Position "c" die Zeichenfolge "s" folgt. Im Erfolgsfall wird "c" zurückgeliefert, sonst "-1".

2.3.6. Catch a movie

Wurde die Cache-StringGrid erfolgreich mit den Namen der Movie-Streams gefüllt, können diese vom Benutzer per Doppelklick in den Konvertierungsspooler übertragen werden. Diesen Job erledigt die Prozedur "catch":

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
//Kopiere/Verschiebe alle in Cache-StringGrid
//selektierten FLV-Datei aus dem
//Cache-Ordner in den Konvertierungsspooler
procedure catch(
  sg:tstringgrid;
  dir,target:string;
  var counter:integer
);
var
  ext,typ,fn_from,fn_to,timestamp:string;
  row:integer;
  check_ok:bool;
begin
  screen.cursor:=crhourglass;

  for row:=sg.Selection.Top to sg.Selection.Bottom do begin
    fn_from:=
      sg.Cells[ord(_cache_path),Row]+
      sg.cells[ord(_cache_file),Row];

    //Zielnamen vorgegeben?
    if target='' then
      target:=sg.cells[ord(_cache_file),Row];

    //Typ des Stream bestimmen
    timestamp:='';
    if file_is_movie_stream(fn_from,typ,check_ok) then begin
      //FLV? Dann Zeitstempel auslesen
      if typ='flv' then
        timestamp:=timestamp_get(fn_from);
    end;

    ext:='.flv';

    //eindeutiger Zielnamen: test -> test.flv+
    fn_to:=filename_unique(
      dir+changefileext(target,'_'+timestamp+ext)
    );

    if state(fn_from)='READY' then begin
      movefile(pchar(fn_from),pchar(fn_to));
      inc(counter);
    end
    else begin
      copyfile(pchar(fn_from),pchar(fn_to),false);
      inc(counter);
    end;
  end;
  screen.cursor:=crdefault;
end;

Die Cache-StringGrid wird durchlaufen und alle selektierten Zeilen betrachtet. Im string "fn_from" wird dabei der jeweilige Dateiname gesichert.

Ist im string "target" ein Ziel-Dateiname vorgegeben, so wird dieser verwendet, ansonsten der Quell-Dateiname als Basis für den Ziel-Dateinamen vorerst beibehalten.

Wir rufen die uns schon bekannte Funktion "file_is_movie_stream" erneut auf. Diese liefert uns nämlich im var-Parameter "typ" auch zurück, um was für eine Art von Movie-Stream es sich konkret handelt: FLV, WMV oder MP4.

2.3.7. Timecodes In FLVs

Haben wir ein FLV-Movie zum Kopieren erwischt, dann rufen wir nun zusätzlich die Funktion "timestamp_get" auf:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
//Ersten Zeitstempel aus einer FLV-Datei auslesen
function timestamp_get(fn:string):string;
const
  _bufsz=1024;
var
  fhi:thandle;
  c,ts:integer;
  rd:cardinal;
  buf:array[0.._bufsz]of char;
  found_c:integer;
begin
  result:='err';

  //Öffne Datei zu lesen
  fhi:=FileOpen(fn,fmOpenREAD or fmShareDenyNone);

  //hat es geklappt?
  if fhi=INVALID_HANDLE_VALUE then exit;

  try
    //Puffer leeren
    for c:=0 to _bufsz-1 do buf[c]:=#0;

    //Puffer mit den ersten 1024 Bytes füllen
    if not ReadFile(fhi,buf,_bufsz,rd,nilthen exit;

    //Taucht im Puffer der Text 'starttime' auf?
    found_c:=pos_buffer('starttime',buf,rd);

    //Starttime-Text gefunden?
    if found_c>-1 then begin
      //Yep, setze Pufferzähler direkt dahinter
      c:=found_c+10;
    end
    else begin
      //Nope, setze Pufferzähler auf fixen Wert
      //(empirisch ermittelt, also unsicher)
      c:=$11;
    end;

    //berechne Time-Code aus nächsten drei Bytes
    ts:=byte(buf[c])*256*256;
    ts:=ts+byte(buf[c+1])*256;
    ts:=ts+byte(buf[c+2]);

    result:=inttostr(ts);

    //fülle Time-Code-String auf 10 Zeichen auf
    //(wegen Sortierbarkeit im String-Style)
    while length(result)<10 do result:='0'+result;

  finally
    //Aufräumen
    closehandle(fhi);
  end;
end;

Diese Funktion versucht, im Kopf einer FLV-Datei Hinweise auf einen Zeitstempel zu finden. Üblicherweise hat der einen Wert von Null, was heisst, dass hier der Anfang des Movies vorliegt.

Wurde jedoch der Download eines FLV-Movies abgebrochen und an anderer Position wieder aufgenommen, so steht im ersten Zeitstempel der neuen FLV-Datei, wie viele Millisekunden (?) seit Filmstart bereits vergangen sind.

Diesen Zahlwert hängen wir nun - in normierter Weise - an den Ziel-Dateinamen dran, wodurch sich alle Teilstücke eines FLVs in zeitlicher Reihenfolge sortieren lassen, auch wenn sie nicht chronologisch geladen worden sein sollten!

FLV-Cache-Catch-Converter - FLV-Parts mit den ermittelten Time-Code im Dateinamen

FLV-Parts mit Time-Code: Ein FLV-Movie wurde mit dem Browser in mehreren Etappen geladen und jeweils über "FLV-CCC.exe" in den Spooler kopiert. Dadurch, dass der Time-Code mit im Dateinamen steckt, lassen sich die Teile nachträglich in der zeitlich korrekten Reihenfolge sortieren, auch wenn z.B. der letzte Teil "flv-part_0023400000.flv" als erstes gedownloadet wurde.

Diese Zeitstempelgeschichte erhebt keinen Anspruch auf volle Gültigkeit. Tatsächlich habe ich mir nur jede Menge FLV-Header mit einem Hex-Viewer angeschaut und die passenden Stellen empirisch ermittelt. Bisher jedenfalls haut sie ziemlich gut hin.

FLV-Cache-Catch-Converter - Analyse des FLV-Formats mit Kuli und Papier

Analyse des FLV-Format: Ohne Blatt und Kuli geht bei mir gar nichts. Selbst so komplexe Fabrikate wie FLV-Movies offenbaren nach und nach ihre Geheimnisse, wenn man nur lang genug in ihnen herumwühlt. Dieses Wissen steht aber bisweilen durchaus auf tönernen Füssen ...

FLV-Cache-Catch-Converter - Zeitstempel in FLV-Movies I

Hex-View eines FLV-Movies I: Die erste Markierung zeigt den Magic Code eines FLV-Movies, die drei Bytes der zweite Markierung enthalten im obigem Beispiel den Time-Code. Hier ist dies "00 00 00", also "0". Der Film beginnt demnach gerade.

FLV-Cache-Catch-Converter - Zeitstempel in FLV-Movies II

Hex-View eines FLV-Movies II: Wurde ein FLV-Movie unterbrochen und der Download an anderer Stelle wieder aufgenommen, findet sich im Header der Datei bisweilen das Schlüsselwort "starttime", hinter dem der Time-Code notiert ist. Je weiter der Film fortgeschritten ist, desto grösser ist diese Zahl.

Ach ja, ähnliche Timecodes existieren vermutlich auch in WMVs und MP4s. Doch solche Movie-Typen kommen mir deutlich seltener unter. Um die Dinger zu knacken hätte die Kosten-Nutzen-Rechnung nicht gestimmt. Also liess ich es bleiben.

2.3.8. Eindeutigkeit bei Quelle und Ziel

Kehren wir zur Prozedur "catch" zurück: Wir haben eine Movie-Stream-Datei ermittelt und einen vorläufigen Ziel-Dateinamen für den Konvertierungsspooler gebildet. Da sich dort unter Umständen bereits eine gleichnamige Datei befinden kann, wird der Ziel-Dateinamen ein weiteres mal adaptiert, diesmal durch die Funktion "filename_unique":

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
//Liefert eindeutigen Dateinamen zurück---------------------
function filename_unique(fn_org:string):string;
var
  s,fn,dir,fn_alone,bracket:string;
  bracket_ok,file_ok:bool;
  sr:tsearchrec;
  c:integer;
begin
  //Pfad auf Konvertierungsspooler
  dir:=extractfilepath(fn_org);

  //Dateinamen freistellen: c: est[1].flv -> test[1]
  s:=extractfilename(fn_org);
  s:=changefileext(s,'');

  //eckige Klammern aus Dateinamen filtern: test[1] -> test
  bracket_ok:=false;
  fn_alone:='';
  for c:=1 to length(s) do begin

    //Flag setzen/löschen
    if s[c]='[' then bracket_ok:=true
    else if s[c]=']' then begin
      bracket_ok:=false;
      continue;
    end;

    //innerhalb eckiger Klammern?
    if bracket_ok then continue;

    //nein, also Text merken
    fn_alone:=fn_alone+s[c];
  end;

  //eindeutigen Dateinamen (ohne Extension) suchen
  c:=0;
  bracket:='';
  file_ok:=false;
  repeat
    fn:=dir+fn_alone+bracket;

    //Kann Dateinamen verwendet werden?
    if findfirst(fn+'.*',faanyfile,sr)=0 then begin
      //Nein, Dateinamen existiert bereits im Spooler
      FindClose(sr);
    end
    else if findfirst(fn+_flv_org,faanyfile,sr)=0 then begin
      //Nein, zukünftigen Original-Dateinamen existiert
      //bereits im Spooler
      FindClose(sr);
    end
    else if findfirst(fn+_flv_err,faanyfile,sr)=0 then begin
      //Nein, möglicher Fehler-Dateinamen existiert bereits
      //im Spooler
      FindClose(sr);
    end
    else file_ok:=true;

    if not file_ok then begin
      //gefundener Dateinamen womöglich nicht eindeutig
      //Nummer anhängen/erhöhen und erneut probieren
      //test -> test[1]
      inc(c);
      bracket:='['+inttostr(c)+']';
    end;
  until file_ok;

  result:=fn+'.flv'+_postfix;
end;

Das Problem, einen eindeutigen Namen für das Ziel-File zu finden, klingt einfach, ist es aber nicht, da dieses Ziel-File in der Folge des Konvertierungsprozesses mehrmals den Namen wechseln wird - und bereits im Vorfeld gesichert sein muss, dass es dabei nicht zu Überschreibungen andere Dateien kommt.

Der Ablauf der (möglichen) Namensbildungen sei hier kurz anhand eines Beispiels geschildert:

  • Die Cache-Datei "test.flv" wird als Movie-Stream identifiziert
  • Die Datei wird als "test.flv+" in den Spooler kopiert.
  • Die Datei wird anhand des "+"-Zeichen als zu konvertieren erkannt. Vor der Konvertierung wird das '+' in '-' gewandelt, die Datei heisst jetzt also "test.flv-".
  • Mögliche Ergebnisfiles während der Konvertierung sind: "test.mpg-" oder "test.m1v-".
  • Mögliche Ergebnisfiles nach der Konvertierung sind: "test.mpg" oder "test.m1v"
  • Die ursprüngliche Datei wird umbenannt in "test#org.flv" oder "test#err.flv", je nach Ausgang der Konvertierung.

Sollte auch nur einer dieser Ergebnis- oder Quell-Dateinamen im Spooler bereits vorliegen, wird die Original-Datei mit einem Zusatz versehen, einer laufenden Nummer, eingefasst in eckigen Klammern. Diese Nummer wird so lange erhöht, bis schliesslich ein eindeutiger Dateiname für alle oben genannte Variationen vorliegt.

FLV-Cache-Catch-Converter - Eindeutiger Spooler-Name

Eindeutiger Spooler-Name: Die "gecatchten" Movies bekommen eindeutige Dateinamen zugewiesen, damit sie sich bei der Konvertierung nicht gegenseitig überschreiben. Anhand der Null-Timecodes im Dateinamen erkennt man hier übrigens, dass es sich bei den gleichnamigen Movies nicht um Teile ein und desselben FLVs handelt, sondern um jeweils neue Filme. Der kryptische Cache-Name wurde durch den sprechenderen Zielname "spongebob" ersetzt.

2.4. Unit "convert_u.pas"

2.4.1. Threads

FLV-Cache-Catch-Converter - TabSheet-Register zur Convert-unit

TabSheet-register zur Convert-unit: Hier kann bestimmt werden, mit welchen Übergabeparametern die "ffmpeg.exe" gefüttert wird, damit das gewünschte Movie-Format generiert wird. Standardmässig werden aus den Movie-Streams MPGs mit 320 x 240 Pixel Auflösung extrahiert, genauso gut könnte man sich aber auch AVIs mit 640 x 480 Pixeln erzeugen lassen. Die Checkbox "Autostart" gibt vor, ob der Konvertierungsspooler nach dem Programmstart aktiviert wird oder nicht. Es muss ja nicht immer sinnvoll sein, ein Movie-Stream aus dem Cache zu konvertieren.

Wie wir eben gesehen haben, werden "gecatchte" Cache-Files mit eindeutigem Namen versehen in den Konvertierungsspooler kopiert oder verschoben. Die Abarbeitung dieser Movies erfolgt über einen speziellen Thread "thread_spooler", der in der unit "convert_u.pas" wie folgt implementiert wird:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
type
  tthread_spooler=class(tthread)
  protected
    procedure execute;override;
  public
    constructor create; virtual;
  end;

[...]

var
  thread_spooler:tthread_spooler;

implementation

constructor tthread_spooler.create;
begin
  //Thread soll sich nach Beendigung automatisch löschen
  inherited create(false);
  freeOnTerminate:=true;
end;

procedure tthread_spooler.execute;
var
  rc:integer;
begin
  //Konvertierung-Panel grün färben
  //daran wird erkannt, dass Konvertierung aktiv ist
  main_f.convert_working_p.color:=cllime;

  //konvertiere erstes File aus Konvertierungsspooler
  rc:=convert_u.spooler_work(
    main_f.convert_flb,
    main_f.homedir+'ffmpeg.exe',
    main_f.convert_param_cb.text
  );

  //passe Konvertierungsstatistiken an
  case rc of
    -1:inc(main_f.convert_err_c);
     0:;
     1:inc(main_f.convert_mpg_c);
     2:inC(main_f.convert_m1v_c);
  end;

  //Konvertierung-Panel wieder rot färben
  main_f.convert_working_p.color:=clred;
end;

Threads verhalten sich im Prinzip wie eigenständige Programme. Man kann sie daher schön im Hintergrund diverse Arbeiten erledigen lassen, ohne dass die Haupt-Applikation dabei gestört wird.

Unser Thread "thread_spooler" ist so konzipiert, dass er sich sofort nach erledigtem Job selbst aus dem Speicher eliminiert ("freeOnTerminate:=true").

In der Prozedur "execute", die direkt nach Erzeugung des Threads startet, wird zuerst ein Panel im Hauptfenster, "convert_working_p", grün umgefärbt. Dadurch wird signalisiert, dass gerade ein Konvertierungsprozess stattfindet.

Durch Aufruf der Funktion "spooler_work" wird dann die eigentliche Konvertierung angestossen (dazu gleich mehr). Im Rückgabewert "rc" steht letztlich das Ergebnis dieser Operation.

Zum Schluss wird dann wieder das Panel "convert_working_p" rot gefärbt; der Konvertierungsprozess ist abgeschlossen, der Thread terminiert sich selbst.

2.4.2. Spooler leeren

Wie wir eben gesehen haben, wird vom Thread "thread_spooler" die Funktion "spooler_work" aufgerufen, um ein Movie-Stream im Konvertierungsspooler in ein MPG (sofern so vorgegeben) zu konvertieren. Genauer gesagt, wird hier zunächst nur der Konvertierungsspooler ausgelesen. Das schauen wir uns jetzt näher an:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
//Lies FLV-Datei aus Spooler und konvertiere sie
//Ergebnis:
// -1: Fehler
//  0: nichts gemacht
//  1: MPG generiert
//  2: M1V generiert
function spooler_work(
  flb:tfilelistbox;
  ffmpeg,param:string
):integer;
var
  dir,fn_free,ext,fn_fro-,fn_to:string;
  rc:integer;
  fhi,fho:integer;
begin
  result:=0;

  //Spooler leer? Dann raus!
  if flb.Items.count=0 then exit;

  //Erste Spooler-Datei holen
  fn_from:=dir+flb.items[0];

  //Auf Freigabe warten
  if file_blocked(fn_from) then exit;

  //Konvertierung im Filenamen dokumentieren:
  //test.flv+ -> test.flv-
  fn_to:=changefileext(fn_from,'.flv'+_postfix_work);
  if not renamefile(fn_from,fn_to) then exit;
  fn_from:=fn_to;

  //Konvertieren: test.flv- in test.mpg- oder test.m1v-
  dir:=flb.directory+'\';
  fn_free:=extractfilename(fn_from);
  fn_free:=changefileext(fn_free,'');
  rc:=convert_movie_stream(ffmpeg,param,dir,fn_free);
  if rc=0 then begin
    //Fehler: Konvertierung misslungen
    //test.flv- -> test#err.flv
    fn_to:=dir+fn_free+_flv_err;
    renamefile(fn_from,fn_to);
    result:=-1;
    exit;
  end;

  //Okay, Original-File retten: test.flv- -> test#org.flv
  fn_to:=dir+fn_free+_flv_org;
  renamefile(fn_from,fn_to);

  //Zielnamen anpassen:
  //test.mpg- -> test.mpg oder test.m1v- -> test.m1v
  if rc=1 then ext:='.mpg' else ext:='.m1v';
  fn_from:=dir+fn_free+ext+_postfix_work;
  fn_to:=dir+fn_free+ext;
  renamefile(fn_from,fn_to);

  //Datum vom Ziel auf Quelle-Datum ändern
  fn_from:=dir+fn_free+_flv_org;
  fn_to:=dir+fn_free+ext;
  fhi:=FileOpen(fn_from,fmOpenREAD or fmShareDenyNone);
  fho:=FileOpen(fn_to,fmOpenWrite or fmShareDenyNone);
  FileSetDate(fho,filegetdate(fhi));
  closehandle(fho);
  closehandle(fhi);

  result:=rc;
end;

Zuerst wird geprüft, ob die FileListBox "flb" des Konvertierungsspoolers leer ist. Ist dem so, gibt es keine Movies, also nichts weiter zu tun.

FLV-Cache-Catch-Converter - Ein wohlgefüllter Konvertierungsspooler

Ein wohlgefüllter Konvertierungsspooler: Hier wartet eine Menge Arbeit auf "ffmpeg.exe". Im Spooler befinden sich bereits 10 Movie-Streams. die zu MPGs konvertiert werden sollen. Der Benutzer wählt gerade die passenden Übergabeparameter dafür aus. Übrigens: Das alle gezeigten Movies die Extension ".flv" aufweisen, bedeutet nicht, dass es sich dabei wirklich in allen Fällen um FLVs handelt. Die Extension wurde von "FLV-CCC" so vorgegeben, um die Namensvielfalt rund um den Eindeutigkeitstest nicht noch verwirrender zu gestalten.

Lungert im Spooler jedoch mindestens eine Datei vor sich hin, so greifen wir uns die erste, und stellen über die Funktion "file_blocked" sicher, dass dieses Movie-Stream derzeit von keinem anderem Prozess verwendet wird.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
//wird Datei gerade von einem anderen Prozess benutzt?
function file_in_use(fn:string):bool;
var
  HF:HFile;
  rc:byte;
begin
  result:=false;if not fileexists(fn) then exit;
  HF:=CreateFile(
    PChar(fn),
    GENERIC_READ or GENERIC_WRITE,
    0,nil,OPEN_EXISTING,0,0
  );
  rc:=(ord(HF=INVALID_HANDLE_VALUE));
  CloseHandle(HF);
  if rc=1 then result:=true;
end;

//warte gewisse Zeit, bis die Datei von keinem
//Prozess mehr genutzt wird.
function file_blocked(fn:string):bool;
var
  r:integer;
begin
  r:=0;
  repeat
    application.processmessages;
    sleep(100);
    inc(r);
  until(not file_in_use(fn))or(r>1000);
  result:=(r>1000);
end;

Ist die Datei nicht in Benutzung, ändern wir ihr Dateinamen-Suffix "+" in "-", aus z.B. "test.flv+" wird also "test.flv-". Da im Konvertierungsspooler nur "*.flv+"-Files angezeigt werden, taucht dadurch diese Datei nach dem nächsten Refresh nicht mehr darin auf!

Anschliessend wird die Datei an die Funktion "convert_movie_stream" übergeben, die aus dem Movie-Stream eine MPG-Datei machen soll. Gelingt dies nicht, wird die Original-Datei umbenannt: Aus "test.flv-" wird dann "test#err.flv" werden.

Im Erfolgsfall erhält unsere Beispiel-Datei dagegen den Dateinamen "test#org.flv". Je nachdem, ob ein MPG oder ein M1V generiert wurde, erhält natürlich auch das Ergebnisfile einen neuen Namen ("test.mpg" bzw. "test.m1v").

Als letzte Massnahme bekommt das Ergebnisfile noch das gleiche Dateidatum wie die Original-Datei verpasst. So bleiben Quelle und Ziel im Explorer auch bei Sortierung nach dem Dateidatum beieinander stehen.

FLV-Cache-Catch-Converter - Konvertierungsspooler während der Arbeit

Konvertierungsspooler während der Arbeit: Die Movie-Streams "hurz*" und "ogl-planets-erde-saturn*" wurden bereits in MPGs konvertiert (kein Suffix). Die Datei "ogl-planets-erde-sonne*" ist gerade in Arbeit (zu erkennen am Suffix "-"). Und die restlichen Movies harren noch ihrer Bearbeitung (Suffix "+"). Oops! Ich merke gerade, dass die Adaption des Ziel-Dateidatum hier nicht stimmt, da muss ich wohl noch einmal ran ...

2.4.3. Von Movie-Stream zu MPG

Okay, nahmen wir noch einmal an, im Konvertierungsspooler wurde ein Movie-Stream gefunden und wie in "spooler_work" beschrieben an die Funktion "convert_movie_stream" übergeben. Dort passiert dann Folgendes:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
//Konvertiere FLV-Movie mittels FFMPEG.EXE entweder in
//eine MPG-Datei mit Sound oder eine M1V-Datei ohne Sound
//Ergebnis:
// 0: Misserfolg
// 1: MPG generiert
// 2: M1V generiert
function convert_movie_stream(ffmpeg,param,dir,fn_free:string):byte;
var
  fn_from,fn_to,cmd,errs:string;
begin

  //Quellnamen bilden, z.B. test.flv-
  fn_from:=dir+fn_free+'.flv'+_postfix_work;

  //Konvertierung: Probiere zuerst: test.flv- -> test.mpg-
  result:=1;
  fn_to:=dir+fn_free+'.mpg'+_postfix_work;

  //Kommando-String für ffmpeg.exe aufbauen
  cmd:=ffmpeg+' '+'-i '+fn_from+' '+param+' '+fn_to;

  //hat es geklappt? Dann raus!
  if shell_execute_wait(cmd,dir,errs) then exit;

  //nein, hat nicht geklappt: Ergebnisfile löschen
  deletefile(fn_to);

  //und noch einmal Konvertierung, diesmal test.flv- -> test.m1v-
  result:=2;
  fn_to:=changefileext(fn_to,'.m1v'+_postfix_work);
  cmd:=ffmpeg+' '+'-i '+fn_from+' '+'-an '+param+' '+fn_to;

  //hat es geklappt? Dann raus!
  if shell_execute_wait(cmd,dir,errs) then exit;

  //nein, hat wieder nicht geklappt
  deletefile(fn_to);

  //Misserfolg mitteilen
  result:=0;
end;

Zu Beginn werden Quell- und Ziel-Dateinamen gebildet ("fn_from" und "fn_to"). Beim ersten Versuch bekommt das Zielfile hierbei die Extension ".mpg-" zugewiesen. Das heisst, es wird zuerst versucht, ein MPG mit Sound zu erzeugen.

Es wird der passende Kommando-string "cmd" aufgebaut, der das externe Programm "ffmpeg.exe" mit den nötigen Übergabe-Parametern aufruft. Um etwa ein FLV-Movie "test.flv-" in eine MPG-Datei "test.mpg-" mit 25 Bildern pro Sekunden, mit einer Auflösung von 320 x 240 Pixeln und einer Bitrate von 400 kbps, umzuwandeln, müsste "ffmpeg.exe" in der DOS-Konsole folgendermassen aufgerufen werden:

  ffmpeg.exe -i test.flv- -r 25fps -s 320x240 -f mpeg -b 400kbps -y test.mpg-

Eben diesen Konsolen-Aufruf simulieren wir nun in "FLV-CCC". Dazu wird der Kommando-Strings "cmd" an die Funktion "shell_execute_wait" übergeben, die etwas weiter unten beschrieben wird.

Der Return-Code "rc" sagt uns, ob alles geklappt hat. Wenn ja, verlassen wir die Prozedur wieder.

Hat die Konvertierung nicht geklappt, dann wird der Kommando-string "cmd" leicht abgewandelt. Denn beim nächsten Versuch soll das FLV-Movie in eine MPG-Datei ohne Sound umgewandelt werden, also in ein M1V-File. Erfahrungsgemäss machen nämlich hauptsächlich die Sound-Codecs Ärger beim Konvertierungsprozess mit "ffmpeg.exe", während die Bilder korrekt verarbeitet werden.

Diesmal wird an "shell_execute_wait" folgender Kommandostring übergeben:

  ffmpeg.exe -i test.flv- -an -r 25fps -s 320x240 -f mpeg -b 400kbps -y test.m1v-

Bei Erfolg wird die Prozedur mit Result "2" verlassen, bei Misserfolg mit "0".

FLV-Cache-Catch-Converter - Ergebnisse des Konvertierungsprozesses

Ergebnisse des Konvertierungsprozesses: "FLV-CCC" hat drei Konvertierungen(-Versuche) durchgeführt. Beim ersten Film "fehlerhaftes_movie.flv" hat "ffmpeg.exe" versagt. Daher gibt es hier kein Ergebnisfile und das Original wurde in "fehlerhaftes_movie#err.flv" umbenannt. Das zweite File "movie_mit_sound.flv" wurde korrekt in "movie_mit_sound.mpg" konvertiert (man beachte, dass die Dateidatum-Modifikation inzwischen stimmt!). Und die letzte Film-Datei "movie_ohne_sound.flv" konnte schliesslich nur in ein M1V-File transferiert werden. Tja, hätte sich das dumme "FLV-CCC" die Dateinamen gleich zu Beginn näher angeschaut, wären die Ergebnis sicher leichter zustande gekommen ...

2.4.4. Ausführen und Warten

Wie beschrieben, wird in der Funktion "convert_movie_stream" ein spezieller Kommando-string "cmd" aufgebaut und an die Funktion "shell_execute_wait" übergeben, wo das Kommando letztlich ausgeführt wird. Wie das genau abläuft, schauen wir uns jetzt an:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
//erzeuge einen neuen Windows-Prozess
//und warte, bis er beendet ist
function shell_execute_wait(
  cmd,dir:string;
  var info:string
):bool;

  function lasterr:string;
  var
    l:dword;
  begin
    l:=getlasterror();
    result:='ERROR: '+inttostr(l);
  end;

const
  _strmax=500;
var
  str:pchar;
  tsi:TStartupInfo;
  tpi:TProcessInformation;
  dw,wrc:dword;
  pdir:pansichar;
begin
  //Prozess-Struktur vorbereiten
  FillChar(tsi,SizeOf(TStartupInfo),0);
  tsi.cb:=SizeOf(TStartupInfo);
  tsi.wShowWindow:=sw_hide;
  tsi.dwFlags:=STARTF_USESHOWWINDOW;

  //Gehe von Erfolg aus
  info:='OK '+cmd;

  //Arbeitsordner setzen
  if dir='' then pdir:=nil
            else pdir:=pansichar(dir);

  //Prozess erzeugen
  str:=allocmem(_strmax);
  t2y
    strpcopy(str,cmd);
    if CreateProcess(
      nil,
      str,
      nil,nil,False,
      0,
      nil,
      pdir,
      tsi,tpi
    )
    then begin

      //Warte maximal 5 Minuten auf Prozess-Ende
      wrc:=WaitForSingleObject(tpi.hProcess,5*60*1000);
      if wrc=WAIT_OBJECT_0 then begin
        //Prozess erfolgreich beendet

        //Hat auch alles geklappt?
        if GetExitCodeProcess(tpi.hProcess,dw) then begin
          if dw<>0 then begin
            info:='GetExitCodeProcess(): '+lasterr()+' '+cmd;
            result:=false;
            exit;
          end;
        end;
        CloseHandle(tpi.hProcess);
        CloseHandle(tpi.hThread);
      end
      else begin
        //es gab Probleme
        info:='WaitForSingleObject(): '+lasterr()+' '+cmd;
        result:=false;
        exit;
      end;
    end
    else begin
      info:=
        'Konnte Prozess '+cmd+' nicht erzeugen - Abbruch! '+
        lasterr()+' '+cmd;
      result:=false;
      exit;
    end;
    result:=true;
  finally
    freemem(str);
  end;
end;

Zuerst wird die Systemstruktur "tsi" vom Typ "TStartupInfo" mit Daten gefüllt, die u.a. festlegen, dass der zu generierende Prozess unsichtbar ablaufen soll ("tsi.wShowWindow:=sw_hide;").

Diese Struktur wird zusammen mit dem Kommando-string "cmd" an die API-Funktion "CreateProcess" übergeben, die einen neuen Prozess im System generiert und damit "cmd" zur Ausführung bringt - ganz so, als wäre das Kommando in die DOS-Konsole getippt worden.

Anschliessend wird dann an dieser Stelle bis zu 5 Minuten gewartet ("WaitForSingleObject(tpi.hProcess,5*60*1000)"), dass der Prozess von alleine beendet wird, bevor automatisch mit der Funktion fortgefahren wird.

In jedem Fall wird eine entsprechende Statusmeldung "wc" zurückgeliefert, die Auswirkungen auf das Gesamtfunktionsergebnis "result" nimmt.

FLV-Cache-Catch-Converter - FFMPEG in der DOS-Konsole

FFMPEG in der DOS-Konsole: Die Funktion "shell_execute_wait" verhält sich ähnlich wie die DOS-Konsole, in die ein Kommando-string eingeben wird. "FLV-CCC" sorgt jedoch dafür, dass der Ausführungsprozess im Verborgenen abläuft. Anders als die DOS-Konsole, die während der "ffmpeg"-Konvertierung eine Menge Informationen auswirft, wie man oben bewundern darf.

3. Epilog

Ich habe einige Cache-Browser zum Aufspüren von FLVs ausprobiert, aber überzeugt habe die mich nicht. Vielleicht hätte ich mich länger mit ihnen beschäftigen sollen, um alle Features auszuloten, dann wären die Ergebnisse vermutlich befriedigender ausgefallen.

Aber ich bin ja Programmierer. Warum also mit (mangelhafter) Fremd-Software herumschlagen? Mit dem "FLV-CCC" bin ich jedenfalls sehr zufrieden. Der findet praktisch alle Movie-Streams, selbst wenn sie gestückelt geladen wurden, und konvertiert sie so, dass sie mit meinen Medien-Tools nachträglich bearbeitet werden können (wie z.B. "VidSplitt", welches nur mit MPGs umgehen kann).

Besonders genial finde ich, dass die Timecodes ausgelesen und in den Dateinamen eingebaut werden. Denn wer z.B. 20 Movie-Fetzen auf Platte hat, mit den sonst üblichen bunt gewürfelten Cache-Dateinamen, wird seine liebe not haben, die wieder in chronologischer Reihenfolge zusammensetzen zu können.

Okay, es klappt nicht immer mit dem Timecodes. Manche Movie-Parts werden von Web-Servern auch derart manipuliert ausgeliefert, dass "ffmpeg.exe" sie nicht konvertieren kann. Auch gibt es in "FLV-CCC" eine Reihe unnötiger Arbeiten, etwa, dass die Header-Bytes der Movie-Streams (unnötigerweise) mehrfach ausgelesen werden (um sie zu als Movie-Streams zu identifizieren, um ihren Typ festzustellen und um letztlich die Timecodes auszulesen).

So etwas verdirbt mir jedenfalls nicht den Spass an meinem Proggy. Ist halt nichts Hundertprozentiges. But who cares?

4. Download

"FLV-CCC" wurde in Delphi 7 programmiert. Im ZIP-File enthalten ist der vollständige Source-Code, die EXE-Datei und auch das Tool "ffmpeg.exe". Das Paket, etwa 4 MB, gibt es hier:

FLV-Cache-Catch-Converter.zip

Es wurde auf die Verwendung von Fremd-Komponenten verzichtet. Auch werden keine speziellen DLLs benötigt. Der Source-Code lässt sich sicher leicht auf andere Delphi-Versionen anpassen. Das ausführbare Programm ist mit 550 kB recht anspruchslos. Ausserdem nimmt es keinerlei Änderungen an der Registry vor. Das Tool "ffmpeg.exe" muss allerdings im gleichen Ordner liegen wie die "flv-ccc.exe", damit die Konvertierung klappen kann!

Have fun!