VidSplitt - Ein GOP-genauer MPG-1-Splitter

VidSplitt-Tutorial von Daniel Schwamm (27.12.2007)

Inhalt

1. Um was geht's hier?

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.

Delphi-Tutorials - Video-Splitter - VidSplitt, der GOP-genaue MPG-1-Video-Splitter
VidSplitt, der GOP-genaue MPG-1-Video-Splitter von Daniel Schwamm

2. Splitting/Cutting von MPG-Movies - wie kann man's machen?

2.1. Ein Wunsch entsteht

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!

2.2. Idee I: Ein Datei-Schredder als MPG-Movie-Splitter

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;

2.3. Idee II: Ein Multi-Part-Datei-Splitter als MPG-Movie-Splitter

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;

2.4. Idee III: Ein GOP-genauer MPG-1-Splitter zur Videobearbeitung

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.

2.4.1. Der Aufbau eines MPG-Videos

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.

Delphi-Tutorials - Video-Splitter - Analyse des M1V-Format mit einem Hex-Viewer

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.

2.4.2. Neu-Synchronisation der Tag-Inhalte

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?

2.4.3. Böse Bytes im Video-Header

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:

Delphi-Tutorials - Video-Splitter - Berechnung der Header-Länge in einer MPG-Videodatei
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.

3. Mein GOP-genauer MPG-1-Splitter: das Delphi-Programm

3.1. MPG-Videofilm einlesen

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;

3.2. MPG-Videodatei schreiben

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 :-)

3.3. Schnipp-Schnapp im Movie und die Treffgenauigkeit

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;

3.4. Hau' die Zeitstempel in die Streams

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;

3.5. Header-Byte-Akrobatik

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!

4. Mein GOP-genauer MPG-1-Splitter: Die Oberfläche

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.

4.1. MCI verzählt sich

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.

Delphi-Tutorials - Video-Splitter - Unterschiedlich viele Frames im MPG: TMediaPlayer versus VirtualDub
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.

4.2. TrackBar ...

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.

Delphi-Tutorials - Video-Splitter - Von-Bis-Schnittbereiche im MPG-Movie
Mehrere MPG-Splitt-Selektionen: Die Von-Bis-Schnittbereiche in der 'goplb'-ListBox und die daraus generierten erfolgreichen Schnittvideos.

4.2.1. ... und Auto-Splitting

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;
Delphi-Tutorials - Video-Splitter - Automatisch generierte Schnitt-Bereiche im Movie
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.

4.2.2. Das Manifest, das keine TrackBar mag

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.

Delphi-Tutorials - Video-Splitter - TrackBar-Selektion ohne und mit XPManifest
TrackBar-Selektion I: Erster Fehler bei der TrackBar. Ohne XPManifest sieht man die Selektion, mit XPManifest dagegen nicht.

4.2.3. Die TrackBar enttäuscht noch einmal

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.

Delphi-Tutorials - Video-Splitter - TrackBar: Fehler bei Selektionsbereichen grösser 32.000 Einheiten
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!

Delphi-Tutorials - Video-Splitter - TrackBar: Es sind 32-Bit-Werte in der API-Message TBM_SETSEL vorgegeben, die werden jedoch nicht korrekt verwendet
API-Message TBM_SETSEL: An die TrackBar werden 32-Bit-Werte zum Setzen des Selektionsbereiches übermittelt, aber offenbar nicht korrekt verwendet.

4.3. Die ScrollBar als Retter in der Not

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.

4.3.1. ScrollBar-Selektion

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.

Delphi-Tutorials - Video-Splitter - Eigene Selektionskomponente in Delphi
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.

Delphi-Tutorials - Video-Splitter - ScrollBar-Selektion mit eigener Selektionskomponente in Delphi
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.

4.4. Zeig mir all die Bereiche

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.

4.4.1. Eine Liste voller Zahlen

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.

Delphi-Tutorials - Video-Splitter - GOP-Liste vor und nach Sortierung
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;

4.4.2. Grafiken sind anschaulicher als Zahlen

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.

Delphi-Tutorials - Video-Splitter - Selektion und ausgewaehlte Schnitt-Bereiche
Grafische Übersicht zu den gesetzten Videoschnitten: Aktuelle Selektion (oben) und die zuvor ausgewaehlten Schnittbereiche (unten).

4.5. Aktivierungszustände von Schnitt-Bereichen

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.

Delphi-Tutorials - Video-Splitter - Videoschnitt in Selektionskomponente aktiviert
Videoschnitt in Selektionskomponente aktiviert: Der hellgruene Videoschnitt ist aktiviert worden und nun bereit zur Bearbeitung.

4.5.1. Näher ran!

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;
Delphi-Tutorials - Video-Splitter - Zoomen in die Zeitskala I
Zoomen in die Zeitskala I: Die Selektion (oben) umschliesst mehrere zuvor gesetzte kleinere Videoschnitte (unten).
Delphi-Tutorials - Video-Splitter - Zoomen in die Zeitskala II
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.

4.6. Abschlussarbeiten

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;

4.7. Ein MPG-Tag-Scanner gibt's gratis dazu

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.

Delphi-Tutorials - Video-Splitter - Der integrierte MPG-Tag-Scanner in VidSplitt
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;

4.8. No sound please

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!

4.9. Kostenlose Zusatzinformationen

Ü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?

5. Die Praxis

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 :-)

5.1. Movie-Merge bzw. Video-Segmente wieder zusammenfügen

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.

5.2. MPEG-2-Format

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.

5.3. Kein Licht ohne Schatten

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?

Delphi-Tutorials - Video-Splitter - Ein cooler, kleines GOP-genauer MPEG-1-Video-Cutter
Daniel Schwamms Video-Splitter: Ein kostenfreies Tool zum schnellen und komfortablen Schneiden von MPG-Videodateien.

6. Danksagung und Quellen

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 :-)

7. Der Download - alles beisammen

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!