C# Quellcode - 27.2 Kb

Worum geht es?

Der Video Stream in einem AVI-Film ist nichts weiter als eine Folge von Bitmaps. In diesem Artikel geht es darum, diese Bitmaps zu extrahieren und den Stream anschließend wieder zusammen zu setzen, um auch im Video eine Nachricht verstecken zu können. Falls Du schon weißt, wie man AVI-Videos bearbeitet, solltest Du diese Seite überspringen, es ist reine Zeitverschwendung.

Den Video Stream lesen

Die Windows AVI Library ist ein Satz von Funktionen in avifil32.dll. Bevor der verwendet werden kann, muss er mit AVIFileInit initialisiert werden.
AVIFileOpen öffnet eine Datei, AVIFileGetStream findet den Video Stream. Jede dieser Funktionen belegt Speicher, der am Ende wieder freigegeben werden muss.

//AVI Library initialisieren
[DllImport("avifil32.dll")]
public static extern void AVIFileInit();

//Eine AVI Datei öffnen
[DllImport("avifil32.dll", PreserveSig=true)]
public static extern int AVIFileOpen(
        ref int ppfile,
        String szFile,
        int uMode,
        int pclsidHandler);

//einen Stream in einer offenen AVI Datei holen
[DllImport("avifil32.dll")]
public static extern int AVIFileGetStream(
        int pfile,
        out IntPtr ppavi,
        int fccType,
        int lParam);

//Einen offenen AVI Stream freigeben
[DllImport("avifil32.dll")]
public static extern int AVIStreamRelease(IntPtr aviStream);

//Eine offene AVI Datei freigeben
[DllImport("avifil32.dll")]
public static extern int AVIFileRelease(int pfile);

//AVI Library schließen
[DllImport("avifil32.dll")]
public static extern void AVIFileExit();

Jetzt können wir eine AVI Datei öffnen und den Video Stream finden. AVI Dateien enthalten mehrere Streams von vier verschiedenen Typen (Video, Audio, Midi und Text). Normalerweise existiert nur ein Stream von jedem Typ, und wir sind nur am Video Stream interessiert.

private int aviFile = 0;
private IntPtr aviStream;

public void Open(string fileName) {
        AVIFileInit(); //Intitialize AVI library

        //Datei öffnen
        int result = AVIFileOpen(
                ref aviFile,
                fileName,
                OF_SHARE_DENY_WRITE, 0);

        //Video Stream holen
        result = AVIFileGetStream(
                aviFile,
                out aviStream,
                streamtypeVIDEO, 0);
}

Bevor wir die Frames auslesen können, müssen wir wissen, was genau wir lesen wollen:
- Wo beginnt der erste Frame?
- Wie viele Frames sind vorhanden?
- Welche Höhe/Breite haben die Bilder?
Die AVI Library enthält Funktionen für jede Frage.

//Startposition eines Streams ermitteln
[DllImport("avifil32.dll", PreserveSig=true)]
public static extern int AVIStreamStart(int pavi);

//Anzahl der Frames in einem Stream ermitteln
[DllImport("avifil32.dll", PreserveSig=true)]
public static extern int AVIStreamLength(int pavi);

//Header-Infos über einen offenen Stream abrufen
[DllImport("avifil32.dll")]
public static extern int AVIStreamInfo(
        int pAVIStream,
        ref AVISTREAMINFO psi,
        int lSize);

Mit diesen Funktionen können wir eine BITMAPINFOHEADER Struktur füllen. Um Bilder zu extrahieren, brauchen wir noch drei weitere Funktionen.

//Pointer auf ein GETFRAME Objekt holen (gibt bei Fehlern 0 zurück)
[DllImport("avifil32.dll")]
public static extern int AVIStreamGetFrameOpen(
        IntPtr pAVIStream,
        ref BITMAPINFOHEADER bih);

