Einfacher MediaPlayer per Media Foundation (IMFMediaEngine(Ex))

    • VB.NET
    • .NET (FX) 4.5–4.8

    Es gibt 18 Antworten in diesem Thema. Der letzte Beitrag () ist von HaLi22.

      Einfacher MediaPlayer per Media Foundation (IMFMediaEngine(Ex))

      Hi@all

      Das ist ein einfacher Media Player per Media Foundation. Ich habe mal dieses hier aus einem größeren Projekt zu einem kompakten Code (nur 2 Klassen) zusammengefasst und soll nur zeigen wie es funktioniert. Klar kann man das auch anders programmieren. Aber darum geht es auch gar nicht. Das wichtigste ist drin: Abspielen aller von der Media Foundation unterstützten Audio- / Videoformate. Inkl WebRadio und WebTV. Lautstärke, Balance, Mute und entsprechenden Events der MediaEngine (IMFMediaEngineNotify). Den Rest kann ja jeder selbst einbauen bzw das ganze komplett neu programmieren. :D Der Screenshot zeigt halt die einfache Oberfläche auf der gerade ein WebTV wiedergegeben wird.
      Bilder
      • MFMediaEnginePlayer.png

        161,91 kB, 585×360, 141 mal angesehen
      Dateien
      Mfg -Franky-
      Cool,
      muss ich mir auf jeden Fall genauer anschauen.
      Ist es aber möglich mehrere Sounds gleichzeitig abzuspielen, oder brauche ich da für jeden eine eigene IMFMediaEngineEx?
      Ist es möglich als Source einen Stream, bzw. Byte-Array anzugeben, oder muss das immer einen Datei sein?
      @Bluespide

      Bluespide schrieb:

      st es aber möglich mehrere Sounds gleichzeitig abzuspielen, oder brauche ich da für jeden eine eigene IMFMediaEngineEx?

      Für jeden Sound, der gleichzeitig abgespielt werden soll, muss eine eigene MediaEngine verwendet werden.

      Bluespide schrieb:

      Ist es möglich als Source einen Stream, bzw. Byte-Array anzugeben, oder muss das immer einen Datei sein?

      WebRadio und WebTV sind ja schon entsprechende Streams. Ich denke aber das Du was anderes suchst. Dann geht das per IMFMediaEngineEx.SetSourceFromByteStream (docs.microsoft.com/en-us/windo…x-setsourcefrombytestream) wobei Du zB. das ByteArray vorher in einen IMFByteStream schaufeln müsstest.

      Edit: Per API MFCreateMFByteStreamOnStream könntest Du von einem IStream (in dem man vorher zB ein ByteArray reinschaufelt) ein IMFByteStream erstellen. Im IStream muss sich dann eine komplettes Audio/Video befinden.
      Mfg -Franky-

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „-Franky-“ ()

      @Bluespide

      Du müsstest Verstehen das die IMFMediaEngine(Ex) ein relativ einfaches Interface ist um mal eben fix einen Player, ohne viel SchnickSchnack, zubauen und viel von dem, was im Hintergrund passiert, Du nicht zu sehen bekommst und auch nicht rankommst. Dafür ist der benötigte Code doch recht kurz. Wenn Du mehr machen möchtest, dann musst Du dich mit den anderen Interfaces der Media Foundation beschäftigen. Da wäre es auch möglich, mehrere Sounds gleichzeitig abzuspielen indem man diese vor dem Abspielen in eine Collection (MFCreateCollection -> AddElement) hinzufügt um dann daraus per MFCreateAggregateSource eine einzige MediaSource erstellt, sozusagen vorher zusammen mixt, die dann abgespielt werden kann. Dann lassen sich die einzelnen Sounds aber nicht einzeln abspielen, stoppen usw. Das geht nur wenn jeder Sound seine eigene Routine zum abspielen bekommt. Ansonsten kann man mit der Media Foundation auch von einem Audio/Videoformat in ein anderes Audio/Videoformat konvertieren. Du findest in der Doku zur Media Foundation und im Internet sehr viele Beispiele was möglich ist.
      Mfg -Franky-

      -Franky- schrieb:

      Du müsstest Verstehen das die IMFMediaEngine(Ex) ein relativ einfaches Interface ist um mal eben fix einen Player, ohne viel SchnickSchnack, zubauen
      Aber genau das strebe ich ja gerade an und deswegen find ich das auch gerade so interessant. Ich erstelle nebenbei ein kleines Spiel (zum Zweck des Codings, nicht zum Spielen) das ich versuche komplett ohne Garbage Collection (aber nicht einfach nur Pooling) zum laufen zu bekommen. Audio ist dabei ein schwieriges Thema. Wobei ich hier nur einfach MP3's abspielen möchte, nix räumliches. Zumindest jetzt nicht. Am Anfang habe ich einfach die CSCore benutzt, aber bin wegen der große Menge an Garbage dann zur Bass.dll gewechselt. Diese native DLL erzeugt natürlich nix innerhalb der CLR, was super ist, muss aber immer mitgeliefert werden. Das ist voll ok, aber einen einfachen Wrapper um IMFMediaEngine fände ich da noch angenehmer. Je schlanker desto besser. Hier z.B. mein Wrap um Bass:

      C#-Quellcode

      1. ​public static unsafe class BassFromMemory {
      2. private static IntPtr bassDll;
      3. public static delegate* unmanaged<int/*device*/, int/*freq*/, BASSInit/*flags*/, IntPtr/*win*/, IntPtr/*clsid*/, bool> BassInit = null;
      4. public static delegate* unmanaged<void> BassFree = null;
      5. public static delegate* unmanaged<BASSError> BassErrorGetCode = null;
      6. public static delegate* unmanaged<bool/*mem*/, IntPtr/*memory*/, long/*offset*/, long/*length*/, BASSFlag/*flags*/, int> BassStreamCreateFile = null;
      7. public static delegate* unmanaged<int/*handle*/, bool> BassStreamFree = null;
      8. public static delegate* unmanaged<int/*handle*/, bool/*restart*/, bool> BassChannelPlay = null;
      9. public static delegate* unmanaged<int/*handle*/, bool> BassChannelPause = null;
      10. public static delegate* unmanaged<int/*handle*/, BASSAttribute/*attrib*/, float/*value*/, bool> BassChannelSetAttribute = null;
      11. public static delegate* unmanaged<int/*handle*/, BASSFlag/*flags*/, BASSFlag/*mask*/, BASSFlag> BassChannelFlags = null;
      12. public static delegate* unmanaged<int/*handle*/, BASSActive> BassChannelIsActive = null;
      13. public static void Init(string bassDllPath, int device = -1, int freq = 44100, BASSInit flags = BASSInit.DeviceDefault, IntPtr win = default) {
      14. BassFromMemory.bassDll = NativeLibrary.Load(bassDllPath);
      15. BassFromMemory.BassInit = (delegate* unmanaged<int, int, BASSInit, IntPtr, IntPtr, bool>)NativeLibrary.GetExport(BassFromMemory.bassDll, "BASS_Init");
      16. BassFromMemory.BassFree = (delegate* unmanaged<void>)NativeLibrary.GetExport(BassFromMemory.bassDll, "BASS_Free");
      17. BassFromMemory.BassErrorGetCode = (delegate* unmanaged<BASSError>)NativeLibrary.GetExport(BassFromMemory.bassDll, "BASS_ErrorGetCode");
      18. BassFromMemory.BassStreamCreateFile = (delegate* unmanaged<bool, IntPtr, long, long, BASSFlag, int>)NativeLibrary.GetExport(BassFromMemory.bassDll, "BASS_StreamCreateFile");
      19. BassFromMemory.BassStreamFree = (delegate* unmanaged<int, bool>)NativeLibrary.GetExport(BassFromMemory.bassDll, "BASS_StreamFree");
      20. BassFromMemory.BassChannelPlay = (delegate* unmanaged<int, bool, bool>)NativeLibrary.GetExport(BassFromMemory.bassDll, "BASS_ChannelPlay");
      21. BassFromMemory.BassChannelPause = (delegate* unmanaged<int, bool>)NativeLibrary.GetExport(BassFromMemory.bassDll, "BASS_ChannelPause");
      22. BassFromMemory.BassChannelSetAttribute = (delegate* unmanaged<int, BASSAttribute, float, bool>)NativeLibrary.GetExport(BassFromMemory.bassDll, "BASS_ChannelSetAttribute");
      23. BassFromMemory.BassChannelFlags = (delegate* unmanaged<int, BASSFlag, BASSFlag, BASSFlag>)NativeLibrary.GetExport(BassFromMemory.bassDll, "BASS_ChannelFlags");
      24. BassFromMemory.BassChannelIsActive = (delegate* unmanaged<int, BASSActive>)NativeLibrary.GetExport(BassFromMemory.bassDll, "BASS_ChannelIsActive");
      25. if (!BassFromMemory.BassInit(device, freq, flags, win, IntPtr.Zero)) {
      26. throw new Exception($"Bass error '{BassFromMemory.BassErrorGetCode()}'");
      27. }
      28. }
      29. }
      30. public unsafe struct BassSample {
      31. public int BassHandle;
      32. public IntPtr MemoryHandle;
      33. public float Volume {
      34. set {
      35. if (!BassFromMemory.BassChannelSetAttribute(this.BassHandle, BASSAttribute.BASS_ATTRIB_VOL, value)) {
      36. throw new Exception($"Bass error '{BassFromMemory.BassErrorGetCode()}'");
      37. }
      38. }
      39. }
      40. public bool IsLooping {
      41. set => BassFromMemory.BassChannelFlags(this.BassHandle, value ? BASSFlag.BASS_SAMPLE_LOOP : BASSFlag.BASS_DEFAULT, BASSFlag.BASS_SAMPLE_LOOP);
      42. }
      43. public bool IsPlaying => BassFromMemory.BassChannelIsActive(this.BassHandle) == BASSActive.BASS_ACTIVE_PLAYING;
      44. public static BassSample FromStream(ReadOnlySpan<byte> stream, BASSFlag flags = BASSFlag.BASS_DEFAULT) {
      45. BassSample nbs = new() { MemoryHandle = Marshal.AllocHGlobal(stream.Length) };
      46. stream.CopyTo(new Span<byte>((void*)nbs.MemoryHandle, stream.Length));
      47. if ((nbs.BassHandle = BassFromMemory.BassStreamCreateFile(true, nbs.MemoryHandle, 0, stream.Length, flags)) == 0) {
      48. throw new Exception($"Bass error '{BassFromMemory.BassErrorGetCode()}'");
      49. }
      50. return nbs;
      51. }
      52. public void Dispose() {
      53. if (this.MemoryHandle != IntPtr.Zero) {
      54. try {
      55. if (!BassFromMemory.BassStreamFree(this.BassHandle)) {
      56. throw new Exception($"Bass error '{BassFromMemory.BassErrorGetCode()}'");
      57. }
      58. } finally {
      59. Marshal.FreeHGlobal(this.MemoryHandle);
      60. this.MemoryHandle = IntPtr.Zero;
      61. }
      62. }
      63. }
      64. public void Play() {
      65. if (!BassFromMemory.BassChannelPlay(this.BassHandle, false)) {
      66. throw new Exception($"Bass error '{BassFromMemory.BassErrorGetCode()}'");
      67. }
      68. }
      69. public void Pause() {
      70. if (this.IsPlaying) {
      71. if (!BassFromMemory.BassChannelPause(this.BassHandle)) {
      72. throw new Exception($"Bass error '{BassFromMemory.BassErrorGetCode()}'");
      73. }
      74. }
      75. }
      76. }


      Die unmanaged Pointer zu den Funktionen erzeugen im Gegensatz zum Delegaten keine Objekte und sind auch noch schneller, da diese den internen und bisher ungenutzten IL-calli​ verwenden. Das BassSample wird vom MP3 Stream geladen und kann eigentlich nur:
      - Play
      - Pause
      - Volume
      - IsLooping
      - IsPlaying

      Mehr brauche ich an dieser Stelle auch nicht. Für jeden Sound erstelle ich einen neuen BassSample. Das ganze jetzt auf IMFMediaEngine umzustellen hört sich für mich nach deiner Beschreibung erstmal möglich an.
      Hab das mal ausprobiert, aufm ersten Blick nicht schlecht. Hab dazu ein paar Fragen.

      Ich habe versucht h265 UHD Videos abzuspielen, habe mehrere h265 UHD Videos getestet keins lief, ist h265 nicht supported oder liegt es an meinen Dateien?(Format)
      Mit der libvlc kriege ich keine AudioFilter zum laufen, einige sagen das ist eine limitierung der libvlc andere sagen es soll gehen, hab dazu keine näheren infos, geht das mir dieser MF-Engine bei Videos?
      Bei manchen h264 FHD Videos ruckelt es am Anfang wenn ich diese übers Netzwerk abspiele(lokaler pfad ist einwandfrei), gibt es einen buffer an dem man schrauben kann(buffersize) oder was anderes um das zu verbessern?
      Die libvlc schafft es auch eine Film-Länge anzuzeigen wenn die Filme übers Netzwerk abgespielt werden, ich sehe mit der MF nur wenn ich via lokalen Pfad abspiele die länge des Films, kann man da was machen?

      Wie sieht es mit DVB-C aus? Kann man TV-Karten/Sticks damit nutzen? Beim VLC hab ich z.B. das Problem, das ich nur DVB-T aber nicht DVB-C nutzen kann mit meinem Stick, liegt daran das der VLC bei meinem Stick nicht auf DVB-C umschalten kann, irgendein Chip oder Tuner muss da Softwareseitig eingestellt werden.
      @Takafusa:
      Ich habe tatsächlich nicht alle möglichen Audio- und Videoformate ausprobiert. Evtl wirst hier fündig. docs.microsoft.com/en-us/windo…rmats-in-media-foundation

      Zu H265 hab ich nur das hier gefunden: docs.microsoft.com/en-us/windo…-265---hevc-video-decoder

      Einen Buffer an dem man Schrauben kann gibt es leider nicht. Wegen der Duration. Sofern es sich um keine http(s) Adresse handelt (WebStream), sollte es auch eine Duration geben. Muss ich mir mal anschauen.

      Da ich keine DVB-C/T Sticks besitze, kann ich dazu nichts sagen.
      Mfg -Franky-
      ->H.265
      OK, h265 Video ist nicht auf der Liste, das ist für mich aber kein Problem, kann die BDs auch mit h264 rippen.

      ->Netzwerk ruckeln beim FIlm starten
      Ich habe ein wenig auf MS gelesen, ich werde in dennächsten Tagen mal schauen, ob ich das ruckeln bei Filmen im Netzwerk wegbekomme, ich denke das
      docs.microsoft.com/en-us/windo…imfmediaengine-setpreload
      könnte da was sein.

      ->Filmlänge
      Eine Filmlänge sehe ich ja, aber nur wenn lokal von der Platte abgespielt wird, sobald übers Netzwerk geladen wird nicht. (HTTP)

      ->AudioFilter kannste nichts zu sagen? Also Equalizer wären schon ein nice to have, ein Kompressor IMO ein must have, muss mich da selbst auch mal einlesen. Wenn das geht werde ich von der libvlc auf die IMFMediaEngine umsteigen. Hab mir ein Programm gebastelt, mit dem ich von jedem Rechner mit Internetzugang aus auf meine Mediathek zugreifen kann. Das einzige was mich noch stört ist der Ton(libvlc ohne AudioFilter), die Sprache in Filmen ist oft sehr leise, man macht den Verstärker lauter, explodiert dann was im Film fliegen die Lautsprecher fast aus dem Gehäuse, da ist ein Kompressor Gold wert.

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Takafusa“ ()

      @Takafusa:
      Filter und Effekte einbinden geht über IMFMediaEngineEx::InsertAudio/VideoEffect sofern entsprechende MFTs vorhanden sind. Es gibt ein paar Video-MFTs/DMOs. Audio-MFTs/DMOs gibt es nur 2 (Resampler und noch einen) auf meinem System. Du kannst Dir aber selber entsprechende Filter-MFTs schreiben. Hab ich noch nie gemacht. Hier gibt es einfaches Beispiel zB für einen AudioDelay: docs.microsoft.com/en-us/windo…und/mft-audiodelay-sample Ganz unten auf dieser Seite "Writing a Custom MFT" wird dann noch mal erklärt wie der Aufbau in so einem MFT auszusehen hat . Innerhalb des MFT kommst an die Audio/Videodaten und kannst sie dort entsprechend manipulieren/bearbeiten.

      Edit: Wegen der Duration: Hab das gerade mit einer MP4 im Netzwerk ausprobiert (über \\server\folder\test.mp4). War allerdings auch nur ein kurzes Video. Die Duration wird angezeigt (siehe Bild, ein älteres VB6 Projekt). Evtl liegt es daran das ich alles was mit HTTP beginnt (m_WebMedia = strSource.StartsWith("http")), dann später in den Events MF_MEDIA_ENGINE_EVENT_CANPLAYTHROUGH und MF_MEDIA_ENGINE_EVENT_TIMEUPDATE, die Duration nicht auslese. Gedacht war das ganze eher für WebRadio. Da gibt es keine Duration die ermittelt werden kann. Alternativ gibt die Duration dann halt 0 zurück.

      Wegen H265. Hab da mal etwas recherchiert. Standardmäßig ist wohl kein De/Encoder für die Media Foundation in Windows vorhanden. Es gab wohl mal einen kostenlosen download ("HEVC Video Extensions from Device Manufacturer") aus dem Store der irgendwann entfernt wurde. Was es noch gibt ist folgendes: microsoft.com/en-us/p/hevc-vid…vetab=pivot%3Aoverviewtab Damit sollen dann auch H265 Videos funktionieren. Kostest aber halt 99 Cent. Da ich keine H265 Videos habe, kann ich das auch nicht testen.

      Second edit: Ach gugge, durch die Hintertür soll man wohl doch noch an den kostenlosen H265 Codec kommen. Hier gibt es entsprechende Links dazu: reddit.com/r/Windows10/comment…10_hevc_video_extensions/
      Bilder
      • MFMediaEngine_LAN.png

        388,89 kB, 1.165×447, 136 mal angesehen
      Mfg -Franky-

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „-Franky-“ ()

      Ähm - nach KurzGuck:
      du codest ja noch Strict Off! (achso, nee - über jede Datei einzeln hingeschrieben statt in Projekteinstellungen)
      Und/Aber hast noch den MVB-Namespace drinne
      Visual Studio - Empfohlene Einstellungen
      Noch ein - Sowas (Vergleich einer doppelten Verneinung mit True):

      VB.NET-Quellcode

      1. If Not IsNothing(m_MFMediaEngineEx) = True Then
      wäre leserlicher:

      VB.NET-Quellcode

      1. If m_MFMediaEngineEx IsNot Nothing Then

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Wat? In allen vb-Dateien steht als erstes

      VB.NET-Quellcode

      1. Option Strict On
      2. Option Explicit On


      ##########

      ErfinderDesRades schrieb:

      Und/Aber hast noch den MVB-Namespace drinne
      Dann kann man aber konsequent weiter machen:

      VB.NET-Quellcode

      1. Set(ByVal value As Boolean)
      2. If Not IsNothing(m_MFMediaEngineEx) = True Then
      3. If m_MFMediaEngineEx.SetAutoPlay(value) = S_OK Then
      4. m_AutoPlay = value
      5. End If
      6. Else
      7. m_AutoPlay = value
      8. End If
      9. End Set

      Bei Set muss man keinen Parameter mehr übergeben, da Value implizit eh schon exisitert, ByVal kann man auch gleich weglassen und dann wär da noch Boolean-Vergleiche mit True sind überflüssig.

      Inwieweit das jetzt allerdings bei diesem Projekt zielführend ist, kann jeder sich selbst beantworten.
      Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

      Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „VaporiZed“ ()

      @ErfinderDesRades

      ErfinderDesRades schrieb:

      du codest ja noch Strict Off! (achso, nee - über jede Datei einzeln hingeschrieben statt in Projekteinstellungen)

      Mach ich immer so. Hab mich halt so dran gewöhnt das explizit hinzuschreiben. Dann passt das auch für die, die das nicht in den Standardeinstellungen eingestellt haben. ;)

      ErfinderDesRades schrieb:

      Und/Aber hast noch den MVB-Namespace drinne

      Da geb ich Dir recht. Asche auf mein Haupt.

      Edit: Zu meiner Verteidigung ;) , das eigentliche große Projekt ist tatsächlich in VB6 geschrieben. Da hab ich halt einiges so übernommen. Eingangs hatte ich ja auch erwähnt das man das auch anders programmieren kann. Mir ging es hauptsächlich darum zu zeigen, wie es funktioniert.
      Mfg -Franky-

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „-Franky-“ ()

      nullnull

      ErfinderDesRades schrieb:

      Bei mir gehts leider nicht, vmt. weil ich Win7 unterwegs bin?

      Jupp, die IMFMediaEngine/Ex gibt es erst ab Windows 8. In dem Fall müsstest auf andere Media Foundation Interfaces ausweichen die es ab Windows Vista gibt. Der Aufwand ist dann allerdings höher.

      Edit: Hier wird der Weg ab Windows Vista gezeigt. docs.microsoft.com/en-us/windo…y-unprotected-media-files
      Mfg -Franky-

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „-Franky-“ ()

      @ErfinderDesRades

      Ich hab mal fix einen anderen VBC Code nach VB.NET übersetzt. Dieser Code zeigt halt einen einfachen Player der ab Windows Vista lauffähig ist / sein sollte. Der VBC Code ist für dieses VB.NET Beispiel auf ein minimum zusammengedampft worden, nicht kommentiert, und zeigt nur wie es grundsätzlich funktioniert. Alles andere wie die Events vom IMFMediaEventGenerator, weitere Services die per MFGetService erstellt werden können usw sind hier nicht vorhanden. Das kann ja dann jeder selbst einbauen.
      Dateien
      Mfg -Franky-
      Bin dabei meinen uralt-Audio-Player von DirectShow auf Media Foundation umzustellen.
      Dabei bin ich auf den MediaEngine Player von Franky gestoßen, der als Grundlage für meine Zwecke schon sehr gut geeignet ist. Eine Sache fehlt mir aber noch. Ich möchte die Wiedergabeposition ändern können.
      Zum Beispiel mit Scroll-Funktion einer SeekTrackBar.
      Das sollte wohl mit MF_MEDIA_ENGINE_EVENT_SEEKED gehen. Leider sind Beispiele im Netz nur
      in C++ zu finden. Da sind meine Kenntnisse nur rudimentär.
      Wie müsste eine Funktion in VB.NET aussehen?

      MfG HaLi22
      MF_MEDIA_ENGINE_EVENT_SEEKED steht für ein event wenn der Player zu einer stelle spult, das kann man nutzen um den Trackbar-Wert einzustellen. Aber es wird darauf hinauslaufen, dass du ein interface implementieren muss. So wie -Franky- es im Beispiel zeigt.

      Das Interface:
      learn.microsoft.com/en-us/wind…ediaengine-imfmediaengine

      Funktionen zum setzen und holen der Position:(oben das Interface)
      learn.microsoft.com/en-us/wind…ediaengine-setcurrenttime
      learn.microsoft.com/en-us/wind…ediaengine-getcurrenttime
      Zitat von mir 2023:
      Was interessiert mich Rechtschreibung? Der Compiler wird meckern wenn nötig :D
      @HaLi22 DTF hat Dir ja bereits die entsprechende Funktion genannt. Kleiner Hinweis von mir: Setze eine neue Position, wenn ein Medium abgespielt wird, nicht im Scroll-Event sondern zB. erst bei einem MouseUp-Event. Sonst hört sich das ganze wie stottern an wenn die neue Position im Scroll-Event gesetzt wird. Das abspielen wird ja pausiert, dann wird der Seek ausgeführt und dann weiter abgespielt. Das geht zwar recht schnell, hört sich dennoch im Scroll-Event komisch an. Besser ist es auf die entsprechenden Events des Players zu reagieren. Dann weis man auch, wann man den nächsten Seek aufrufen kann. Also erst nachdem ein vorhergehender Seek beendet wurde.
      Mfg -Franky-

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „-Franky-“ ()