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.
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:
//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.
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 ;-)
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.
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 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.