//Pointer auf ein DIB holen (gibt bei Fehlern 0 zurück)
[DllImport("avifil32.dll")]
public static extern int AVIStreamGetFrame(
        int pGetFrameObj,
        int lPos);

//GETFRAME Object freigeben
[DllImport("avifil32.dll")]
public static extern int AVIStreamGetFrameClose(int pGetFrameObj);

Endlich können wir die Frames entpacken...

//Startposition und Anzahl der Frames holen
int firstFrame = AVIStreamStart(aviStream.ToInt32());
int countFrames = AVIStreamLength(aviStream.ToInt32());

//Header-Inforamtionen holen
AVISTREAMINFO streamInfo = new AVISTREAMINFO();
result = AVIStreamInfo(aviStream.ToInt32(), ref streamInfo, Marshal.SizeOf(streamInfo));

//Header für die Bitmaps zusammensetzen
BITMAPINFOHEADER bih = new BITMAPINFOHEADER();
bih.biBitCount = 24;
bih.biCompression = 0;
bih.biHeight = (Int32)streamInfo.rcFrame.bottom;
bih.biWidth = (Int32)streamInfo.rcFrame.right;
bih.biPlanes = 1;
bih.biSize = (UInt32)Marshal.SizeOf(bih);

//Entpacken von DIBs (device independend bitmaps) vorbereiten
int getFrameObject = AVIStreamGetFrameOpen(aviStream, ref bih);

...

//Den Frame an einer bestimmten Position exportieren
public void ExportBitmap(int position, String dstFileName){
        //Frame dekomprimieren und Pointer zum DIB zurückgeben
        int pDib = Avi.AVIStreamGetFrame(getFrameObject, firstFrame + position);

        //Bitmap-Header in eine verwaltete Struktur kopieren
        BITMAPINFOHEADER bih = new BITMAPINFOHEADER();
        bih = (BITMAPINFOHEADER)Marshal.PtrToStructure(new IntPtr(pDib), bih.GetType());

        //Das Bild kopieren
        byte[] bitmapData = new byte[bih.biSizeImage];
        int address = pDib + Marshal.SizeOf(bih);
        for(int offset=0; offset<bitmapData.Length; offset++){
                bitmapData[offset] = Marshal.ReadByte(new IntPtr(address));
                address++;
        }

        //Bitmap-Details kopieren
        byte[] bitmapInfo = new byte[Marshal.SizeOf(bih)];
        IntPtr ptr;
        ptr = Marshal.AllocHGlobal(bitmapInfo.Length);
        Marshal.StructureToPtr(bih, ptr, false);
        address = ptr.ToInt32();
        for(int offset=0; offset<bitmapInfo.Length; offset++){
                bitmapInfo[offset] = Marshal.ReadByte(new IntPtr(address));
                address++;
        }

...und in einer Bitmap-Datei speichern.

        //Header aufbauen
        Avi.BITMAPFILEHEADER bfh = new Avi.BITMAPFILEHEADER();
        bfh.bfType = Avi.BMP_MAGIC_COOKIE;
        bfh.bfSize = (Int32)(55 + bih.biSizeImage); //Größe der gespeicherten Datei
        bfh.bfOffBits = Marshal.SizeOf(bih) + Marshal.SizeOf(bfh);

        //Zieldatei erstellen oder überschreiben
        FileStream fs = new FileStream(dstFileName, System.IO.FileMode.Create);
        BinaryWriter bw = new BinaryWriter(fs);

        //Header schreiben
        bw.Write(bfh.bfType);
        bw.Write(bfh.bfSize);
        bw.Write(bfh.bfReserved1);
        bw.Write(bfh.bfReserved2);
        bw.Write(bfh.bfOffBits);
        //Details schreiben
        bw.Write(bitmapInfo);
        //Write bitmap data
        bw.Write(bitmapData);
        bw.Close();
        fs.Close();
} //end of ExportBitmap

