VidSplitt - Ein GOP-genauer MPG-1-Splitter
VidSplitt-Tutorial von Daniel Schwamm (27.12.2007)
Inhalt
In diesem Delphi-Tutorial betrachten wir die Entwicklung eines Tools, mit dem man ein
Movie-File in mehrere Teile aufspalten kann, wobei jedes Einzelteil abspielbar bleibt.
Denn die Zerkleinerung oder Verkleinerung von digitalen Filmen ist angesichts
ihres grossen Speicherbedarfs sicher häufig angebracht, wenn dadurch keine
wichtigen Szenen verloren gehen. Mittels der Aufspaltung können besonders
interessante Abschnitte auf die gewünschte Länge hin isoliert werden.
Der hier vorgestellte kostenlose Videosplitter für das Windows-Betriebssystem
erlaubt es, über eine grafische
Oberfläche mit Leichtigkeit mehrere Segmente in einem Videostream zu markieren, und
diese dann mit nur einem Knopfdruck herauszuschneiden. Da dabei das Videoformat
des Filmes bestehen bleibt, also keine neue Konvertierung bzw. Resampling anfällt,
geht dieser Vorgang blitzschnell vonstatten und es sind dadurch auch keinerlei
Qualitätseinbussen bei den Einzelfilmen zu beklagen. Die so entstandenen
Kurzfilme lassen sich leichter als der Komplettfilm per E-Mail verschicken oder
auch auf einer Webseite zum Download anbieten.
Um es gleich vorwegzusagen: Bei VidSplitt handelt es sich technisch gesehen nur
um einen GOP-genauen MPEG-1-Splitter. Es lassen sich also nur Filme im
MPEG-Video-Format der Version 1 trimmen, und dies auch nicht an jeder x-beliebigen
Stelle, sondern nur an bestimmten Schlüsselpositionen, die durch den Codec vorgegeben
sind. Was dies Einschränkung genau bedeutet, werden wir im Verlauf des
Tutoriums noch erfahren. Andere gängige Videoformate wie AVI, FLV, MOV, MP4,
WMV usw. werden von VidSplitt leider nicht unterstützt.
VidSplitt, der GOP-genaue MPG-1-Video-Splitter von Daniel Schwamm
Meine Festplatte beherbergt viele Videodateien. Oft auch grosse Movies. Platz ist
eigentlich kein Problem mehr. Oft interessieren mich aber nur kleine Teile aus
den Movies, der Rest stört. Und so entstand der Wunsch nach einem Programm, welches
mir erlaubt, auf der Zeitachse eines digitalen Films Bereiche zu markieren und diese
dann per Knopfdruck automatisch herausextrahieren zu lassen.
Es gibt den hervorragenden Video-Editor TMPGEnc im Web zu finden, mit dem das geht.
Allerdings werden hier die Movies neu codiert. Das dauert lange und kostet Qualität.
VirtualDub, eine weitere Video Editing Software, kann das Ganze auch ohne Resampling.
Soweit ich weiss, aber nur mit AVIs.
Ich liebe jedoch MPGs. Die sind so knackig klein. Und v.a. erlauben sie es am besten,
innerhalb des Movies zu springen (seeken). Zumindest mit meinem eigenen Media-Player,
den ich mir vor ein paar Jahren zusammenprogrammiert habe. Von allen Multimedia-Formaten
ist das MPEG-Videoformat mir das vertrauteste und genehmste.
Ausserdem ist die Bedienung von TMPGEnc und VirtualDub nicht so mein Ding.
Ein eigenes Programm musste also her!
Schnell stellte ich bei meinen Experimenten fest, dass man MPG-Filme
an einer belieben Stelle teilen kann und der erste Teil (aber auch nur
dieser) i.d.R. noch funktioniert, sprich, von Video-Playern wie dem
Windows Media Player abgespielt werden kann. Coole Sache. Und leicht
programmiert. Ein 15-Minuten-Job in Delphi.
Das folgende kleine Programm zerteilt die übergebene Datei "fnin" ab Position
"position" und speichert den ersten Teil unter dem anderen Dateinamen "fnout"
ab. Es muss sich dabei übrigens nicht um eine MPG-Videodatei handeln; mit
jedem anderen Dateiformat funktioniert dies genauso:
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
procedure SplittAtPos(fnin,fnout:string;position:integer);
var
fin,fout:file of byte;
i:integer;
b:byte;
begin
screen.cursor:=crhourglass;
assignfile(fin,fnin);
reset(fin);
assignfile(fout,fnout);
rewrite(fout);
i:=0;
while(not eof(fin))and(i<position)do begin
read(fin,b);
write(fout,b);
inc(i);
end;
closefile(fout);
closefile(fin);
screen.cursor:=crdefault;
end;
procedure Tform1.Button1Click(Sender: TObject);
begin
splittatpos('d:\tmp\tst.mpg','d:\tmp\tst_splitt.mpg',1024000);
end;
Bei einigen wenn nicht meisten M1V-Movies - MPGs ohne Sound (intern: Elementary
Streams) - lassen sich nach dem Schreddern nicht nur der erste Teil, sondern sogar
alle Parts mit Video-Playern abspielen, nachdem man das Movie in mehrere, nicht zu
kleine (gleichgrosse) Segmente unterteilt hat. Auch ein solches
Multi-Part-Splitter-Programm war in Delphi schnell geschrieben.
Das folgende Programm zerteilt die Videodatei "tst.m1v" in fünf gleichgrosse
Stücke (der letzte Part, der Restfilm, kann unter Umständen etwas kleiner sein):
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
procedure SplittInParts(fnin:string;parts:integer);
var
fin,fout:file of byte;
fsz,partsz,i,fnc:integer;
b:byte;
fnout:string;
begin
screen.cursor:=crhourglass;
assignfile(fin,fnin);
reset(fin);
fsz:=filesize(fin);
partsz:=fsz div parts;
//Moeglichen Rest abfangen: letzter Part dann kleiner
if fsz mod parts>0 then inc(partsz);
fnc:=1;
i:=0;
while not eof(fin) do begin
read(fin,b);
//neues Ausgabefile anfangen?
if i mod partsz=0 then begin
//altes Ausgabefile schliessen
if fnc>1 then closefile(fout);
//neuen Ausgabenamen bilden
fnout:=
copy(fnin,1,length(fnin)-4)+
'_'+
inttostr(fnc)+extractfileext(fnin);
//Ausgabefile generieren
assignfile(fout,fnout);
rewrite(fout);
//Ausgabe-File-Counter erhoehen
inc(fnc);
end;
write(fout,b);
inc(i);
end;
closefile(fout);
closefile(fin);
screen.cursor:=crdefault;
end;
procedure Thauptf.Button2Click(Sender: TObject);
begin
splittinparts('d:\tmp\tst.m1v',5);
end;
Leider ist das echte Splitten von MPGs in mehrere Teile, die danach auch
einzeln abgespielt werden können, ungleich komplizierter. Und ohne etwas
Theorie auch nicht zu verstehen.
Videos im MPG-Format sind grob folgendermassen aufgebaut:
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
Magic-Number
Header
GOP (Group of Picture)
Frame
Video-Stream
Audio-Stream
Frame
Video-Stream
Audio-Stream
...
GOP (Group of Picture)
Frame
Video-Stream
Audio-Stream
Frame
Video-Stream
Audio-Stream
...
GOP (Group of Picture)
...
Frame
Video-Stream
Audio-Stream
Frame
Video-Stream
Audio-Stream
Alle Sektionen eines MPGs (Header, GOP, Frame, Streams ...) beginnen mit einer
bestimmten Nummer, bestehend aus einer Folge von 4 Bytes. Ich nenne die Dinger
Tags, weil mich das Format an HTML-Tags erinnert.
Der Aufbau ist: 0 0 1 TAG-Nummer
Es kostete mich einiges an Zeit und Nerven, bis ich im Web alle Tag-Nummern
des MPG-Formats ermittelt hatte. Wir benötigen eigentlich nicht alle, sie sind
hier der Vollständigkeit halber aber komplett aufgeführt:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
const
//mpg-tags------------------------------------
_FRAME_tag =$00;
_PADDING_tag =$be;
_VIDEO_tag =$e0;
_AUDIO_tag =$c0;
_AUDIO2_tag =$d0;
_SYSTEM_tag =$bb;
_PACK_tag =$ba;
_SEQU_tag =$b3;
_GOP_tag =$b8;
_USER_tag =$b2;
_SEQERR_tag =$b4;
_SEQEND_tag =$b7; //nur am Ende der letzten Sequenz!
_EXT_tag =$b5;
_PRGEND_tag =$b9;
_VSTREAM1_tag =$E0;
_VSTREAM2_tag =$EF;
_ASTREAM1_tag =$C0;
_ASTREAM2_tag =$CF;
_SLICE1_tag =$01;
_SLICE2_tag =$AF;
Mit einem Hex-Viewer sieht das Innere einer MPG-Datei folgendermassen aus:
Der Header ist grün markiert, die roten Stellen zeigen die Magic-Number zu
Beginn des Videos (ein _SEQU_tag), eine GOP, ein Frame und ein Slice mit
den nachfolgenden Daten. Aus Gründen der Übersichtlichkeit wählte ich
für die Demonstration das wesentlich weniger komplexe M1V-Format statt
eines MPGs.
Okay, dachte ich, ich parse also das MPG, ziehe mir dessen Header in ein
eigenes Byte-Array und merke mir dann die Positionen der einzelnen GOPs.
Denn die sind offenbar adäquate Schnittstellen im Video zur Editierung.
Konkreter: Ich schreibe den Header in eine neue Datei, hole mir dann
die Bytes ab GOP 5 bis zur GOP 10 minus 4 Bytes (der Tag-Länge) und hänge
sie an das Header-Ausgabefile einfach hinten dran. Fertig ist mein
geschnittenes Movie.
Funzte aber leider nicht. Der Windows Media Player spielte das so nicht ab.
Die Idee an sich scheint okay. Jedoch müssen einige Tag-Inhalte offenbar
an die neuen Verhältnisse angepasst werden. In den PACK-, VSTREAM- und
ASTREAM-Tags gibt es nämlich Zeitmarken, die ein Video-Player benötigt,
um Video und Sound synchronisieren sie können. Durch unsere Schnitte,
die ja Teile des ursprünglichen Videos aussparen, stimmen die Werte nun
aber nicht mehr überein oder weisen Lücken auf.
Es galt also, diese Tags bzw. deren Zeitmarken beim Wegschreiben ins neue
File entsprechend zu adaptieren. Die eigentlichen Stream-Inhalte dagegen
konnten glücklicherweise so belassen werden, wie sie waren, zumindest
soweit ich das bisher überblickte.
Das wurde heavy. Ich suchte mir eine Woche lang im Web alles zusammen,
was zu den Video-Zeitmarken passen könnte, was irgendetwas an Information
lieferte, auf die ich würde aufbauen können. Viel gefunden habe ich nicht. Doch
irgendwann wusste ich trotzdem, an welcher Stelle die Zeitmarken wie gesetzt
werden mussten.
Nur, das brachte mich seltsamerweise nicht weiter. Die so entstandenen,
nachträglich resynchronisierten MPG-Videos wurden etwa vom Windows Media
Player nach wie vor nicht akzeptiert. Auch die Video Editing Software TMPGEnc
schluckte sie nicht und brach mit Fehlmeldungen ab, wann immer ich dort
meine Schnitt-Movies einzuladen versuchte.
Ein anderer Player, der Open Source Encoder/Decoder MPlayer, spielte die
Videos dagegen tatsächlich ab! Auch wenn am Anfang der Splitting-Filme
jede Menge Schlieren und grünfarbige Blöcke auftraten. Aber es war ein
Anfang, ein Lichtblick am Ende des Horizonts. Ohne den MPlayer hätte ich
jedenfalls an dieser Stelle das Handtuch geschmissen und meinen Plan vom
eigenen Video-Cutter begraben.
Nur, was zum Teufel störte die anderen Player jetzt eigentlich noch?
Das bekam ich nur durch sehr viel Geduld und unermüdliches Analysieren
der Hex-Codes von etlichen MPG-Videos heraus. Machte keinen Spass. In
diesem Wust aus Zahlen etwas Sinnvolles herauszulesen war mühsam. Und
es bereitete mir ziemliche Kopfschmerzen. Zumal ich nie wusste, ob sich
die ganze Arbeit überhaupt lohnen würde.
Aber letztlich fand ich die entscheidende Stelle: Im MPG-Header gibt es
nämlich ein paar Bytes, die offenbar den Abstand im File zwischen dem ersten
VSTREAM im Header und dem ersten PACK in der ersten GOP angeben. Stimmt dieser
Wert nicht, geht nichts mehr; die meisten Video-Decoder verlaufen sich dann im
Stream und kehren niemals wieder.
Es ist schon komisch, aber ich habe im Web wirklich keinen verdammten Hinweis
auf dieses doch nicht eben unwichtige Faktum gefunden. Übrigens auch hinterher
nicht, nachdem ich bereits wusste, nach was ich Ausschau halten sollte.
Es folgt nun ein Beispiel für die Position und die Berechnung der Header-Länge
in einem MPG-Videofilm. Dargestellt werden die Daten über einen MPG-Tag-Viewer,
den ich eigens zu diesem Zweck in das VidSplitt-Tool mit eingebaut habe:
Berechnung der Header-Länge in einer MPG-Videodatei:
Der erste VSTREAM im Header ist markiert, File-Position 0x1234.
Ebenfalls markiert ist das erste PACK der ersten GOP, File-Position 0x1B3C.
Daraus ergibt sich eine Header-Length im markierten VSTREAM von:
0x1B3C - 0x1234 - 6 = 0x802 = 2306 Bytes!
Sollte es das gewesen sein? Ich musste in meinen Schnittvideos, die ich
bereits generiert hatte, nur diese beiden Bytes anpassen und dann ...?
Heureka! Es klappte - der Windows Media Player spielte meine Cutting-Videos ab.
Nun aber zum Delphi-Source. Da hätten wir auch schon gleich eine fette Prozedur,
"ScanFrames", die mit einem MPG gefüttert wird, in einer Schleife alle relevanten
Informationen heraus parst und diese in diverse Arrays schreibt, die zur weiteren
Verarbeitung später noch benötigt werden:
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
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
00210
00211
00212
00213
00214
00215
00216
00217
00218
00219
00220
00221
00222
00223
00224
00225
00226
00227
00228
function thauptf.ScanFrames(fn:string;var gopc:integer):integer;
const
_bufmax=1024000;
_chksz=50;
var
fh:thandle;
cc,c,fc:int64;
hc,frmc,rd,gopszc:integer;
buf:array[0.._bufmax+1]of byte;
b,bb:byte;
pbc,fbc:integer;
firstreadok,firstgopok:bool;
pts,dts:double;
i:int64;
status:string;
begin
screen.cursor:=crhourglass;
result:=-1;
mvok:=false;
status:='';
//ProgressBar, filesz liefert die Dateigroesse
pb.Max:=filesz(fn) div _bufmax;pb.position:=0;
//das Movie wir geoeffnet
fh:=FileOpen(fn,fmOpenRead);
try
try
firstreadok:=true;
firstgopok:=true;
gopmaxsz:=0;gopszc:=0;
frmno:=0;
pbc:=0;
fc:=0;
frmc:=0;
gopc:=0;
//mache so lange, bis das File durchgeparst ist
repeat
//Fortschrittsanzeige
inc(pbc);pb.position:=pbc;application.processmessages;
// dickes Byte-Array vom File einlesen
rd:=fileread(fh,buf,_bufmax);
if rd=-1 then begin
status:='FEHLER FileRead';
break;
end;
if firstreadok and(rd>=4)then begin
//ist es ueberhaupt ein MPG? Magic Number Check
if not(
(buf[0]=0)and(buf[1]=0)and(buf[2]=1)and(buf[3]=_PACK_tag)
)then begin
status:='MPG-Magic Code fehlt: Kein MPG-File!';
break;
end;
//Test wird nur einmal gemacht
firstreadok:=false;
end;
//zu wenig Daten eingelesen fuer weiteres parsen
if rd<_chksz then begin
//regulaerer Abbruch
break;
end;
//durchlaufe das Lese-Array, suche nach MPG-Tags
c:=0;
while c<rd-_chksz do begin
//MPG-Tag erwischt?
if (buf[c]=0)and(buf[c+1]=0)and(buf[c+2]=1)then begin
//die ersten drei Bytes stimmen, ist Nummer 4 eine Tag-Nummer?
b:=buf[c+3];
if b=_GOP_tag then begin
//GOP-Position merken
frmtyp[frmc]:='G';
frmpos[frmc]:=fc+c;
frmnr[frmc]:=-1;
inc(frmc);
inc(gopc);
if firstgopok then begin
//MPG Header wegsichern (fuer spaetere Split-Files)
for hc:=0 to c-1 do begin
hdbuf[hc]:=buf[hc];
end;
hdlen:=c-1;
firstgopok:=false;
end;
//merke dir die Groesse der groessten GOP
if fc+c-gopszc>gopmaxsz then gopmaxsz:=fc+c-gopszc;
gopszc:=fc+c;
end
else if b=_PACK_tag then begin
frmtyp[frmc]:='C';
frmpos[frmc]:=fc+c;
frmnr[frmc]:=-1;
inc(frmc);
end
else if
(B>=_VSTREAM1_tag)and(b<=_VSTREAM2_tag)or
(B>=_ASTREAM1_tag)and(b<=_ASTREAM2_tag)
then begin
if
firstgopok and(B>=_VSTREAM1_tag)and
(b<=_VSTREAM2_tag)
then begin
//Header-Laengen-Position merken,
//muss spaeter pro Splitt adaptiert werden
hdlenpos:=c;
end;
//Fuellbytes zu ueberspringen?
fbc:=Get_fuellbytes(@buf,c+6,_bufmax);
if fbc>20 then beep; //fehler?
//Puffer vorhanden? Dann PTS zwei Bytes weiter
cc:=c+6+fbc;
b:=buf[cc];b:=byte(b shr 7);
bb:=buf[cc];bb:=byte(bb shl 1);bb:=byte(bb shr 7);
if(b=0)and(bb=1)then cc:=cc+2;
//presentation / decode time stamp
b:=buf[cc];b:=byte(b shl 2);b:=byte(b shr 7);
bb:=buf[cc];bb:=byte(bb shl 3);bb:=byte(bb shr 7);
b:=b*2+bb;
pts:=-1;dts:=-1;
if b=0 then begin
// !PTS !DTS
frmnr[frmc]:=0;
end
else if b=1 then begin
//PTS-Fehler
frmnr[frmc]:=1;
end
else if b=2 then begin
//nur pts
pts:=get_ts(@buf,cc);
frmnr[frmc]:=2;
end
else if b=3 then begin
//pts und dts
pts:=get_ts(@buf,cc);
dts:=get_ts(@buf,cc+5);
frmnr[frmc]:=3;
end;
if(pts<>-1)or(dts<>-1)then begin
b:=buf[c+3];
if(B>=_VSTREAM1_tag)and(b<=_VSTREAM2_tag)then
frmtyp[frmc]:='V'
else
frmtyp[frmc]:='A';
frmpos[frmc]:=fc+cc;
inc(frmc);
end;
end
else if b=_FRAME_tag then begin
//gueltiges Frame erwischt?
b:=buf[c+5];b:=byte(b shl 2);b:=byte(b shr 5);
//Typ und Position merken
if b=0 then frmtyp[frmc]:='L' //Leer-Frame
else if b=1 then frmtyp[frmc]:='I'
else if b=2 then frmtyp[frmc]:='P'
else if b=3 then frmtyp[frmc]:='B'
else if b=4 then frmtyp[frmc]:='D';
frmpos[frmc]:=fc+c;
frmnr[frmc]:=frmno;
inc(frmc);
inc(frmno);
end;
end;
inc(c);
end;
//springe im File _chksz-1 zurueck,
//damit diese Bytes beim naechsten Scan beruecksichtigt werden
i:=fileseek(fh,-(_chksz-1),1);
if i=-1 then begin
status:='FEHLER FileSeek';
break;
end;
//aktuelle Position im File berechnen
fc:=fc+rd-(_chksz-1);
until rd=0;
//End-Marker setzen
frmtyp[frmc]:='G';
frmpos[frmc]:=filesz(fn);//fc+c;
//maxgopsz anpassen?
if frmpos[frmc]-gopszc>gopmaxsz then
gopmaxsz:=frmpos[frmc]-gopszc;
inc(frmc);
framec:=frmc;
result:=frmc;
except
status:='FEHLER ???';
end;
finally
fileclose(fh);
pb.position:=0;
screen.cursor:=crdefault;
if status<>'' then
application.MessageBox(pchar(status),'*** FEHLER ***',mb_ok)
else
mvok:=true;
end;
end;
Viel Stoff, oder? Was also mache ich da genau?
Ich schreibe die Bytes von 0 bis zur ersten GOP - 4 Bytes in das Header-Array "hdbuf".
Im Array "frmtyp" merke ich mir den geparsten Tag-Typ: "G" für GOP, "C" für PACK,
"L","I","G","P" und "D" für die verschiedenen FRAME-Typen, "V" für VSTREAM und "A"
für ASTREAM.
Die Positionen der Tags im File merke ich mir im Array "frmpos".
Die Frame-Nummern von Frame-Tags merke ich mir im Array "frmnr". Handelt es sich
um ASTREAM- oder VSTREAM-Tags, merke ich mir zudem, welche Arten von Zeitstempeln
sich hier befinden (DTS: decoding time stamp, PTS: presentation time stamp). Diese
Information wird später beim Wegschreiben des Splittings wieder benötigt, warum
also dann noch einmal neu scannen?
Die Anzahl aller gefunden Tags steht in "frmc", die Anzahl gefundener Frames
in "frmno". Ich kann dann später sagen: Das Tag mit der Nummer 20.223 ist ein
I-Frame, beginnend bei Position 1.226.002 im File, und es handelt sich dabei
um Bild bzw. Frame Nummer 5.667.
Ausserdem merke ich mir die Grösse der grössten gefundenen GOP in "gopmaxsz".
Um später eine beliebige komplette GOP aus dem aktuellen Movie einzuladen,
muss ich nur maximal so viel Bytes allokieren, damit es zu keinem Speicherüberlauf
kommen kann.
Des Weiteren merke ich mir die Position des ersten VSTREAM-Tags vor
der ersten GOP (also im Header) in "hdlenpos". Die Differenz zwischen diesem Wert und
dem ersten Auftreten des ersten PACKs der ersten GOP ist eben jene berüchtigte
Header-Length, die später im Header notiert werden muss (siehe oben, "Böser Bytes
im Header"). Vermutlich ist das nötig, damit die Player wissen, wo sie mit dem
Decodieren der eigentlichen Bildinformationen beginnen müssen.
Man merkt vielleicht, dass der Source "natürlich" gewachsen ist, weswegen
einige Variablen-Namen etwas unglücklich gewählt wurden, weil ich zu einem früheren
Zeitpunkt noch nicht so recht wusste, für was die gut sind. Statt "frmtyp", "frmnr"
usw. wäre z.B. "tagtyp", "tagnr" sinnvoller gewesen. Das nachträglich wieder zu
ändern war ich aber zu faul.
Die Deklaration der eingesetzten Arrays findet im Übrigen in der Klasse zur Hauptform
statt. Dadurch kann ich im Source von überall darauf zugreifen uns muss sie nicht
als Funktionsparameter übergeben:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
const
_hdbufsz=800960;
_frmmax=2000000;
class
...
public
hdbuf:array[0.._hdbufsz] of byte;
gopbufp:pchar;
gopnsbufp:pchar;
frmtyp:array[0.._frmmax]of char;
frmpos:array[0.._frmmax]of int64;
frmnr :array[0.._frmmax]of integer;
...
end;
Je nach Grösse der Movies muss die Konstante "_frmmax" gegebenenfalls vergrössert
werden. Ich habe allerdings schon 1 GB-Movies bearbeitet, denen die hier implementierte
Grösse locker genügt hat.
Des Weiteren finden folgende Funktionen in der Prozedur "ScanFrames" Verwendung:
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
//liefert Groesse des MPGs in Bytes zurueck (fuer die ProgressBar)
function thauptf.filesz(fn:string):int64;
var
sr:_WIN32_FIND_DATA;
h:thandle;
begin
result:=-1;
h:=findfirstfile(pchar(fn),sr);
try
if h<>INVALID_HANDLE_VALUE then begin
result:=(sr.nFileSizeHigh*MAXDWORD)+sr.nFileSizeLow;
end;
finally
windows.FindClose(h);
end;
end;
//ueberspringe in ASTREAM- und VSTREAM-Tags eine bestimmte
//Anzahl von Fuellbytes, damit der Array-Zeiger auf der
//exakten Startpositionen der Zeitstempel sitzt
function thauptf.get_FuellBytes(
buffer:pchar;
offset,max:integer
):Byte;
begin
Result:=0;
while(offset<max)and(byte(buffer[offset])=$ff) do begin
inc(offset);
inc(result);
end;
end;
//lese Zeitstempel aus PACKS, ASTREAM- und VSTREAM-Tags
//
//Den Source dazu fand ich im Web, in C++ (siehe Kommentarblock)
//Ich setzte ihn in Delphi um, bekam allerdings
//Ueberlaeufe, wenn in einem Movie das High-Bit gesetzt war.
//Kurzerhand habe ich das High-Bit standardmaessig auf null
//gesetzt. Es gab nie Probleme. Vermutlich
//treten die erst auf, wenn das Movie extrem
//lange ist, etliche Stunden oder so.
//
function thauptf.get_ts(buffer:pchar;offset:integer):double;
var
//highbit:byte;
low4Bytes:integer;
TS:double;
begin
//highbit:=(byte(buffer[offset])shr 3)and $01;
//muss auf null, sonst knallt es
low4Bytes :=((byte(buffer[offset]) shr 1) and $03) shl 30;
// hours
low4Bytes :=low4Bytes or byte(buffer[offset+1]) shl 22;
// minutes
low4Bytes :=low4Bytes or (byte(buffer[offset+2]) shr 1) shl 15;
low4Bytes :=low4Bytes or byte(buffer[offset+3]) shl 7;
low4Bytes :=low4Bytes or byte(buffer[offset+4]) shr 1;
//hier gibt es den Ueberlauf wenn High-Bit>0
//TS:=highbit*$10000*$10000+low4Bytes;
//TS:=TS/_STD_SYSTEM_CLOCK_FREQ;
TS:=low4Bytes;
TS:=TS/_STD_SYSTEM_CLOCK_FREQ;
result:=TS;
exit;
{
in c++:
highbit= (buffer[offset]>>3)&0x01;
low4Bytes = ((buffer[offset] >> 1) & 0x03) << 30;
low4Bytes |= buffer[offset + 1] << 22; // hours
low4Bytes |= (buffer[offset + 2] >> 1) <<15; // minutes
low4Bytes |= buffer[offset + 3] << 7;
low4Bytes |= buffer[offset + 4] >> 1;
}
end;
//_STD_SYSTEM_CLOCK_FREQ ist definiert als:
const
_STD_SYSTEM_CLOCK_FREQ=90000;
So weit, so gut. Der nächste Schritt bestand darin, ein MPG zu schreiben.
Sagen wir also, wir haben ein MPG mit 1.000 Frames. Ich will daraus nun ein
neues Movie generieren, welches nur von Frame 300 bis 500 geht.
Ich übergebe an die Schreibfunktion ein Filehandle des MPGs, das Start-Frame,
das Ende-Frame und den Namen der Zieldatei. Ich wählte ein Input-Filehandle
statt eines Dateinamens deshalb, damit das MPG nur einmal geöffnet werden muss,
auch wenn gleich mehrere Segmente daraus extrahiert werden sollen.
Und hier ist sie dann auch schon, die Funktion "SplittFile", das eigentliche
Kernstück der ganzen Arbeit:
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
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
function thauptf.SplittFile(
fhin:thandle;
frmvon:integer;
frmbis:integer;
fnnach:string
):bool;
const
_bufc=20;
var
fhout:thandle;
cc,i,si:int64;
c,rd,wr:integer;
pts,dts,
tsadd,ts,tsdiff,scr:double;
b:integer;
gopsz,r:integer;
hl,tr,nsc:integer;
packfirstok:bool;
cpok:bool;
begin
//Zeitstempel-Startwert. Fuer dessen Wert habe mich einfach
//an einem beliebigen MPG orientiert, welches ich dahingehend
//analysierte
tsadd:=0.26;
tsdiff:=0;
packfirstok:=true;
//Ausgabefile vorbereiten
fhout:=FileCreate(fnnach);
try
//Startwert auf GOP
frmvon:=frame2gop(frmvon,false);
//Endwert auf GOP
frmbis:=frame2gop(frmbis,true);
//GOP fuer GOP einlesen
for r:=frmvon to frmbis do begin
//beachte nur GOP-Tags
if frmtyp[r]<>'G' then continue;
//zur GOP-Position im File springen
i:=frmpos[r];
si:=fileseek(fhin,i,0);
if si=-1 then begin
beep;
break;
end;
//dreckige GOP einlesen
gopsz:=getgopsz(r);
rd:=fileread(fhin,gopbufp^,gopsz);
if rd=-1 then begin
beep;
break;
end;
//Time-Codes neu setzen
tr:=r+1;
repeat
if frmtyp[tr]='C' then begin
//ist ein PACK, hat also Zeitstempel
//Position im File
cc:=frmpos[tr];
//Position im Lese-Puffer
cc:=cc-i;
//SCR holen (system clock reference)
scr:=get_ts(gopbufp,cc+4);
//erstes PACK i im ersten Treffer-GOP?
if packfirstok then begin
//erstes neue PACK, Zeitdifferenz berechnen
tsdiff:=scr-0.02;
//Header-Length fuer ersten VSTREAM berechnen
hl:=hdlen+cc+1-hdlenpos-6;
//hl in Header schreiben
b:=byte(hl div 256);hdbuf[hdlenpos+4]:=b;
b:=byte(hl mod 256);hdbuf[hdlenpos+5]:=b;
packfirstok:=false;
end;
//neuen SCR berechnen
ts:=scr-tsdiff+tsadd;
//und zurueckschreiben
put_ts(gopbufp,cc+4,ts);
end
else if
//((frmtyp[tr]='A')and soundchb.Checked)or
(frmtyp[tr]='A')or
(frmtyp[tr]='V')
then begin
//Audio bzw. Video-stream
//Position im File
cc:=frmpos[tr];
//Position im Puffer
cc:=cc-i;
b:=frmnr[tr];
pts:=-1;dts:=-1;
if b=0 then begin
// !PTS !DTS
end
else if b=1 then begin
//PTS-Fehler
beep;
end
else if b=2 then begin
//nur PTS
pts:=get_ts(gopbufp,cc);
end
else if b=3 then begin
//PTS und DTS
pts:=get_ts(gopbufp,cc);
dts:=get_ts(gopbufp,cc+5);
end;
//PTS neu berechnen?
if pts>-1 then begin
ts:=pts-tsdiff+tsadd;
//s:=s+'PTS:'+ftoa(pts)+'->'+ftoa(ts)+' ';
put_ts(gopbufp,cc,ts);
end;
//DTS neu berechnen?
if dts>-1 then begin
ts:=dts-tsdiff+tsadd;
//s:=s+'DTS:'+ftoa(dts)+'->'+ftoa(ts)+' ';
put_ts(gopbufp,cc+5,ts);
end;
end;
inc(tr);
until frmtyp[tr]='G';
//adaptierten Header einmal an Anfang schreiben
if r=frmvon then begin
wr:=filewrite(fhout,hdbuf,hdlen+1);
if wr=-1 then
beep;
end;
//jede adaptierte GOP anhaengen
wr:=filewrite(fhout,gopbufp^,rd);
if wr=-1 then begin
beep;
end;
end;
result:=true;
finally
fileclose(fhout);
end;
end;
Gar nicht mal so lange, oder? Knackig-elegant gelöst, wie ich finde :-)
Mit der Funktion "frame2gop" wandle ich die gewünschte Frame-Position
zu einer GOP-Position um. Wenn ich also wie gesagt von Frame 300 bis 500
cutten will, mache ich das tatsächlich nicht wirklich so. Ich arbeite
vielmehr nur mit Näherungswerten.
MPG-Filme haben ein paar Eigenschaften, die dienlich sind, um sie leicht
abzuspielen. Das Editieren derselben machen sie dagegen schwer. In die
Details will ich gar nicht eingehen, nur so viel:
Jede GOP baut sich aus verschieden Frames auf, die typischerweise
folgendermassen organisiert sind:
00001
GOP IBBPBBPBBPBBPBBPBB GOP IBBPBBPBBPBBPBBPBB GOP IBBPBBPBBPBBPBBPBB ...
I-Frames sind quasi JPG-Bilder. P-Frames sind Bilder, die nur die Änderungen
des vorherigen I-Frames enthalten. Und B-Frames sind eine Art Morphing-Struktur,
die vom I- zum P-Frame führt.
Man kann also eigentlich nicht direkt bei einem B- oder P-Frame schneiden,
denn ohne das zugehörige I-Frame ist die Information darin für den Player
nicht zu interpretieren. Beginnt ein Movie mit einem B- oder P-Frame, zeigt
sich das typischerweise in grün geblockten Bildern, bis das erste I-Frame
auftaucht.
Und es wird noch komplizierter. Die ersten B-Frames einer GOP gehören
nämlich nicht zum ersten I-Frame der GOP, wie man annehmen könnte. Sie
gehören vielmehr zum letzten I-Frame der vorherigen GOP!
Nachdem, was man so liest, gibt es wohl einige wenige Video-Editoren, die
tatsächlich ein MPG exakt bei seinen Frames unterteilen können. Wie genau
sie das anstellen, weiss ich nicht. Ist mir auch egal.
Schon deutlich mehr Programme können bei den I-Frames schneiden. VirtualDub
und TMPGEnc gehören wohl in diese Kategorie. Aber auch nur das zu realisieren
war mir zu mühsam.
Ich schneide nur GOP-genau. Das zu erreichen war schwer genug. Zudem
wäre ein Frame-genaues Aufspalten des Filmes bei meinem Source ohnehin nicht
drin gewesen, da die TMediaPlayer-Komponente von Delphi Frame-Positionen liefert,
die nicht mit meinem geparsten MPG-Tags übereinstimmen; was TMediaPlayer als
Frame 1.000 ausgibt, ist bei mir vielleicht Frame 1.010. Aber dazu später
mehr.
Okay, ich zerteile also einen Film nur auf Basis kompletter GOPs. Wenn ich ab
Frame 300 schneiden will, muss ich zuerst diese Frame-Nummer im Array "frmnr"
ermitteln. So erhalte ich die Position des Frames im Array "frmtyp". Nun gehe
ich in diesem Array zurück, bis ich den Tag-Typ "GOP" finde. Um die Endposition,
also Frame 500, zu ermitteln, gehe ich genauso vor, nur das ich diesmal in
"frmtyp" nicht zurück, sondern nach vorne laufe, bis ich erneut eine GOP finde
(bzw. das Dateiende).
Mit anderen Worten: Wenn ich angebe, von Frame 300 bis 500 zu schneiden, dann
schneide ich vermutlich eher etwas in der Art wie Frame 289 bis Frame 504!
Nicht sehr genau, aber mir genügt's vollauf.
Die "Konvertierung" von Frame zu GOP erledigt die Funktion "frame2gop":
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
//suche die vorherige oder folgende GOP zu einer Frame-Nummer
function thauptf.frame2gop(fnr:integer;nextok:bool):integer;
var
r:integer;
foundok:bool;
begin
//Framen-Nummer im Tag-Array finden
foundok:=false;
for r:=0 to framec-2 do begin
if frmnr[r]<>fnr then continue;
foundok:=true;
fnr:=r;
break;
end;
if not foundok then begin
//Frame nicht gefunden
if not nextok then begin
//nimm erste gop
for r:=0 to framec-2 do begin
if frmtyp[r]<>'G' then continue;
result:=r;
exit;
end;
end
else begin
//nimm letzte GOP
for r:=framec-2 downto 0 do begin
if frmtyp[r]='G' then begin
result:=r;
exit;
end;
end;
end;
//Fehler
result:=-1;
exit;
end;
if fnr<0 then fnr:=0;
if(fnr>framec-2)or(fnr>_frmmax-2) then fnr:=framec-2;
if not nextok then begin
for r:=fnr downto 0 do begin
if frmtyp[r]='G' then begin
result:=r;
exit;
end;
end;
result:=0;
end
else begin
for r:=fnr to framec-2 do begin
if frmtyp[r]='G' then begin
result:=r;
exit;
end;
end;
result:=framec-2;
end;
end;
Habe ich so also die Start- und End-GOP ermittelt, kann ich nun jede
GOP einzeln aus dem MPG einlesen.
Über das Array "frmpos" erfahre ich die genaue Position der GOP
im MPG-File, kann also per File-Seek direkt an die richtige Stelle springen,
ohne die Datei neu parsen zu müssen. Das bringt Speed ohne Ende!
Die interne Funktion "getgopsz" berechnet die Grösse der jeweiligen GOP.
Sie ergibt sich aus der Differenz der Fileposition der GOP zur Fileposition
der nächsten GOP. Die maximale GOP-Grösse wurde ja zuvor in "ScanFrames"
ermittelt, mehr Speicherplatz für den Lese-Puffer muss nicht allokiert werden.
Innerhalb einer so geladenen GOP befinden sich nun all die PACK- und VSTREAM-
und ASTREAM-Tags, die Zeitstempel besitzen, die umgeschrieben werden müssen. Ihre
exakte Position kann direkt mittels der Arrays "frmtyp" und "frmpos" ermittelt
werden (Position im Lese-Puffer ist die Differenz der Fileposition des Tags
zur Fileposition des GOP-Tags).
Mittels der Funktion "get_ts" wird der jeweilige Zeitstempel eingelesen (siehe
weiter oben). Da wir mitten ins Video gesprungen sind, steht hier etwas in der
Art: "Lieber Player, wenn du dieses Frame findest, dann spiele es nach 22 Sekunden
seit Videobeginn ab".
Tatsächlich ist es so, das ein Player ein geschnittenes Video, dass die Zeitstempel
unverändert lässt, nach dem Start exakt diese Zeit mit einem stehendes I-Frame-Picture
wartet, bevor er dann das Movie korrekt abspielt.
Also muss der Zeitstempel logischerweise auf einen kleineren Wert gesetzt werden.
Einfach einen eigenen Wert neu berechnen geht nicht, da die Anzahl angezeigter
Frames je Sekunde je nach Sampling Rate unterschiedlich ist, ja, sich innerhalb eines
Movies auch permanent verändern kann. Erschwerend kommt hinzu, dass die I-, B- und
P-Frames nicht in der Reihenfolge abgespielt werden, wie sie im Movie auftauchen.
Denn die ersten Frames einer GOP, die angezeigt werden, sind die B- und P-Frames,
die hinter dem ersten I-Frame liegen - ich erwähnte das schon
weiter oben. Vermutlich erleichtert dieses merkwürdige Verhalten den Playern das
Decodieren der Bilder.
Nun ja, zum Glück kann uns das Wurst sein. Finden wir den ersten Time-Code, merken
wir ihn uns in "tsdiff". Nur diesen Time-Code müssen wir quasi auf einen Nullwert
setzen. Von allen anderen gefundenen Zeitwerten wird einfach stur "tsdiff" abgezogen,
damit's passt.
Das Schreiben des Zeitstempels erledigt die Funktion "put_ts":
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
//schreiben der Zeitstempel
/c++ source im Web geklaut, weiss nicht mehr wo
procedure thauptf.put_ts(buffer:pchar;offset:integer;ts:double);
var
uppercode:byte;
hibit:byte;
ats,d:double;
lowint:integer;
begin
ats:=ts;
uppercode:=byte(buffer[offset]) and $F0;
ats:=ats*_STD_SYSTEM_CLOCK_FREQ;
d:=$10000;
d:=d*$10000;
if ats>d then begin
hiBit:=1;
ats:=ats-d;
lowInt:=trunc(aTS);
end
else begin
hiBit:=0;
lowInt:=trunc(aTS);
end;
uppercode:=
uppercode or
(hiBit shl 3) or
((lowInt and $C0000000) shr 29) or
$01;
lowInt:=
$00010001 or
((lowInt and $3FFF8000) shl 2) or
((lowInt and $00007FFF) shl 1);
buffer[offset] := char(uppercode);
buffer[offset+1] := char(lowInt shr 24);
buffer[offset+2] := char((lowInt shr 16)and $FF);
buffer[offset+3] := char((lowInt shr 8)and $FF);
buffer[offset+4] := char(lowInt and $FF);
{
byte uppercode = buffer[offset] & 0xF0;
byte hiBit;
long lowInt=0;
double TS=ts;
int hour;
int minute;
int sec;
int pics;
TS *= STD_SYSTEM_CLOCK_FREQ;
if (TS > FLOAT_0x10000 * FLOAT_0x10000) KLAMMER-auf
hiBit = 1;
TS -= FLOAT_0x10000 * FLOAT_0x10000;
KLMMAER-zu
else KLAMMER-auf
hiBit = 0;
lowInt = (long) TS;
KLAMMER-zu
uppercode = uppercode |
(hiBit << 3) |
((lowInt & 0xC0000000) >> 29) |
0x01;
lowInt = 0x00010001 |
((lowInt & 0x3FFF8000) << 2) |
((lowInt & 0x00007FFF) << 1);
buffer[offset] = uppercode;
buffer[offset+1] = (byte)(lowInt>>24);
buffer[offset+2] = (byte)((lowInt>>16)&0xFF);
buffer[offset+3] = (byte)((lowInt>>8)&0xFF);
buffer[offset+4] = (byte)(lowInt&0xFF);
}
end;
Befinden wir uns in der ersten neu zu schreibendem GOP, kommt die erwähnte Geschichte
zum tragen, dass der Abstand zwischen erstem PACK der ersten GOP und erstem VSTREAM
des Headers im Header vermerkt werden muss. Das erledigen folgende Zeilen:
00001
00002
00003
00004
00005
hl:=hdlen+cc+1-hdlenpos-6;
//hl in head schreiben schreiben
b:=byte(hl div 256);hdbuf[hdlenpos+4]:=b;
b:=byte(hl mod 256);hdbuf[hdlenpos+5]:=b;
Wir erinnern uns: "hdlen" gibt die Grösse des Headers an, "hdlenpos" die
Position im Header, wo der erste VSTREAM steht. "hl" berechnet nun die
gesuchte Differenz der Tags, wobei "cc" die aktuelle Position im Lese-Puffer
angibt (hdlen+cc zeigt also auf das erste PACK der ersten GOP).
Komplizierte Geschichte. Und das sollte sie auch sein. Diese drei Zeilen
Source haben mich viel Nerven gekostet, bis ich sie ausgeschwitzt hatte.
Okay, die in den GOPs enthaltenen Zeitstempel wurden also adaptiert. Der Header
wurde auch angepasst. Jetzt muss das Ganze nur noch GOP für GOP ins Ausgabefile
weggeschrieben werden. That's it!
Das Splitten der MPGs klappte, nun fehlt noch eine Oberfläche.
Ich warf also eine TMediaPlayer-Komponente und eine TTrackBar auf die Delphi-Form.
Nach der Auswahl des MPG-Filenames per File-Open-Dialog wird "ScanFrames" aufgerufen,
danach das Video mit dem Player geöffnet, dessen Ausgabe auf ein TPanel umgelenkt,
und in dem OnChange-Ereignis der TTrackBar die TrackBar-Position mit der Player-Position
abgeglichen.
Die Idee war nun, mit der TrackBar einen bestimmten Bereich im Video zu selektieren,
wobei das Video ja optisch im Ausgabe-Panel angezeigt wird, und dann diesen Bereich
per Knopfdruck mit der "SplittFile"-Funktion ohne Qualitätsverluste aus dem Video
herauszuschneiden.
Zu meiner Überraschung musste ich jedoch feststellen, dass die Anzahl
Frames, die die MediaPlayer-Komponente ermittelt, häufig deutlich von der Anzahl Frames
abweicht, die ich durch meine "ScanFrames"-Funktion mühsam selbst ermittelt hatte.
Die Unterschiede sind zum Teil echt krass: TMediaPlayer lieferte z.B. einmal
satte 200.000 Frames, während ich selbst nur etwa 3000 echte Frames im Video gefunden
hatte. Ein Check mit VirtualDub und TMPGEnc zeigte im Übrigen, dass ich Recht hatte
(obwohl es auch hier zu ungeklärten Differenzen kommt, was sich aber auf einige
wenige Frames Unterschied beschränkt).
Offenbar berechnet TMediaPlayer die Anzahl Frames aus der Spieldauer und der
Sampling Rate oder so. Dadurch "weiss" die Komponente auch bei grossen Movies
sofort, wie viele Frames angezeigt werden müssen. Mein Parser - und die der
anderen Videoprogramme - benötigen einige Zeit, bis sie dieses Ergebnis
auswerfen.
Unterschiedlich viele Frames im MPG: TMediaPlayer versus VirtualDub:
TMediaPlayer findet 80.538, VirtualDub dagegen nur 2.405 Frames
Ich analysierte also die TMediaPlayer-Komponente, und prüfte, ob ich irgendetwas bezüglich
der Frame-Anzahl würde drehen können. Letztlich blieb ich aber in irgendwelchen
Microsoft-APIs hängen. Hier kam ich nicht weiter.
Okay, blieb nur, die von mir so genannten MCI-Frames (die des TMediaPlayers) auf meine
geparsten Frames umzurechnen. Dafür gibt's die Funktion "mcifrm2frm":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
//liefert je MCI-Frame den passenden Scan-Frame
function thauptf.mcifrm2frm(i:integer):integer;
var
d:double;
begin
result:=0;if mcifrmno=0 then exit;
d:=frmno/mcifrmno;
i:=trunc(i*d);
result:=i;
end;
Also: In "mcifrmno" steht die Anzahl Frames, die TMediaPlayer gefunden hat,
in "frmno" meine herausgeparste echte Frame-Anzahl. Ist "mcifrmo" also 1.000 und
"frmno" 100, dann bilde ich den MCI-Frame 500 auf den "echten" Frame 500*(100/1.000)=50
ab. Nicht besonders tricky, funktionierte aber zu meiner eigenen Überraschung
erstaunlich gut.
Nun zur TTrackBar: Diese Delphi-Komponente besitzt Eigenschaften, mit denen
ein Bereich grafisch markiert werden kann. Prima, dachte ich, genau das, was ich
brauche. Nun ja, eigentlich wollte ich ja ursprünglich gleich mehrere Bereiche
selektieren können, aber okay, für's erste genügt's.
Die Selektion mit der TrackBar war leicht programmiert. Ich wählte einen Start- und
ein Ende-Frame, beides MCI-Frames, rechnete sie mit "mcifrm2frm" in meine Scan-Frames um,
übergab das an die "SplittFile"-Funktion, wo wiederum die Start- und Ende-GOPs ermittelt
wurden, bis schliesslich der eigentliche Cut vorgenommen wurde.
Klar, von einem exakten Schnitt konnte bei all der Umrechnung kein Rede mehr sein,
aber was sind schon 100 Frames plus/minus daneben? Nur maximal ein paar Sekunden
falsche Bildinformation. Da pfeife ich doch drauf!
Der Wunsch nach mehreren Selektionen war auch leicht erfüllt, wenn auch nicht
direkt grafisch sichtbar: Ich machte mit der TrackBar eine Selektion und trug die
ermittelten Von-Bis-Werte in eine ListBox ein, dann die nächste Selektion usw.
Anschliessend öffnete ich mein Video, lief die ListBox Zeile für Zeile durch,
bildete durchnummerierte Dateinamen für die Ausgabe und rief jeweils die Funktion
"SplittFile" auf. Klappte einwandfrei.
Mehrere MPG-Splitt-Selektionen: Die Von-Bis-Schnittbereiche in der 'goplb'-ListBox und die daraus generierten erfolgreichen Schnittvideos.
Noch ein SpinEdit dazu ("autose"), in dem ich die Anzahl gewünschter Schnitte eingab,
und ich konnte per Knopfdruck die Schnitte-ListBox alternativ auch automatisch füllen.
Auf diese Weise liess sich ein MPG z.B. sehr einfach in 10 etwa gleichgrosse Teile
zerteilen.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
procedure Thauptf.autobClick(Sender: TObject);
var
r,von,bis:integer;
dx,d:double;
begin
if autose.value=0 then exit;
goplb.Clear;
d:=mcifrmno/autose.value;
dx:=0;
bis:=0;
for r:=0 to autose.value-1 do begin
von:=bis;
bis:=trunc(von+d);
goplb.Items.add(fill(von,7)+':'+fill(bis,7));
dx:=dx+d;
end;
formresize(self);
end;
Multi-Auto-Video-Splitting: Die automatisch generierten Schnitt-Bereiche in der 'goplb'-ListBox und die daraus erfolgreich generierten Schnittvideos.
Sauber Daniel! Jetzt die Oberfläche noch etwas grafisch aufmotzen. Also gleich mal
das XPManifest auf die Form geschmissen, dass verpasst einem jeden Delphi-Proggy
sofort einen edleren Look.
Aber was war das? Grafisch sah's jetzt schöner aus, aber wo war bei der TrackBar
meine Selektion hin verschwunden? Sie war durchaus noch vorhanden, nur wurde
sie leider optisch nicht mehr angezeigt. Nach einiger Suche im Web erfuhr ich,
dass dies ein bekannter Bug vom XPManifest bzw. der TrackBar ist. Nicht die
einzige Unzulänglichkeit vom XPManifest übrigens. Da haben die Borlander wohl
geschlampt.
TrackBar-Selektion I: Erster Fehler bei der TrackBar. Ohne XPManifest sieht man die Selektion, mit XPManifest dagegen nicht.
Okay, okay, ich kann auch ohne XPManifest leben, also wieder runter damit. Doch
die nächste böse Überraschung liess nicht lange auf sich warten. In der Testphase
habe ich - natürlich - nur mit kleinen MPGs gearbeitet. Da ich das Proggy sicher
einige 100 mal kompiliert und gestartet habe, wäre es ja blödsinnig gewesen, jedes
Mal einen langen Frame-Scan-Vorgang abwarten zu müssen.
Und so musste ich bei den ersten grossen Movies mit mehr als 32.000 Frames
feststellen, dass sich mit der TrackBar zwar ohne Probleme auch 200.000 Frames
auswählen lassen. Nur Selektionen, die in Bereiche über 32.000 reichen, werden
optisch nicht mehr angezeigt.
TrackBar-Selektion II: Zweiter Fehler bei der TrackBar. Selektionsbereiche über eine Spanne von 32.000 Einheiten werden nicht mehr angezeigt.
Aaarg! Die wollen mich ärgern, die Borlander (das sind die Entwickler von Delphi)!
Also dachte ich, gut, leite ich eine eigene Klasse von TTrackBar ab und sehe zu,
dass ich diesen Fehler beseitige. Vielleicht würde ich's ja sogar schaffen, dass
meine so entstehende TDanTrackBar sich dann auch mit dem XPManifest vertrug.
Der Grenzwert, bei der die Selektion versagte, roch sofort nach 16 bit Single-
Integer-Überlauf; hier liegt nämlich der Wertebereich in etwa zwischen plus/minus
32.000. Vermutlich musste ich nur an passender Stelle ein Single in ein Integer
wandeln und die Sache war gegessen.
Dem war aber nicht so. Letztlich endete meine Analyse der TTrackBar wie schon
beim TMediaPlayer in Microsoft API-Calls. Per SendMessage werden hier die
Selektionswerte übergeben, und zwar explizit als 32 Bit-Integer! Irgendwo fand
ich dann auch den Hinweis, dass man tatsächlich grosse Werte übergeben kann,
diese aber nicht korrekt als Selektion angezeigt würden. Borland waren
also unschuldig, Microsoft hatte geschlampt!
API-Message TBM_SETSEL: An die TrackBar werden 32-Bit-Werte zum Setzen des Selektionsbereiches übermittelt, aber offenbar nicht korrekt verwendet.
Ich hatte die Nase voll. TDanTrackBar liess ich sofort wieder sterben. Und die
Original-TTrackBar verschwand ebenso von meiner Form. Stattdessen nahm ich eine
TScrollBar, nannte sie "sc", und verband sie mit der Positionierung des Videos.
Mit TScrollBar kann kein Bereich optisch selektiert werden, nicht einmal falsch,
so wie bei der TTrackBar. Aber wenn ich's schon selbst programmieren muss, dann
doch mit der mir genehmeren Von-Bis-Komponente.
Hinter die ScrollBar legte ich eine TPaintBox "selpb". Zwei globale Variablen,
"selstart" und "selend", merken sich die Grenzwerte. Eine globale Bitmap "selbmp"
speichert die Grafik, die in der PaintBox beim OnPaint-Ereignis angezeigt wird.
Und diese Grafik wird zuvor in der Funktion "mktbsel" generiert.
Aufbau einer eigene Selektionskomponente in Delphi: Unter der ScrollBar liegt eine PaintBox zur Wiedergabe der Selektion.
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
//male selbmp in selpb-Bereich auf die Form -----------------
procedure Thauptf.selpbPaint(Sender: TObject);
begin
if selbmp=nil then exit;
try
bitblt(
selpb.Canvas.Handle,0,0,selpb.width,selpb.Height,
selbmp.canvas.handle,0,0,
srccopy
);
except
end;
end;
//male Selektion auf selbmp-------------------------
procedure thauptf.mktbsel;
var
i,c,dx,von,bis,w:integer;
d,dz:double;
clr,cl:tcolor;
s:string;
begin
dx:=_pbdx;
w:=(sc.Width-(2*dx)-14);
d:=w/(sc.Max-sc.Min);
//Selection leeren
clr:=clbtnface;cl:=clbtnface;
selbmp.Canvas.pen.color:=clr;
selbmp.Canvas.Brush.color:=cl;
selbmp.Canvas.Brush.style:=bssolid;
selbmp.Width:=selpb.Width;
selbmp.Height:=selpb.Height;
selbmp.Canvas.Rectangle(0,0,selbmp.Width,selbmp.height);
clr:=clgray;cl:=clsilver;
selbmp.Canvas.pen.color:=clr;
selbmp.Canvas.Brush.color:=cl;
selbmp.Canvas.Rectangle(dx+sc.left,0,dx+w+sc.left,selbmp.height);
//Selektion einzeichnen
clr:=clblack;cl:=clblue;
selbmp.Canvas.pen.color:=clr;
selbmp.Canvas.Brush.color:=cl;
von:=trunc(dx+(selstart-sc.Min)*d)+sc.left;
bis:=trunc(dx+(selend -sc.Min)*d)+sc.left;
if von=bis then bis:=von+1;
selbmp.Canvas.Rectangle(von,0,bis,selbmp.height);
//Selektionsbereich?
if bis-von>1 then begin
s:='SELEKTION: '+getsplittinfo(selx2frm(von),selx2frm(bis));
wrstat('-1',s);
end;
//Zaehlerstriche
selbmp.Canvas.Brush.style:=bsclear;
clr:=clgray;
selbmp.Canvas.pen.color:=clr;
i:=(sc.Max-sc.min) div sc.largechange;
if i>500 then i:=500;
dz:=w/i;
bis:=selbmp.Height;
for c:=1 to i-1 do begin
von:=dx+sc.left+trunc(c*dz);
selbmp.canvas.MoveTo(von,bis-5);
selbmp.canvas.lineTo(von,bis);
end;
//Zaehlerwerte
selbmp.Canvas.Font.Name:='MS Sans Serif';
selbmp.Canvas.Font.size:=6;
selbmp.Canvas.Font.color:=clgray;
i:=18;
von:=dx+sc.left+2;
s:=inttostr(sc.min);
selbmp.Canvas.TextOut(von,bis-i,s);
von:=dx+sc.left+(w div 2);
s:=inttostr(sc.min+(sc.max-sc.min)div 2);
von:=von-selbmp.Canvas.TextWidth(s) div 2;
selbmp.Canvas.TextOut(von,bis-i,s);
von:=dx+sc.left+w;
s:=inttostr(sc.max);
von:=von-selbmp.Canvas.TextWidth(s)-2;
selbmp.Canvas.TextOut(von,bis-i,s);
end;
In "mktbsel" wird zunächst die Bitmap in ihrer Grösse der darüber liegenden
ScrollBar angepasst. Dann wird sie mit der Hintergrundfarbe versehen. Ein
innerer Bereich, der von der ScrollBar-Knopf Start- bis End-Position reicht,
wird anschliessend silberfarbend gefüllt. Nun wird zusätzlich der mit "selstart"
und "selend" definierte Bereich blau eingefärbt; liegt keine Selektion vor,
wird nur ein Strich bei der "Cursor"-Position gemalt. Zuletzt kommen an den unteren
Rand die Skalenstriche. Start, Mitte und Ende werden zudem mit Skalenwerten
versehen.
ScrollBar-Selektion mit eigener Selektionskomponente in Delphi: Daniels hübsche ScrollBar-PaintBox-Selektionskomponente oder auch kurz SelektionsPaintBox genannt.
Es war eine ziemliche Fummelei, bis alle Bereiche da waren, wo sie hin sollten.
Und das adaptiv, denn diese Kollektion von Komponenten muss ja auch korrekt auf
OnResize-Ereignisse der Form reagieren. Im Vergleich jedoch zur Timestamp-
Berechnung und -Setzung innerhalb von MPG-Videofilmen war's ein Spaziergang :-)
Die Selektion eines Bereiches mit obiger Konstruktion erfolgt folgendermassen:
Mit der Maus verschiebt man den Knopf der ScrollBar. Das Video zeigt die zugehörigen
Frames an, die SelektionsPaintBox einen "Cursor"-Balken. So wie man die STRG-Taste
drückt, wird "selstart" festgelegt und "selend" auf die Scroll-Position gesetzt.
Anschliessend wird "mktbsel" aufgerufen und der Selektionsbereich in Echtzeit
blau markiert. Geregelt wird dies alles über das OnChange-Ereignis der ScrollBar:
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
procedure Thauptf.scChange(Sender: TObject);
var
i,rc:integer;
begin
i:=sc.position;
//ist STRG gedrueckt?
rc:=getkeystate(VK_control);
if rc<0 then begin
if i<selstart then begin
selend:=selstart;
selstart:=i;
end
else begin
selend:=i;
end;
end
else begin
selstart:=i;
selend:=i;
end;
mktbsel;
selpbPaint(self);
sc.Position:=i;
mp.position:=sc.position;
wrstat('Frame: '+inttostr(mp.position),'-1');
setbuttons;
end;
Die Funktion "wrstat" gibt im ersten Panel der Statusbar den aktuellen
Frame an, der mittels der ScrollBar ausgewählt wurde.
00001
00002
00003
00004
00005
procedure thauptf.wrstat(s1,s2:string);
begin
if s1<>'-1' then statb.Panels[0].text:=s1;
if s2<>'-1' then statb.Panels[1].text:=s2;
end;
Die Funktion "setbuttons" aktiviert Menüs und diverse Buttons auf der Form,
je nachdem, ob ein Video geladen ist, ob ein zerteilbares MPG geladen wurde,
oder ob ein Bereich selektiert wurde. Dazu später mehr.
Nun habe ich mir ja in den Kopf gesetzt, nicht nur einen, sondern gleich
mehrere Bereiche selektieren zu können. Die Eintragungen der Selektionsbereiche
in eine ListBox, wie oben beschrieben, funktioniert nach wie vor. Sieht aber
nicht so schön aus. Besser wäre es, alle gewählten Selektionsbereiche unter
der ScrollBar sauber anzuzeigen.
Dazu benötigte ich eine weitere TPaintBox "pntb", die ich direkt unter die
SelektionsPaintBox "selpb" legte. Habe ich also einen Bereich gewählt,
klicke ich auf einen Knopf und trage die Von-Bis-Werte in meine ListBox,
die ich "goplb" genannt habe. Der Name passt nicht (mehr) ganz, "schnittelb"
oder so wäre treffender. Aber egal.
In der "goplb" landen jedenfalls die Von-Bis-Werte, jeweils mit Nullen auf
sieben Stellen aufgefüllt, getrennt durch einen Doppelpunkt. Die Auffüllung
der Grenzwerte mit Nullen macht die Liste etwas übersichtlicher und hat den
grossen Vorteil, dass die Liste später sortiert werden kann, wodurch Schnitte,
die bei niedriger Frame-Nummer beginnen, vor denen mit höhere Frame-Nummer liegen.
Man muss also bei der Bestimmung der Schnittbereiche nicht stur sequenziell
vorgehen, sondern kann von mir aus auch das Video von hinten nach vorne nach
interessanten Stellen absuchen. Die generierten Split-Files sind - wenn die
Liste aufsteigend sortiert wurde - dennoch in der logischen Reihenfolge
durchnummeriert.
Sortierung der selektierten Videoschnitte: Die GOP-Liste 'goplb' vor und nach der Sortierung.
Okay, weiter im Source. Um einen Schnittbereich in die ListBox einzutragen,
wird folgende Funktion verwendet:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
//fuelle String mit vorangehenden Nullen bis Laenge len erreicht
function thauptf.fill(i:integer;len:integer):string;
begin
result:=inttostr(i);
while length(result)<len do result:='0'+result;
end;
procedure Thauptf.splitsetbClick(Sender: TObject);
var
s:string;
begin
if(selrow<>-1)and splitwrkok then begin
//gewaehlten Splitt adaptieren
s:=fill(selstart,7)+':'+fill(selend,7);
goplb.Items[selrow]:=s;
FormResize(sender);
exit;
end;
//neuen Splitt setzen
s:=fill(selstart,7)+':'+fill(selend,7);
goplb.Items.add(s);
FormResize(sender);
end;
Die Variablen "selrow" und "splitwrkok" sind Globale, die angeben,
ob ein bestehender Selektionsbereich mit den neuen Selektionswerten
"selstart" und "selend" verändert werden soll, oder ob ein neuer
Schnitt in die ListBox "goblb" eingetragen werden soll. "selwrow"
zeigt dabei auf den ItemIndex (den aktuell gewählten Schnitt) der
"goplb".
Ziemlich simpel sind die Zusatzoperationen mit der "goplb":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
//loesche alle schnitte
procedure Thauptf.splitlstdelbClick(Sender: TObject);
begin
goplb.Clear;
FormResize(sender);
end;
//loesche nur selektierten schnitt
procedure Thauptf.splitdelbClick(Sender: TObject);
begin
goplb.DeleteSelected;
FormResize(sender);
end;
//sortiere schnitt-liste
procedure Thauptf.splitsortbClick(Sender: TObject);
begin
goplb.Sorted:=true;
goplb.Sorted:=false;
end;
Oben haben wir die PaintBox "pntb" erwähnt, die die Eintragungen
in der ListBox "goplb" optisch unter unserer ScrollBar wiedergeben
soll. Dazu wird in der Funktion "mkpntb" die Bitmap "bmp" gefüllt und
beim OnPaint-Ereignis von "pntb" angezeigt.
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
procedure Thauptf.pntbPaint(Sender: TObject);
begin
if bmp=nil then exit;
try
bitblt(
pntb.Canvas.Handle,0,0,pntb.width,pntb.Height,
bmp.canvas.handle,0,0,
srccopy
);
except
end;
end;
//generiere Bitmap bmp fuer Selektionsbereiche im mpg, paint auf pntpb
procedure thauptf.mkpntb;
var
w,dx,c,r,von,bis:integer;
s:string;
d:double;
clr,cl:tcolor;
begin
//sc positionieren
sc.Left:=8;
sc.Height:=13;
sc.width:=selpb.width-16;
sc.Top:=selpb.Top+((selpb.Height-sc.height) div 3);
mktbsel;
dx:=_pbdx;
w:=(sc.Width-(2*dx)-14);
d:=w/(sc.Max-sc.Min);
//Bitmap leeren
clr:=clbtnface;cl:=clbtnface;
bmp.Canvas.pen.color:=clr;
bmp.Canvas.Brush.color:=cl;
bmp.Canvas.Brush.style:=bssolid;
bmp.Width:=pntb.Width;
bmp.Height:=pntb.Height;
bmp.Canvas.Rectangle(0,0,bmp.Width,bmp.height);
clr:=clgray;cl:=clsilver;
bmp.Canvas.pen.color:=clr;
bmp.Canvas.Brush.color:=cl;
bmp.Canvas.Rectangle(dx+sc.left,0,dx+w+sc.left,bmp.height);
//bmp mit Splitts fuellen
//d:=(sc.Width-_pbdx)/sc.Max;
//dx:=_pbdx;
for r:=0 to goplb.items.count-1 do begin
s:=goplb.Items[r];
c:=pos(':',s);if c=0 then c:=length(s)+1;
von:=strtoint(trim(copy(s, 1,c-1 )));
bis:=strtoint(trim(copy(s,c+1,length(s))));
von:=trunc(dx+(von-sc.Min)*d)+sc.left;
bis:=trunc(dx+(bis-sc.Min)*d)+sc.left;
if r=selrow then begin
if splitwrkok then bmp.Canvas.Brush.color:=cllime
else bmp.Canvas.Brush.color:=clgreen;
end
else begin
bmp.Canvas.Brush.color:=clblue;
end;
bmp.Canvas.Rectangle(von,0,bis,bmp.height);
end;
end;
Zunächst wird in "mkpntb" meine ScrollBar ordentlich auf der Form positioniert.
Ich sorge dafür, dass sie auf der SelektionsPaintBox genau im oberen Drittel
liegt, sodass die Skalenstriche und -Werte lesbar bleiben.
Eigentlich hat dieser Job hier nichts zu suchen. Aber aus verschiedenen Gründen,
auf die ich nicht weiter eingehen muss, ist die Bereich-PaintBox älter als die
SelektionsPaintBox. Darum hat die Bereichs-PaintBox als Grafikspeicher auch nur
die phantasielos benamte Bitmap "bmp" zur Verfügung - denn ursprünglich dachte
ich, mir würde eine Bitmap genügen. Aber hey, es funktioniert, warum also weiter
drüber nachdenken?
Hier mal ein kleiner Einschub: Entgegen landläufiger Meinung sind nicht alle
Programmierer penible Bitschubser, die mathematisches Denken lieben und anal
fixiert durch's Leben schreiten. Ich jedenfalls bin eher chaotisch als organisiert.
Und Programmieren ist für mich vielmehr ein künstlerischer Prozess als ein streng
durchdachtes Vorgehen, wie's an der Uni gelehrt wird. Vielleicht habe ich mich ja
deswegen an der Universität nie richtig wohl gefühlt.
Grafische Übersicht zu den gesetzten Videoschnitten: Aktuelle Selektion (oben) und die zuvor ausgewaehlten Schnittbereiche (unten).
Zurück zur "mkpntb"-Funktion. Wir haben die ScrollBar positioniert, anschliessend
wird mit "mktbsel" die Selektionsgrafik generiert (siehe weiter oben). Nun kommt
endlich die Bereichsgrafik dran, um die es eigentlich geht. Zunächst wird die "bmp"
mit der Hintergrundfarbe versehen, dann wird ein silbergrauer Bereich definiert,
der dem Selektionsbereich der ScrollBar "sc" entspricht. Nun wird die ListBox "goplb"
zeilenweise durchgegangen und die jeweiligen Von-Bis-Werte herausgezogen. Mit
diesen Werten werden dann die Rechtecke ins Bitmap gemalt, die die Schnittbereiche
wiedergeben. Je nach "Aktivierungszustand" der Schnittbereiche werden diese
entsprechend anders gefärbt: Inaktiv=blau, aktiv=grün, aktiv und bearbeitbar=hellgrün
(cllime)
Die Aktivierungszustände werden durch Klicken auf grafisch angezeigten Schnittbereiche
in der PaintBox "pntb" erreicht. Dabei gilt:
-
Ein Klick ins "Leere" deaktiviert alle Schnittbereiche; sie werden blau.
-
Erwischt man einen Schnittbereich, wird dieser grün gemalt, die ScrollBar
positioniert sich an dessen Startwert, und das Video zeigt den entsprechenden MCI-Frame
an. Parallel dazu ändert sich auch der ItemIndex der ListBox "goplb".
-
Doppelklickt man einen Schnittbereich, so wird er hellgrün. Der Schnittbereich
wird diesmal auch in der SelektionsPaintBox wiedergegeben. Der ScrollBar-Cursor landet
am Selektionsende. Drückt man nun die STRG-Taste und ändert den Selektionsbereich,
so ändert sich auch der zugehörige Eintrag in der ListBox "goplb".
Die Aktivierung der Schnittbereiche erfolgt über die Ereignisse OnClick und OnDblClick
der Schnitt-PaintBox:
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
//Doppelklick auf Schnitte-PaintBox --------------
procedure Thauptf.pntbDblClick(Sender: TObject);
var
s:string;
c,von,bis:integer;
begin
if selrow=-1 then exit;
s:=goplb.items[selrow];
c:=pos(':',s);
von:=strtoint(trim(copy(s,1,c-1)));
bis:=strtoint(trim(copy(s,c+1,length(s))));
sc.Position:=bis;
selstart:=von;
selend:=bis;
splitwrkok:=true;
mktbsel;
selpbPaint(self);
mkpntb;
pntbPaint(self);
end;
//Einfachklick auf Schnitte-PaintBox --------------
procedure Thauptf.pntbClick(Sender: TObject);
var
c,r,i,von,bis:integer;
s:string;
p:tpoint;
begin
splitwrkok:=false;
getcursorpos(p);
p:=pntb.ScreenToClient(p);
//welchen bereich getroffen?
i:=selx2frm(p.x);
//markierten Bereich getroffen?
pntb.hint:='';
selrow:=-1;
for r:=0 to goplb.items.count-1 do begin
s:=goplb.items[r];
c:=pos(':',s);
von:=strtoint(trim(copy(s,1,c-1)));
bis:=strtoint(trim(copy(s,c+1,length(s))));
if(von<=i)and(i<=bis)then begin
selrow:=r;
sc.Position:=von;
s:=
'Splitt '+inttostr(r+1)+' |'+
getsplittinfo(von,bis);
wrstat('-1',s);
break;
end;
end;
goplb.ItemIndex:=selrow;
mkpntb;
pntbPaint(self);
end;
Wiederholt tritt hier übrigens auf, dass die Von-Bis-Werte aus der ListBox "goplb"
extrahiert werden müssen. Ein ordentlicher Programmierer hätte das längst
in eine eigene Funktion gepackt. Ein fauler wie ich allerdings nicht.
Die Funktion "getcursorpos" ist eine API-Funktion, die uns die Mauskoordinaten
liefert. Mittels "pntb.ScreenToClient" werden diese Screen-Koordinaten auf die
Komponenten-Koordinaten umgerechnet. Die Funktion "selx2frm" schliesslich wandelt
die so ermittelte "Klick-x-Position" in eine Frame-Position auf der ScrollBar-
Skala um.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
function thauptf.selx2frm(x:integer):integer;
var
dx,w,i:integer;
d:double;
begin
dx:=_pbdx;
w:=(sc.Width-(2*dx)-14);
d:=w/(sc.Max-sc.Min);
x:=x-dx-sc.left;
i:=trunc(x/d);
if i<sc.min then i:=sc.min;
if i>sc.Max then i:=sc.Max;
result:=i;
end;
Jetzt muss man nur noch die Schnittmarken-ListBox durchrennen und prüfen,
ob der gefundene Frame sich innerhalb eines bestimmten Selektionsbereiches
befindet. Wenn ja, wird der Aktivierungszustand entsprechend angepasst.
Das Ganze ist ziemlich dirty. Mittels der kombinierten ScrollBar-PaintBox-Komponente
können nämlich z.B. auch ohne Weiteres überlappende Videoschnittbereiche definiert
werden, ja, ein Schnittintervall kann einen oder mehrere andere selektierte
Filmausschnitte völlig überdecken. Die Aktivierung per Mausklick der inneren
Bereiche ist dann nicht mehr möglich (oder der äusseren Bereiche, je nach Reihenfolge
in der ListBox "goplb"). Das geht mir aber, ehrlich gesagt, völlig am A ...
vorbei.
Videoschnitt in Selektionskomponente aktiviert: Der hellgruene Videoschnitt ist aktiviert worden und nun bereit zur Bearbeitung.
So, wir näheren uns dem Ende. Wir können ein Video laden, es parsen,
bestimmte Bereiche darin auswählen, als Schnitt-Bereich markieren, diese
Schnitt-Bereiche ändern und löschen wie's und gefällt, und zuletzt
das Video zurecht schnippeln.
Mich hat jetzt nur noch eines gestört: Bei grossen Movies bewirkt eine
minimale Änderung der ScrollBar eine rasante Änderung der Frame-Positionierung.
Ein Millimeter nach rechts und der Schnittbereich wächst um 1.000 Frames.
Das kann beim Aufteilen des Filmes zu gehörigen Ungenauigkeiten führen.
Eine feinere Selektionsmöglichkeit wäre da wünschenswert.
Und als hätte ich es von vorneherein eingeplant, liess diese sich auch
erstaunlich simpel realisieren. Alles, was ich machen musste, war, einen
gewählten Selektionsbereich per Knopfdruck zum neuen Minimum-Maximum-
Bereich der ScrollBar zu machen!
00001
00002
00003
00004
00005
00006
procedure Thauptf.selcutbClick(Sender: TObject);
begin
sc.Min:=selstart;
sc.max:=selend;
formresize(self);
end;
Erstaunlich, aber das funzt tatsächlich. Habe ich etwa ein Video von sagen
wir mal 100.000 Frames, kann ich nun mit der Maus einen kleinen Bereich selektieren,
von 4.000 bis 4.500, ohne dass ich wissen muss, was sich im Detail darin
abspielt. Optisch ist das gerade mal eine dünne Linie in der SelektionsPaintBox.
Nun einfach Selektion-Cut gedrückt und ich bekomme meine selektierten 500 Frames
in der ScrollBar über die komplette Formbreite aufgespannt.
Ist mir das übrigens noch nicht fein genug, hält mich niemand davon ab, wiederum
einen Bereich zu selektieren, den Cut-Button zu klicken und so gewissermassen noch
weiter in die Zeitachse hineinzuzoomen.
Theoretisch kann man das Spiel treiben, bis man nur noch zwei Frames mit der
ScrollBar auswählen kann. Da ich aber wie oben erwähnt nur GOP-genau schneide,
wäre es vertane Liebesmüh, so exakt Schnittpunkte zu setzen.
Ach ja, das Beste kommt ja noch! Da der linke und rechte Rand der Schnitte-PaintBox
sich am Minimum bzw. Maximum der ScrollBar der SelektionsPaintBox orientiert, wachsen
und schrumpfen die Schnittbereiche bei deren Änderung automatisch mit (oder verschwinden
aus dem Bild, wenn etwa das neue Minimum grösser als die Endmarke eines Schnittes ist).
Cool! War so nämlich gar nicht eingeplant. Es ging einfach (okay, fast: Ich musste
dafür in diversen Funktionen noch den sich ändernden "sc.min"-Wert einarbeiten,
damit es passt, denn zuvor ging ich ja immer von einem Null-Minimum aus).
Was soll ich sagen? Dieses unerwartete Bonmot macht mein kleines, dreckiges Tool nun
auch noch von der Bedienung her zu dem für meine persönlichen Zwecke besten
Video-Cutter auf weiter Flur :-)
Ach so, die Wiederherstellung des ursprünglichen Minimum-Maximum-Bereichs fehlt
ja noch. Das ist aber ein Klacks:
00001
00002
00003
00004
00005
00006
procedure Thauptf.selresetbClick(Sender: TObject);
begin
sc.Min:=0;
sc.max:=mp.length;
formresize(self);
end;
Zoomen in die Zeitskala I: Die Selektion (oben) umschliesst mehrere zuvor gesetzte kleinere Videoschnitte (unten).
Zoomen in die Zeitskala II: Durch die Zooming-Funktionalitaet wird der zuvor ausgewaehlte Selektionsbereich ueber die gesamte PaintBox gestreckt. Die in diesem Bereich enthaltenden Videoschnitte sind nun deutlicher zu erkennen und leichter anzusteuern.
So, zu guter Letzt noch ein paar Funktionen abgehakt, die in VidSplitt
Verwendung finden:
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
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
00210
00211
00212
00213
00214
00215
00216
00217
00218
00219
00220
00221
00222
00223
00224
00225
00226
00227
00228
00229
00230
00231
00232
00233
//OnResize-Ereignis der Form, passt alles den neuen Groessenverhaeltnissen an
procedure Thauptf.FormResize(Sender: TObject);
begin
//videogroesse anpassen
setvidsz;
//schnitte- und selektionsgrafik anpassen
mkpntb;
//korrigierte selektionsgrafik anzeigen
selpbPaint(Sender);
//korrigierte schnitt-grafik anzeigen
pntbPaint(Sender);
//knoepfe aktivieren/deaktivieren
setbuttons;
end;
//aktiviert und deaktiviert Buttons,
//je nachdem Video geladen ist oder nicht
procedure thauptf.setbuttons;
begin
splitlstdelb.Enabled:=(goplb.Items.count>0);
splitsortb.Enabled:=(goplb.Items.count>0);
splitdelb.enabled:=(goplb.ItemIndex<>-1);
//video geladen?
if mp.DeviceID=0 then begin
//nein
Schliessen1.enabled:=false;
sc.enabled:=false;
selcutb.Enabled:=false;
selresetb.Enabled:=false;
splitsetb.enabled:=false;
splittb.Enabled:=false;
autob.enabled:=false;
autose.Enabled:=false;
caption:=_caption;
end
else begin
Schliessen1.enabled:=true;
sc.enabled:=true;
selcutb.Enabled:=(selstart<>selend)and mvok;
selresetb.Enabled:=
((sc.min<>0)or(sc.max<>mp.Length))and mvok;
splitsetb.enabled:=selcutb.Enabled and mvok;
splittb.Enabled:=(goplb.Items.count>0)and mvok;
autob.enabled:=mvok;
autose.Enabled:=mvok;
caption:=_caption+' - '+afne.text;
end;
end;
procedure Thauptf.FormCreate(Sender: TObject);
begin
//Selektionsgrafik
selbmp:=tbitmap.Create;
selbmp.PixelFormat:=pf24bit;
//Schnitte-Grafik
bmp:=tbitmap.Create;
bmp.PixelFormat:=pf24bit;
//Aktivierungszustand von Schnitten
selrow:=-1;
splitwrkok:=false;
selp.ParentBackground:=false;selp.color:=clbtnface;
selstart:=0;
selend:=0;
mkpntb;
//Video-Ausgabe-Panel maximieren
vp.align:=alclient;
vp.Parentbackground:=false;vp.color:=clgray;
mp.Display:=vp;
wrstat('','Kein Movie geoeffnet');
end;
procedure Thauptf.Beenden1Click(Sender: TObject);
begin
mp.close;
close;
end;
procedure Thauptf.oeffnen1Click(Sender: TObject);
begin
if not opendlg.execute then exit;
vidopen(opendlg.filename);
end;
procedure Thauptf.vidopen(fn:string);
var
gopc,frmc:integer;
begin
try
mp.close;
except
end;
try
wrstat('','Scanne Tags von '+fn+' ...');
frmc:=ScanFrames(fn,gopc);
afne.text:=fn;
mp.FileName:=fn;
mp.open;
mp.TimeFormat:=tfFrames;
orgw:=mp.DisplayRect.Right;
orgh:=mp.DisplayRect.Bottom;
mcifrmno:=mp.length;
sc.min:=0;
sc.Max:=mp.Length;
sc.Position:=1;
afne.text:=fn;
setvidsz;
FormResize(self);
wrstat(
'-1',
'Movie '+fn+' geoeffnet ==> '+
inttostr(frmc)+
' Tag-Infos Frames: '+
inttostr(frmno)
);
except
wrstat('-1','FEHLER! Konnte Movie '+fn+' nicht oeffnen.');
//afne.text:='';
end;
setbuttons;
screen.Cursor:=crdefault;
end;
//Videogroesse anpassen: Originalgroesse oder auf Panel maximieren -----
procedure thauptf.setvidsz;
var
ow,oh,t,l,w,h:integer;
begin
if not mp.CanFocus then exit;
LockWindowUpdate(Handle);
ow:=orgw;if ow=0 then ow:=vp.Clientwidth;
oh:=orgh;if oh=0 then oh:=vp.ClientHeight;
w:=ow;h:=oh;
if szmax1.Checked then begin
if(w/h)>(vp.ClientWidth/vp.clientheight)then begin
//Querformat
w:=vp.ClientWidth-5;
h:=trunc((w*oh)/ow);
end
else begin
//Hochformat
h:=vp.Clientheight-5;
w:=trunc((h*ow)/oh);
end;
end;
l:=trunc((vp.ClientWidth-w)/2)+1;
t:=trunc((vp.Clientheight-h)/2)+1;
mp.DisplayRect:=rect(l,t,w,h);
LockWindowUpdate(0);
end;
//Schnittbereiche wurden definiert,
//jetzt Video entsprechend splitten ------------------
procedure Thauptf.splittbClick(Sender: TObject);
var
fhin:thandle;
pi,c,nr,frmv,frmb:integer;
s,fno,fn,ext:string;
begin
if goplb.items.count=0 then exit;
screen.Cursor:=crhourglass;
pi:=mp.Position;
mp.Close;
//gop-speicher allokieren
gopbufp:=Pchar(AllocMem(gopmaxsz+1));
//ohne Sound-Variante
gopnsbufp:=Pchar(AllocMem(gopmaxsz+1));
fhin:=FileOpen(afne.text,fmOpenRead);
try
fno:=afne.Text;
ext:=extractfileext(fno);
fno:=copy(fno,1,length(fno)-length(ext));
pb.Max:=goplb.Items.count;
for nr:=0 to goplb.Items.count-1 do begin
pb.Position:=nr+1;
application.processmessages;
s:=goplb.Items[nr];
c:=pos(':',s);if c=0 then continue;
frmv:=strtoint(copy(s,1,c-1));
frmv:=mcifrm2frm(frmv);
frmb:=strtoint(copy(s,c+1,length(s)));
frmb:=mcifrm2frm(frmb);
fn:=fno+'_'+fill(nr,3)+ext;
SplittFile(fhin,frmv,frmb,fn);
end;
finally
fileclose(fhin);
freemem(gopnsbufp);
freemem(gopbufp);
//vidopen(afne.text);
mp.Open;
mp.Position:=pi;
formresize(nil);
pb.Position:=0;
screen.cursor:=crdefault;
end;
end;
In meinem VidSplitt-Proggy ist auch ein MPG-Tag-Scanner integriert. Er listet auf,
wo im MPG wann welche Tags mit welchen Header-Informationen auftauchen. Tatsächlich
wäre ich ohne diesen Scanner ziemlich aufgeschmissen gewesen, er hat mich viel über
die MPG-Interna gelehrt.
Insbesondere erlaubt es der Scanner auch, zwei MPGs direkt nebeneinander zu stellen.
So konnte ich mich bei meinen Schnitten viel leichter ganz allmählich an den
Originalaufbau herantasten.
Der integrierte MPG-Tag-Scanner in VidSplitt: Der Tag-Scanner ermoeglicht die direkte Gegenueberstellung der internen Strukturen zweier (verschiedener) MPG-Videodateien. Alle Tags werden dabei mit den wichtigsten Informationen in 'chronologischer' Reihenfolge nebeneinander aufgelistet.
Hier - kommentarlos, weil zu faul - der Source zum MPG-Tag-Scanner in einem Rutsch:
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
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
00210
00211
00212
00213
00214
00215
00216
00217
00218
00219
00220
00221
00222
00223
00224
00225
00226
00227
00228
00229
00230
00231
00232
00233
00234
00235
00236
00237
00238
00239
00240
00241
00242
00243
00244
00245
00246
00247
00248
00249
00250
00251
00252
00253
00254
00255
00256
00257
00258
00259
00260
00261
00262
00263
00264
00265
00266
00267
00268
00269
00270
00271
00272
00273
00274
00275
00276
00277
00278
00279
00280
00281
00282
00283
00284
00285
00286
00287
00288
00289
00290
00291
00292
00293
00294
00295
00296
00297
00298
00299
00300
00301
00302
00303
00304
00305
00306
00307
00308
00309
00310
00311
00312
00313
00314
00315
00316
00317
00318
00319
00320
00321
00322
00323
00324
00325
00326
00327
00328
00329
00330
00331
00332
00333
00334
00335
00336
00337
00338
00339
00340
00341
00342
00343
00344
00345
00346
00347
00348
00349
00350
00351
00352
00353
00354
00355
function thauptf.ftoa(f:double):string;
begin
result:=format('%.3f',[f]);
end;
procedure Thauptf.showtagsbClick(Sender: TObject);
const
_bufmax=20;
var
tbuf:array[0.._bufmax]of byte;
function b2hex(b:byte):string;
begin
if b<10 then result:=inttostr(b)
else result:=chr(ord('A')+(b-10));
end;
function hex(b:byte):string;
begin
//result:=inttostr(b); exit;
result:=
b2hex(b div 16)+
b2hex(b mod 16);
end;
function itoa(i,len:integer):string;
begin
result:=inttostr(i);
while length(result)<len do result:='0'+result;
end;
function getsz(offset:integer):integer;
begin
result:=byte(tbuf[offset])*256+byte(tbuf[offset+1]);
end;
var
fn:string;
b,bb,bbb:byte;
rd:integer;
fh:thandle;
lb:tlistbox;
fc,i:integer;
s,hs:string;
hr,min,sec:integer;
f:double;
pts,dts:double;
printok:bool;
frmc:integer;
begin
//fn:='d: mp\p.m1v';
fn:=afne.Text;//.Directory+'\'+flb.Items[flb.itemindex];
screen.cursor:=crhourglass;
if taglb1rb.checked then lb:=tag1lb else lb:=tag2lb;
lb.Hint:=afne.text;//flb.Items[flb.itemindex];
lb.showhint:=true;
lb.enabled:=false;
lb.clear;
lb.Items.add(afne.text);
lb.Items.add('==========');
mp.Close;
fh:=FileOpen(fn,fmOpenRead);
rd:=1;
fc:=0;
frmc:=0;
while rd>0 do begin
rd:=FileRead(fh,tbuf,_bufmax+1);
if (rd>=4)then begin
printok:=true;
if(tbuf[0]=0)and(tbuf[1]=0)and(tbuf[2]=1)then begin
s:='';
b:=tbuf[3];
if b=_SEQU_tag then begin
//sequenz
printok:=sequenzchb.checked;
if printok then begin
s:='SEQUENZ: ';
b:=tbuf[4];
bb:=tbuf[5];bb:=byte(bb shr 4);
i:=b*16+bb;
s:=s+inttostr(i)+'x';
b:=tbuf[5];b:=byte(b shl 4);b:=byte(b shr 4);
bb:=tbuf[6];
i:=b*256+bb;
s:=s+inttostr(i)+' ';
b:=tbuf[7];b:=byte(b shr 4);
//nur mpg-1?
if b= 1 then s:=s+'1/1(VGA) '
else if b= 2 then s:=s+'4/3(TV) '
else if b= 3 then s:=s+'16/9(Large TV PAL) '
else if b= 4 then s:=s+'2.21/1(Cinema) '
else if b= 5 then s:=s+'Asp:0.8055 '
else if b= 6 then s:=s+'16/9(Large TV NTSC) '
else if b= 7 then s:=s+'Asp:0.8935 '
else if b= 8 then s:=s+'4/3(CCIR601 PAL) '
else if b= 9 then s:=s+'Asp:0.9815 '
else if b=10 then s:=s+'Asp:1.0255 '
else if b=11 then s:=s+'Asp:1.0695 '
else if b=12 then s:=s+'4/3(CCIR601 NTSC) '
else if b=13 then s:=s+'Asp:1.1575 '
else if b=14 then s:=s+'Asp:1.2015 '
else s:=s+'?-Aspect('+inttostr(b)+') ';
b:=tbuf[7];b:=byte(b shl 4);b:=byte(b shr 4);
if b=1 then s:=s+'23.976fps(EncapsNTSC) '
else if b=2 then s:=s+'24fps(Cinema) '
else if b=3 then s:=s+'25fps(PAL) '
else if b=4 then s:=s+'29.97fps(NTSC) '
else if b=5 then s:=s+'30fps(DropNTSC) '
else if b=6 then s:=s+'50fps(DoublePAL) '
else if b=7 then s:=s+'59.94fps(DoubleNTSC) '
else if b=8 then s:=s+'60fps(DoubleDropNTSC) '
else s:=s+itoa(b,1)+'=Err-Framerate ';
b:=tbuf[8];
bb:=tbuf[9];
bbb:=tbuf[10];bbb:=byte(bbb shr 6);
i:=b*1024+bb*4+bbb;
i:=(i*400)div 1000;
s:=s+'BR:'+inttostr(i)+'kbps ';
// $3FFFF bedeutet variable Bitrate
//Merkbit muss immer eins sein
//vbv: Decodierpuffergroesse in 16-kB-Bloecken
end;
end
else if b=_GOP_tag then begin
//GOP
printok:=gopchb.checked;
if printok then begin
lb.items.add('-----');
s:='GOP: ';
b:=tbuf[4];
if (b and bin2int('10000000')=128) then
s:=s+'DRP ' else s:=s+'!DRP ';
b:=tbuf[4];b:=byte(b shl 1);b:=byte(b shr 3);hr:=b;
s:=s+'H:'+itoa(b,1)+' ';
b:=tbuf[4];b:=byte(b shl 5);b:=byte(b shr 5);
bb:=tbuf[5];bb:=byte(bb shr 4);
i:=b*16+bb;min:=i;
s:=s+'M:'+itoa(i,2)+' ';
b:=tbuf[5];b:=byte(b shl 5);b:=byte(b shr 5);
bb:=tbuf[6];bb:=byte(bb shr 5);
i:=b*8+bb;sec:=i;
s:=s+'S:'+itoa(i,2)+' ';
{
//geht auch so
i:=((tbuf[6] and $1F) shl 1)+(tbuf[7] shr 7);
}
b:=tbuf[6];b:=byte(b shl 3);b:=byte(b shr 3);
bb:=tbuf[7];bb:=byte(bb shr 7);
i:=b*2+bb;
s:=s+'F:'+itoa(i,2)+' ';
hr:=(hr*60+min)*60+sec;
s:=s+'P:'+inttostr(hr)+'s ';
b:=tbuf[7];
if (b and bin2int('1000000')=64) then
s:=s+'CLS ' else s:=s+'!CLS ';
if (b and bin2int('100000')=32) then
s:=s+'BRK ' else s:=s+'!BRK ';
end;
end
else if b=_FRAME_tag then begin
//Frame
s:='FRAME: ';
b:=tbuf[4];b:=byte(b shl 5);b:=byte(b shr 5);
bb:=tbuf[5];bb:=byte(bb shr 6);
i:=b*4+bb;
s:=s+'SEQ:'+itoa(i,3)+' ';
b:=tbuf[5];b:=byte(b shl 2);b:=byte(b shr 5);
if b=0 then begin s:=s+'L-FRAME ';printok:=true;end
else if b=1 then begin s:=s+'I-FRAME ';printok:=iframechb.checked;end
else if b=2 then begin s:=s+'P-Frame ';printok:=pframechb.checked;end
else if b=3 then begin s:=s+'B-Frame ';printok:=bframechb.checked;end
else if b=4 then begin s:=s+'D-Frame ';printok:=dframechb.checked;end
else begin s:=s+hex(b)+'-Frame? ';printok:=false;end;
if printok then begin
b:=tbuf[5];b:=byte(b shl 5);b:=byte(b shr 5);
bb:=tbuf[6];
bbb:=tbuf[7];bbb:=byte(bbb shr 3);
i:=b*(32*256)+bb*32+bbb;
//Variable: ffff
s:=s+'VBVDelay:'+format('%.4fs ',[i/_STD_SYSTEM_CLOCK_FREQ]);
s:=s+'Frmc:'+inttostr(frmc);
inc(frmc);
end;
end
else if b=_PADDING_tag then begin
printok:=padchb.checked;
if printok then s:='PADDING '+inttostr(Getsz(4));
end
else if b=_SYSTEM_tag then begin
printok:=systemchb.checked;
if printok then s:='SYSTEM '+inttostr(Getsz(4));
end
else if b=_PACK_tag then begin
printok:=packchb.checked;
if printok then begin
s:='PACK ';
b:=tbuf[4];b:=byte(b shl 1);b:=byte(b shr 3);
s:=s+'H:'+itoa(b,1)+' ';
b:=tbuf[4];b:=byte(b shl 5);b:=byte(b shr 5);
bb:=tbuf[5];bb:=byte(bb shr 4);
i:=b*16+bb;
s:=s+'M:'+itoa(i,2)+' ';
b:=tbuf[5];b:=byte(b shl 5);b:=byte(b shr 5);
bb:=tbuf[6];bb:=byte(bb shr 5);
i:=b*8+bb;
s:=s+'S:'+itoa(i,2)+' ';
f:=get_ts(@tbuf,4);
s:=s+'SCR:'+ftoa(f);
end;
end
else if b=_USER_tag then begin
printok:=userchb.checked;
if printok then s:='USER DATA';
end
else if b=_SEQERR_tag then begin
s:='SEQUENZ FEHLER';
end
else if b=_SEQEND_tag then begin
printok:=seqendchb.checked;
if printok then s:='SEQUENZ ENDE';
end
else if b=_EXT_tag then begin
s:='EXTENSION';
end
else if b=_PRGEND_tag then begin
s:='PROG ENDE';
end
else if (B>=_SLICE1_tag)and(b<=_SLICE2_tag) then begin
printok:=slicechb.checked;
if printok then s:='SLICE';
end
else if
(B>=_ASTREAM1_tag)and(b<=_ASTREAM2_tag)or
(B>=_VSTREAM1_tag)and(b<=_VSTREAM2_tag)
then begin
if(B>=_ASTREAM1_tag)and(b<=_ASTREAM2_tag)then begin
s:='ASTREAM ';
printok:=audiochb.checked;
end
else begin
s:='VSTREAM';
printok:=videochb.checked;
end;
if printok then begin
s:=s+'HL:'+inttostr(Getsz(4))+' ';
i:=Get_fuellbytes(@tbuf,6,_bufmax);
s:=s+'FB:'+inttostr(i)+' ';
i:=i+6;
b:=byte(tbuf[i]);b:=byte(b shr 7);
bb:=byte(tbuf[i]);bb:=byte(bb shl 1);bb:=byte(bb shr 7);
if(b=0)and(bb=1)then begin
s:=s+'STD ';
i:=i+2;
end
else begin
s:=s+'!STD ';
end;
//if fc=2091 then beep;
//presentation time stamp bzw. decode time stamp
b:=byte(tbuf[i]);b:=byte(b shl 2);b:=byte(b shr 7);
bb:=byte(tbuf[i]);bb:=byte(bb shl 3);bb:=byte(bb shr 7);
b:=byte(b*2+bb);
if b=0 then begin
s:=s+'!PTS !DTS ';
end
else if b=1 then begin
s:=s+'PTS/DTS-Error ';
end
else if b=2 then begin
pts:=get_ts(@tbuf,i);
s:=s+'PTS:'+ftoa(pts)+'s !DTS ';
end
else if b=3 then begin
pts:=get_ts(@tbuf,i);
i:=i+5;
dts:=get_ts(@tbuf,i);
s:=s+'PTS:'+ftoa(pts)+'s DTS:'+ftoa(dts)+'s ';
end;
end;
end
else begin
s:='?';
end;
if printok then begin
hs:=inttohex(fc,7);
lb.items.add(hs+':'+hex(tbuf[3])+' -> '+s);
end;
end;
fileseek(fh,-(rd-1),1);
inc(fc);
end
else begin
break;
end;
//if lb.count>2500 then break;
end;
fileclose(fh);
lb.enabled:=true;
//vidopen(afne.text);
mp.open;
screen.cursor:=crdefault;
end;
Hie und da findet man im Source von VidSplitt auch noch rudimentäre Spuren des Versuchs,
den Splitter-Movies den Sound auszutreiben, um sie so noch weiter schrumpfen zu lassen.
Dazu wollte ich beim Wegschreiben der Ausgabefiles alle ASTREAM-Informationen einfach
weglassen. Das Experiment ging aber in die Hose. Keine Ahnung, warum. Ich verlor jedenfalls
schnell das Interesse daran. Bleibt der Sound halt drin. Die paar Bytes mehr ... Egal!
Über eine Funktion bin ich eben noch gestolpert, die eine Extra-Erwähnung
verdient: "getsplittinfo". Wird der Aktivierungszustand eines Schnittbereichs
geändert oder ein neuer Selektionsbereich definiert, sieht man in der Statusbar
on the fly einige Informationen zu diesem Bereich. Neben einigen Insider-Informationen
wie die Nummer des MCI-Frames, Rag-Frames, GOP-Index usw. wird v.a. auch die Grösse
des Videoschnitts angezeigt, und zwar auf 's Byte genau!
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
//Groessenangabe in GB, MB, kB oder Byte -----------------------------
function thauptf.sz2str(l:extended):string;
var
t:int64;
txt:string;
dig:integer;
begin
dig:=2;
if abs(l)>(1024*1024*1024) then begin
t:=(1024*1024*1024);
txt:='GB';
end
else if abs(l)>(1024*1024) then begin
t:=(1024*1024);
txt:='MB';
end
else if abs(l)>1024 then begin
t:=1024;
txt:='kB';
end
else begin
t:=1;
txt:='BT';
dig:=0;
end;
try
result:=floattostrf(l/t,ffnumber,18,dig)+' '+txt;
except
result:='0 '+txt;
end;
end;
function thauptf.getsplittinfo(von,bis:integer):string;
var
ivon,ibis:integer;
gvon,gbis:int64;
begin
ivon:=mcifrm2frm(von);
ibis:=mcifrm2frm(bis);
gvon:=frame2gop(ivon,false);
gvon:=frmpos[gvon];
gbis:=frame2gop(ibis,true);
gbis:=frmpos[gbis]+getgopsz(gbis);
result:=
'Frames: '+
inttostr(von)+'-'+inttostr(bis)+
' ('+inttostr(bis-von)+') | '+
'InxFrames: '+
inttostr(ivon)+'-'+inttostr(ibis)+
' ('+inttostr(ibis-ivon)+') | '+
'GOP: '+
inttostr(gvon)+'-'+inttostr(gbis)+
' ('+inttostr(gbis-gvon)+') | '+
'Dateigroesse: '+
sz2str((gbis-gvon)+hdlen+1)+
' ('+inttostr((gbis-gvon)+hdlen+1)+' Byte)';
end;
Diese Information ist auch so ein Zufallsprodukt, welches nie eingeplant war.
Aber da ich die Splitts ja selbst mache, stand mir alles zur Verfügung, was ich
zur Berechnung der exakten Splitter-Grösse benötigte. Also warum nicht schon anzeigen,
bevor's zu spät ist, sprich, der Schnitt auf der Festplatte geschrieben wurde?
Ah! Ich liebe es, Programmierer zu sein!
Ich habe mir beim ersten realen Einsatz gleich
ein dickes MPG aus dem Web gezogen ... Ja, okay, es war ein Porno.
Jedenfalls war das
Teil etwa 700 MB gross. Ich lud das Movie in VidSplitt, nach ein paar Sekunden waren
alle Frames gescannt. Die Schnitte machte ich mit meiner SelektionsPaintBox, zoomte
per Selektion-Cut immer mal wieder tiefer in die Zeitskala hinein, ging anschliessend
wieder auf die Gesamtansicht, und setzte so an die 60 Schnitte bei den interessanten
Stellen. Dann führte ich die Zerkleinerung aus. Da dabei das Video ja nicht neu codiert
werden musste, sondern nur einmal sequenziell durchlaufen, hatte ich schon nach wenigen
Sekunden alle gewählten Videosequenzen sauber auf Festplattte gebannt. Gesamtgrösse
jetzt nur noch 31 MB. Na, das hat sich doch gelohnt :-)
Für einen findigen Programmierer dürfte es im Übrigen kein allzu grosses Problem
sein, die Schnitte nicht nur als Einzelfiles zu speichern, sondern zu einem neuen File
zusammen zu fügen (Video-Join-Funktionalität). Man schreibt dann halt nur einmal
den Header weg und sorgt dafür, dass die Zeitstempel durchgehend aufsteigend
vergeben werden. Die Idee klingt eigentlich machbar, beschäftigt mich selbst
momentan allerdings eher weniger.
Interessant ist vielleicht auch, das Programm so zu erweitern, dass es mit MPEG-2-Movies
umgehen kann. So gross scheint der Unterschied zu MPEG-1 gar nicht zu sein. Ich habe
jedenfalls C-Source gesehen, der Informationen aus beiden MPG-Arten ausliest, und die
programmtechnischen Abweichungen waren nicht unüberwindbar, auch wenn MPEG-2 ein Tick
komplizierter zu sein scheint.
Bleibt einschränkend noch zu sagen: So manches Movie konnte ich mit VidSplitt nicht
bearbeiten. Die Schnitte gingen völlig daneben oder die Ausgabevideos konnten nicht
abgespielt werden. Passiert aber selten. Relativ oft hatte ich dagegen das Probleme,
dass der MCI-Frame-Bereich in keiner Weise deckungsgleich mit dem Scan-Frame-Bereich
war. In diesem Fall musste man halt hinterher noch den Schnittbereich verkleinern bzw.
vergrössern, bis es doch einigermassen passte.
So etwas verdirbt mir jedenfalls nicht den Spass an meinem Proggy. Ist halt nichts
100%iges. But who cares?
Daniel Schwamms Video-Splitter: Ein kostenfreies Tool zum schnellen und komfortablen Schneiden von MPG-Videodateien.
Noch vor einigen Jahren wäre es mir nicht möglich gewesen, VidSplitt zu programmieren.
Da gab's nämlich noch kein Internet :-)
Alle Informationen, die ich brauchte, fand ich im Web. Da ich die Standleitung
meiner Arbeitsstätte nutzen konnte, kostete mich die Recherche nicht einen Pfennig,
äh, Cent. Vielen Dank also an die Buchhorn & Melzer GmbH!
Ohne Google wäre natürlich auch nichts gegangen. Meine Verbeugung vor diesen
Jungs & Mädels!
Danke auch an die Bastler von TMPGEnc und VirtualDub, an Borland und Microsoft, AMD
und Intel, und wer sonst noch alles PCs zu dem gemacht haben, was sie heute sind. Nicht zu
vergessen all die Kaffee- und Tabak-Pflanzer, die mir - und 1.000 anderen Programmieren -
meinen täglichen Sprit liefern.
Die meisten Autoren oder Quellen, die mir auf die Sprünge halfen, lassen sich leider
nicht mehr rekonstruieren. Ich sammle alles auf, was ich am "Wegesrand" im Web finde,
lese es mir immer wieder durch, jeweils mit erweitertem Kenntnisstand, und schreibe
interessante Sachen in Notepad raus. Manchmal denke ich auch intensiv nach, aber i.d.R.
lasse ich mein Hirn den Job mehr oder weniger alleine machen; irgendwann lass ich es
von der Leine, es filtert mir dann den aufgehäuftem Wust von Informationen zurecht.
Ich hocke mich nur noch vor meine Kiste und lasse es aus den Fingern fliessen. Keiner
ist überraschter als ich, wenn der heruntergetippte Source dann auch tatsächlich
einmal hinhaut.
Fast alles, was ich so im Web fand, war in englisch verfasst. Kann ich zum Glück
ganz gut lesen. Ich fand allerdings auch genügend Stoff in "exotischeren" Räumen,
wie etwa Frankreich, Japan und Russland. Die scheinen dort Delphi fast noch mehr
zu schätzen als die US-Amerikaner. Und das Schöne an Quellcodes ist ja, das denen
die Sprache des Programmierers letztlich herzlich Einerlei sind.
Hier sind noch ein paar der nützlichen Dokumente, die ich zur Thematik Videoschnitt
in den Weiten des Internets aufstöberte:
Der grösste Glücksfall für mich waren aber ohne jeden Zweifel die genialen Seiten von
http://www.fr-an.de/.
Hier werden sehr detailliert alle Tags von MPGs erklärt. Und das sogar in deutsch.
Und mit zahlreichen Programmbeispielen, die darüber hinaus auch noch in Delphi
programmiert sind! Danke, Danke, Danke! Ohne diese Hilfestellung
wäre sicher einmal mehr ein Projekt mehr von mir auf der Müllhalde der verlorenen
Bits & Bytes gelandet :-)
Video-Splitt ist Freeware. Das Programm wurde in Delphi 7 programmiert, der beiliegende
Source sollte aber auch ohne grössere Probleme mit anderen Delphi-Versionen kompiliert
werden können. Im ZIP-File enthalten ist der komplette Sourcecode sowie die ausführbare
EXE für alle die, die kein Delphi ihr eigen nennen.
VidSplitt.zip (300 kB)
Have Fun!