FLV-Cache-Catch-Converter
FLV-CCC-Tutorial von Daniel Schwamm (20.07.2009)
Inhalt
Eigentlich kann ich es ja nicht leiden, das dreckige FLV-Format (Flash Video).
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.
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.
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.
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 ...
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.
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.
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.
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.
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.
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-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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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,nil) then 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.
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" ...
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.
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".
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.
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,nil) then 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-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.
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 ...
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.
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.
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.
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.
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.
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.
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.
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 ...
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".
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 ...
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.
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.
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?
"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!