Die Anwendung kann die extrahierten Bitmaps genauso verwenden wie jedes andere Bild. Wenn eine Träger-Datei ein AVI Video ist, wird der erste Frame in eine temporäre Datei extrahiert, geöffnet und ein Teil der Nachricht darin versteckt. Anschließend wird die geänderte Bitmap in einen neuen Stream geschrieben und mit dem nächsten Frame weiter gearbeitet. Nach dem letzten Frame schließt die Anwendung beide Video Dateien, löscht die temporären Bitmap Dateien, und macht mit der nächsten Träger-Datei weiter.

Einen Video Stream schreiben

Wenn die Anwendung eine AVI Träger-Datei öffnet, erstellt sie auch eine weitere AVI Datei für die resultierenden Bitmaps. Der neue Video Stream muss die gleiche Höhe/Breite und Frame Rate haben wie das Original, darum können wir ihn nicht gleich in der Open() Methode anlegen. Wenn die erste Bitmap extrahiert wurde wissen wir das Format der Frames und können den Video Stream erstellen. Die Funktionen zum Anlegen von Streams und Hinzufügen von Frames sind AVIFileCreateStream, AVIStreamSetFormat und AVIStreamWrite:

//Neuen Strean in einer vorhandenen AVI Datei erstellen
[DllImport("avifil32.dll")]
public static extern int AVIFileCreateStream(
        int pfile,
        out IntPtr ppavi,
        ref AVISTREAMINFO ptr_streaminfo);

//Format eines neuen Stram festlegen
[DllImport("avifil32.dll")]
public static extern int AVIStreamSetFormat(
        IntPtr aviStream, Int32 lPos,
        ref BITMAPINFOHEADER lpFormat, Int32 cbFormat);

//Einen Frame in einen Stream schreiben
[DllImport("avifil32.dll")]
public static extern int AVIStreamWrite(
        IntPtr aviStream, Int32 lStart, Int32 lSamples,
        IntPtr lpBuffer, Int32 cbBuffer, Int32 dwFlags,
        Int32 dummy1, Int32 dummy2);

Jetzt können wir einen Stream erstellen...

//Neuen Viedo Stream anlegen
private void CreateStream() {
        //Eigenschaften des Streams festlegen
        AVISTREAMINFO strhdr = new AVISTREAMINFO();
        strhdr.fccType = this.fccType; //mmioStringToFOURCC("vids", 0)
        strhdr.fccHandler = this.fccHandler; //"Microsoft Video 1"
        strhdr.dwScale = 1;
        strhdr.dwRate = frameRate;
        strhdr.dwSuggestedBufferSize = (UInt32)(height * stride);
        //Höhste Qualität verwenden! Kompression zerstört die versteckte Nachricht.
        strhdr.dwQuality = 10000;
        strhdr.rcFrame.bottom = (UInt32)height;
        strhdr.rcFrame.right = (UInt32)width;
        strhdr.szName = new UInt16[64];

        //Den Stream erstellen
        int result = AVIFileCreateStream(aviFile, out aviStream, ref strhdr);

        //Format festlegen
        BITMAPINFOHEADER bi = new BITMAPINFOHEADER();
        bi.biSize      = (UInt32)Marshal.SizeOf(bi);
        bi.biWidth     = (Int32)width;
        bi.biHeight    = (Int32)height;
        bi.biPlanes    = 1;
        bi.biBitCount  = 24;
        bi.biSizeImage = (UInt32)(this.stride * this.height);

        //Format zuweisen
        result = Avi.AVIStreamSetFormat(aviStream, 0, ref bi, Marshal.SizeOf(bi));
}

...und Video Frames schreiben.

//Leere AVI Datei anlegen
public void Open(string fileName, UInt32 frameRate) {
        this.frameRate = frameRate;

        Avi.AVIFileInit();

        int hr = Avi.AVIFileOpen(
                ref aviFile, fileName,
                OF_WRITE | OF_CREATE, 0);
}

//Ein Bild zum Stream hinzufügen - wenn erstes Bild: Stream erstellen
public void AddFrame(Bitmap bmp) {
        BitmapData bmpDat = bmp.LockBits(
                new Rectangle(0, 0, bmp.Width, bmp.Height),
                ImageLockMode.ReadOnly,PixelFormat.Format24bppRgb);

        //Es ist der erste Frame - Größe festlegen und Stream erstellen
        if (this.countFrames == 0) {
                this.stride = (UInt32)bmpDat.Stride;
                this.width = bmp.Width;
                this.height = bmp.Height;
                CreateStream(); //a method to create a new video stream
        }

        //Bitmap hinzufügen
        int result = AVIStreamWrite(aviStream,
                countFrames, 1,
                bmpDat.Scan0, //pointer to the beginning of the image data
                (Int32) (stride  * height),
                0, 0, 0);

        bmp.UnlockBits(bmpDat);
        this.countFrames ++;
}

Mehr brauchen wir nicht, um Video Stream zu lesen und zu schreiben. Non-Video Streams und Kompression sind erstmal uninteressant, weil Kompression die versteckte Nachricht durch Farbveränderungen zerstören würde, und Sound die Dateien noch grösser machen würde - unkomprimierte AVI Dateien sind gross genug ;-)

Änderungen in CryptUtility

Die Methode HideOrExtract() hat bisher alle Träger-Bilder auf einmal geladen. Das war von Anfang an nicht gerade perfekt, und ist ab jetzt unbrauchbar, da alle AVIs entpackt werden müssten. Ab jetzt lädt HideOrExtract() nur eine Bitmap, und schließt sie wieder bevor das nächste Bild geladen wird. Das aktuell verwendete Träger-Bild - einfache Bitmap oder extrahierter AVI Frame - wird in einer BitmapInfo Struktur gespeichert.

public struct BitmapInfo{
        //Unkomprimiertes Bild
        public Bitmap bitmap;

        //Position des Frames im AVI Stream, -1 für Non-AVI Bitmaps
        public int aviPosition;
        //Anzahl der Frames im AVI Stream,  0 für Non-AVI Bitmaps
        public int aviCountFrames;

        //Pfad und Name der Bitmap-Datei
        //Diese Datei wird später gelöscht, wenn aviPosition 0 oder größer ist
        public String sourceFileName;
        //Anzahl der Bytes, die in diesem Bild versteckt werden
        public long messageBytesToHide;

        public void LoadBitmap(String fileName){
                bitmap = new Bitmap(fileName);
                sourceFileName = fileName;
        }
}

Jedesmal wenn MovePixelPosition die Position ins nächste Träger-Bitmap versetzt, prüft die Methode das Feld aviPosition. Wenn aviPosition < 0 ist, wird die Bitmap gespeichert und geschlossen. Wenn aviPosition 0 oder größer, ist es ein Frame in einem AVI Stream. Dieser wird nicht in einer Datei gespeichert, sondern zum geöffneten AVI Stream hinzugefügt. Falls die Bitmap ein einfaches Bild oder der letzte Frame eines AVI Streams war, öffnet die Methode das nächste Träger-Bild. Gibt es weitere Frames im AVI Stream, wird die nächste Bitmap exportiert und geöffnet.

Hinweise

Das Project enthält drei neue Klassen:
- AviReader öffnet AVI Dateien und kopiert Frames in Bitmap Dateien.
- AviWriter erstellt neue AVI Dateien und kombiniert Bitmaps zu einem Video Stream.
- Avi enthält alle Deklarationen und Struktur-Definitionen.

Danke

Danke an Shrinkwrap Visual Basic, für das "Visual Basic AVIFile Tutorial", speziell für das Beispiel zum kopieren von DIBs.

Danke an Rene N., der eine AviWriter-Klasse in microsoft.public.dotnet.languages.csharp gepostet hat.