Webradio mit Python: HTTP-Streaming für MP3 und M3U leicht gemacht
15. März 2025
Ich habe schon länger mit dem Gedanken gespielt, einen eigenen Webradio-Stream für mein lokales Netzwerk einzurichten – idealerweise mit einer einfachen grafischen Oberfläche, um die Songs und Playlists zu verwalten. Nach einigen Experimenten und Fehlschlägen habe ich endlich ein funktionierendes Python-Skript erstellt, das genau das macht: Es streamt MP3-Dateien oder M3U-Playlists über HTTP, sodass man sie mit einem Client wie VLC abspielen kann.
Das Beste daran? Es kommt mit einer hübschen GUI in sanften Graublau-Tönen, die die Bedienung kinderleicht macht. In diesem Artikel teile ich das Skript, erkläre, wie es funktioniert, was nicht geklappt hat, und wie man den Stream auch über das Internet zugänglich macht.
Die GUI des Web-Streaming-Servers. Die Pfadangaben zu den Ordnern und M3U-Listen können in einer einer ini-Datei eingetragen werden.
Bedienung: Das Skript ist sehr einfach zu bedienen. Nach dem Start öffnet sich ein Fenster, in dem man entweder einen Ordner mit MP3-Dateien oder eine M3U-Playlist auswählen kann. Ein Klick auf „Stream starten“ startet den HTTP-Server, und der Statusbereich zeigt die URL an, unter der der Stream erreichbar ist – z. B. http://192.168.1.233:8000/stream. Diese URL kann man dann in VLC eingeben, um den Stream abzuspielen. Mit „Stream stoppen“ wird der Server sauber beendet. Wichtig: Wenn man einen Ordner auswählt, sollten nur MP3-Dateien darin enthalten sein, da andere Dateitypen (z. B. Bilder oder Textdateien) den Abspielprozess stören können. Sowohl M3U-Playlists als auch MP3-Ordner werden in einer Dauerschleife abgespielt, sodass der Stream kontinuierlich läuft, bis man ihn stoppt.
Beispiel einer Ini-Datei: Ein Muster wird automatisch angelegt.
[Paths]
path1 = /home/volker/Schreibtisch/MP3 Mix Smooth Jazz
path2 = /home/volker/Schreibtisch/Verknüpfung mit MP4 Smoth Jazz Pat Metheny Paul Hardcastle/mp3/Pat_Metheny_Group/playlist.m3u
path3 = /home/volker/Schreibtisch/Verknüpfung mit MP4 Smoth Jazz Pat Metheny Paul Hardcastle/mp3/Paul_Hardcastle_III
path4 = /home/volker/Schreibtisch/Miles Davis - Kind of Blue/playlist.m3u
path5 = /home/volker/Musik/MP3/60er 70er Klassiker
path6 = /home/volker/Musik/MP3/Konstantin Klashtorni - Kool & Klean - Vol. II (2011)/playlist.m3u
path7 = /home/volker/Musik/MP3/Chris Standring - Compilation/playlist.m3u
path8 = /home/volker/Musik/MP3/playlist.m3u
Funktion: Das Skript basiert auf mehreren Bausteinen. Die grafische Oberfläche wird mit tkinter erstellt, einer Standardbibliothek von Python, die ich mit einem Graublau-Farbschema optisch aufgewertet habe. Der Streaming-Server nutzt Pythons http.server-Modul in Kombination mit socketserver.ThreadingMixIn, um mehrere Clients gleichzeitig zu unterstützen (theoretisch bis zu MAX_CLIENTS = 5, aber das ist aktuell nicht strikt begrenzt). Die Audiodaten werden in Chunks gesendet, wobei das Transfer-Encoding: chunked-Protokoll verwendet wird, um einen kontinuierlichen Stream zu gewährleisten. Das Skript lädt entweder alle MP3-Dateien aus einem Ordner oder liest eine M3U-Playlist und streamt die Songs in einer Endlosschleife.
Was nicht geklappt hat: Ich habe anfangs versucht, Metadaten (wie Titel und Interpret) in den Stream einzubetten, damit VLC sie anzeigen kann. Das ist leider nicht gelungen – die Metadaten wurden nicht korrekt übertragen, und ich habe nach mehreren Ansätzen (z. B. mit icy-metaint und Metadaten-Blöcken) aufgegeben. Ebenso wollte ich eine Anzeige einbauen, die zeigt, wie viele Clients gerade zuhören, aber das war mit der aktuellen Implementierung zu komplex und hat nicht zuverlässig funktioniert. Außerdem hatte ich Probleme mit verzerrtem Ton, als ich versucht habe, die Audiodaten manuell in Chunks zu splitten – das lag an der fehlenden Frame-Awareness für MP3-Dateien. Letztlich habe ich diese Features weggelassen, um eine stabile Basisversion zu haben.
Streaming über das Internet: Um den Stream nicht nur im lokalen Netzwerk, sondern auch über das Internet zugänglich zu machen, sind ein paar Schritte nötig:
Portforwarding: Logge dich in deinen Router ein (oft über 192.168.1.1 im Browser) und richte ein Portforwarding für Port 8000 ein. Weiterleite den Port 8000 (TCP) an die lokale IP-Adresse deines Computers, auf dem das Skript läuft (z. B. 192.168.1.233). Die genaue Vorgehensweise hängt vom Router ab – bei einer Fritzbox findest du das unter „Internet > Freigaben > Portfreigaben“.
Öffentliche IP-Adresse: Finde deine öffentliche IP-Adresse heraus, z. B. über eine Webseite wie whatismyipaddress.com. Diese IP wird sich bei den meisten Internetanbietern regelmäßig ändern, da sie dynamisch ist.
DynDNS-Dienst: Um mit einer dynamischen IP umzugehen, kannst du einen DynDNS-Dienst wie No-IP oder DynDNS nutzen. Registriere dich dort, erstelle einen Hostnamen (z. B. meinwebradio.ddns.net), und installiere den DynDNS-Client auf deinem Computer oder Router, damit deine öffentliche IP automatisch aktualisiert wird.
Stream abrufen: Sobald alles eingerichtet ist, kannst du den Stream von außerhalb deines Netzwerks über die URL http://meinwebradio.ddns.net:8000/stream (oder deine öffentliche IP) in VLC abspielen. Beachte, dass dein Router möglicherweise zusätzliche Sicherheitsfunktionen wie eine Firewall hat – stelle sicher, dass Port 8000 nicht blockiert wird.
Sicherheitshinweis: Wenn du deinen Stream öffentlich machst, solltest du dich mit Sicherheitsaspekten auseinandersetzen, z. B. den Zugriff mit einer Firewall beschränken oder das Skript um eine Authentifizierung erweitern, damit nicht jeder darauf zugreifen kann.
Abhängigkeiten: Das Skript hat minimale Abhängigkeiten, da es fast ausschließlich auf Python-Standardbibliotheken setzt:
Python 3: Das Skript läuft mit Python 3 (getestet mit Python 3.10.12).
tkinter: Für die GUI – normalerweise in Python enthalten, unter Debian/Ubuntu ggf. nachinstallieren mit sudo apt install python3-tk.
Keine weiteren externen Bibliotheken wie ffmpeg oder gTTS sind nötig, im Gegensatz zu meinen früheren Ansätzen.
Das Skript wurde auf Debian und Ubuntu getestet und läuft auf beiden Systemen einwandfrei. Für die Installation der Abhängigkeiten reicht dieser Befehl:
Client: Ich habe den Stream mit VLC als Client getestet. Einfach die URL (z. B. http://192.168.1.233:8000/stream) in VLC unter „Medien > Netzwerkstream öffnen“ eingeben, und schon läuft die Musik. VLC ist stabil und unterstützt das chunked Encoding, das das Skript verwendet, problemlos.
Code des Skripts: Web Radio Streaming Server
import os
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
import socketserver
import errno
import threading
import tkinter as tk
from tkinter import ttk, messagebox
import configparser
# Farbschema (sanfte Graublau-Töne)
COLORS = {
"bg_dark": "#2E3B4E",
"bg_main": "#3D4F65",
"bg_light": "#4A5D75",
"text": "#E0E7EF",
"accent": "#6B8CAE",
"button": "#5A7A9E"
}
# Konfigurationsdatei für Pfade
CONFIG_FILE = "webradio_paths.ini"
# Maximale Anzahl der Clients (für zukünftige Erweiterungen, aktuell nicht begrenzt)
MAX_CLIENTS = 5
class RadioStreamHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/stream":
self.send_response(200)
self.send_header("Content-type", "audio/mpeg")
self.send_header("Transfer-Encoding", "chunked")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "keep-alive")
self.send_header("icy-name", "Mein LAN-Radio")
self.end_headers()
playlist = self.server.get_playlist()
if not playlist:
self.wfile.write(b"0\r\n\r\n")
return
while True:
for mp3_file in playlist:
try:
with open(mp3_file, "rb") as f:
audio_data = f.read()
chunk_size = len(audio_data)
self.wfile.write(f"{chunk_size:x}\r\n".encode())
self.wfile.write(audio_data)
self.wfile.write(b"\r\n")
self.wfile.flush()
print(f"Spiele für Client {self.client_address}: {os.path.basename(mp3_file)}")
except IOError as e:
if e.errno == errno.EPIPE:
print(f"Client {self.client_address} getrennt bei {mp3_file}, fahre fort...")
return # Client-Verbindung endet, aber andere können weiterlaufen
else:
print(f"Fehler bei {mp3_file} für Client {self.client_address}: {e}")
return
except Exception as e:
print(f"Unbekannter Fehler bei {mp3_file} für Client {self.client_address}: {e}")
return
time.sleep(0.1) # Kurze Pause zwischen Songs für Synchronisation
else:
self.send_error(404, "Nur /stream verfügbar")
class ThreadingRadioServer(socketserver.ThreadingMixIn, HTTPServer):
def __init__(self, server_address, handler_class, music_dir=None, m3u_file=None):
super().__init__(server_address, handler_class)
self.music_dir = music_dir
self.m3u_file = m3u_file
self.playlist = self.load_playlist()
def load_playlist(self):
if self.m3u_file and os.path.exists(self.m3u_file):
with open(self.m3u_file, "r") as f:
playlist = []
for line in f:
line = line.strip()
if line and line.endswith(".mp3"):
# Absoluter Pfad oder relativ zum M3U-Ordner
if os.path.isabs(line):
mp3_path = line
else:
mp3_path = os.path.join(os.path.dirname(self.m3u_file), line)
if os.path.exists(mp3_path):
playlist.append(mp3_path)
else:
print(f"Datei nicht gefunden: {mp3_path}")
return playlist
elif self.music_dir:
return [os.path.join(self.music_dir, f) for f in os.listdir(self.music_dir) if f.endswith(".mp3")]
return []
def get_playlist(self):
return self.playlist
class WebradioApp:
def __init__(self, root):
self.root = root
self.root.title("Webradio Streaming Server")
self.root.geometry("600x500")
self.root.configure(bg=COLORS["bg_main"])
self.server = None
self.server_thread = None
self.paths = self.load_paths()
self.create_styles()
self.create_widgets()
def load_paths(self):
"""Lade Pfade aus der INI-Datei"""
config = configparser.ConfigParser()
if not os.path.exists(CONFIG_FILE):
# Standard-Pfade, falls die INI-Datei nicht existiert
config['Paths'] = {
'path1': '/home/volker/Schreibtisch/MP3 Mix Smooth Jazz',
'path2': '/home/volker/Musik'
}
with open(CONFIG_FILE, 'w') as configfile:
config.write(configfile)
config.read(CONFIG_FILE)
return list(config['Paths'].values())
def create_styles(self):
"""Definiere die Stile für ttk-Widgets"""
self.style = ttk.Style()
self.style.configure("TFrame", background=COLORS["bg_main"])
self.style.configure("TLabel",
background=COLORS["bg_main"],
foreground=COLORS["text"],
font=("Arial", 10))
self.style.configure("TButton",
background=COLORS["button"],
foreground=COLORS["text"],
font=("Arial", 10, "bold"))
self.style.configure("TListbox",
background=COLORS["bg_light"],
foreground=COLORS["text"])
self.style.map("TButton",
background=[("active", COLORS["accent"])])
self.style.configure("Header.TLabel",
font=("Arial", 14, "bold"),
padding=10)
self.style.configure("Section.TLabel",
font=("Arial", 12, "bold"),
padding=(0, 10, 0, 5))
def create_widgets(self):
"""Erstelle die GUI-Elemente"""
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
# Titel
title_label = ttk.Label(main_frame, text="Webradio Streaming Server",
style="Header.TLabel")
title_label.pack(fill=tk.X)
# Auswahlbereich
selection_section = ttk.Label(main_frame, text="Ordner oder M3U auswählen:",
style="Section.TLabel")
selection_section.pack(fill=tk.X)
self.listbox = tk.Listbox(main_frame,
bg=COLORS["bg_light"],
fg=COLORS["text"],
selectbackground=COLORS["accent"],
relief="flat",
height=10)
self.listbox.pack(fill=tk.BOTH, expand=True, pady=5)
# Fülle die Listbox mit Pfaden
for path in self.paths:
self.listbox.insert(tk.END, path)
# Aktionsbereich
actions_frame = ttk.Frame(main_frame)
actions_frame.pack(fill=tk.X, pady=10)
start_button = ttk.Button(actions_frame,
text="Stream starten",
command=self.start_stream)
start_button.pack(side=tk.LEFT, padx=5)
stop_button = ttk.Button(actions_frame,
text="Stream stoppen",
command=self.stop_stream)
stop_button.pack(side=tk.LEFT, padx=5)
# Status
self.status_var = tk.StringVar()
self.status_var.set("Bereit")
status_frame = ttk.Frame(main_frame)
status_frame.pack(fill=tk.X, pady=10)
status_label = ttk.Label(status_frame, text="Status:")
status_label.pack(side=tk.LEFT, padx=5)
status_text = ttk.Label(status_frame, textvariable=self.status_var)
status_text.pack(side=tk.LEFT, padx=5)
def start_stream(self):
"""Starte den Streaming-Server"""
if self.server:
messagebox.showwarning("Warnung", "Der Server läuft bereits!")
return
selection = self.listbox.curselection()
if not selection:
messagebox.showwarning("Warnung", "Bitte wählen Sie einen Ordner oder eine M3U-Datei aus!")
return
selected_path = self.listbox.get(selection[0])
music_dir = None
m3u_file = None
if selected_path.endswith(".m3u"):
m3u_file = selected_path
else:
music_dir = selected_path
self.status_var.set("Starte Server...")
self.root.update_idletasks()
try:
server_address = ("0.0.0.0", 8000)
self.server = ThreadingRadioServer(server_address, RadioStreamHandler, music_dir, m3u_file)
local_ip = self.get_local_ip()
self.status_var.set(f"LAN-Radio läuft auf http://{local_ip}:8000/stream (bis zu {MAX_CLIENTS} Clients)")
self.server_thread = threading.Thread(target=self.server.serve_forever)
self.server_thread.daemon = True
self.server_thread.start()
except Exception as e:
self.status_var.set("Fehler beim Starten des Servers")
messagebox.showerror("Fehler", f"Fehler beim Starten des Servers: {e}")
self.server = None
def stop_stream(self):
"""Stoppe den Streaming-Server"""
if not self.server:
messagebox.showwarning("Warnung", "Kein Server läuft!")
return
self.status_var.set("Stoppe Server...")
self.root.update_idletasks()
try:
self.server.shutdown()
self.server.server_close()
self.server = None
self.server_thread = None
self.status_var.set("Server gestoppt")
except Exception as e:
self.status_var.set("Fehler beim Stoppen")
messagebox.showerror("Fehler", f"Fehler beim Stoppen des Servers: {e}")
def get_local_ip(self):
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
except Exception:
ip = "localhost"
finally:
s.close()
return ip
if __name__ == "__main__":
root = tk.Tk()
app = WebradioApp(root)
root.mainloop()
Modifikation für mpg123 und Asterisk: Falls der Stream in Asterisk oder mit mpg123 abgespielt werden soll, sind Modifikationen am obigen Skript notwendig. Dadurch wird der Stream nicht mehr in Chunks gesendet sondern an einem Stück. Nachfolgend die Änderungen:
class RadioStreamHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/stream":
self.send_response(200)
self.send_header("Content-type", "audio/mpeg")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "keep-alive")
self.send_header("icy-name", "Mein LAN-Radio")
self.end_headers()
playlist = self.server.get_playlist()
if not playlist:
self.wfile.write(b"") # Leere Antwort bei leerer Playlist
return
while True:
for mp3_file in playlist:
try:
with open(mp3_file, "rb") as f:
audio_data = f.read()
self.wfile.write(audio_data) # Direkt schreiben ohne Chunk-Header
self.wfile.flush()
print(f"Spiele für Client {self.client_address}: {os.path.basename(mp3_file)}")
except IOError as e:
if e.errno == errno.EPIPE:
print(f"Client {self.client_address} getrennt bei {mp3_file}, fahre fort...")
return
else:
print(f"Fehler bei {mp3_file} für Client {self.client_address}: {e}")
return
except Exception as e:
print(f"Unbekannter Fehler bei {mp3_file} für Client {self.client_address}: {e}")
return
time.sleep(0.1) # Kurze Pause zwischen Songs
else:
self.send_error(404, "Nur /stream verfügbar")
Skript zum automatischen Erzeugen von m3u-Dateien: Liest alle mp3-Dateien in den Unterordnern und macht daraus eine M3U-Datei.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Dieses Python-Skript erstellt eine M3U-Playlist aus den Audio-Dateien in dem Verzeichnis, in dem das Skript ausgeführt wird, sowie in allen Unterordnern. Es unterstützt .mp3- und .mp4-Dateien und ist darauf ausgelegt, Metadaten von .mp3-Dateien zu extrahieren, um diese in die Playlist zu integrieren. Relative Pfade werden verwendet.
Erstellt mit Grok unter Anleitung von Volker Lange-Janson SM5ZBS, angepasst am 16. März 2025.
Jeder darf damit machen, was er will.
"""
import os
try:
from mutagen.mp3 import MP3
mutagen_available = True
except ImportError:
mutagen_available = False
# Definiere die Dateiendungen, die in die Playlist aufgenommen werden sollen
erlaubte_endungen = ('.mp3',)
# Ermittle das Verzeichnis, in dem sich das Skript befindet
skript_verzeichnis = os.path.dirname(os.path.abspath(__file__))
# Erstelle den Namen der m3u-Datei
m3u_datei = os.path.join(skript_verzeichnis, 'playlist.m3u')
def get_mp3_info(dateipfad):
"""Versucht, MP3-Metadaten zu extrahieren, wenn mutagen verfügbar ist."""
if not mutagen_available:
return "Unknown Artist - Unknown Title", None, 0
try:
audio = MP3(dateipfad)
title = audio.get('TIT2', ['Unknown Title'])[0]
artist = audio.get('TPE1', ['Unknown Artist'])[0]
album = audio.get('TALB', ['Unknown Album'])[0]
track = audio.get('TRCK', ['0'])[0].split('/')[0] # Nur die erste Zahl der Tracknummer nehmen
duration = int(audio.info.length) if audio.info else 0
return f"{artist} - {title} ({album})", int(track) if track.isdigit() else None, duration
except Exception:
return "Unknown Artist - Unknown Title", None, 0
# Dateien sortieren nach Tracknummer und Dateiname
def datei_sortierschluessel(dateipfad):
if dateipfad.lower().endswith('.mp3'):
_, tracknummer, _ = get_mp3_info(dateipfad)
return (tracknummer if tracknummer is not None else float('inf'), dateipfad.lower())
return (float('inf'), dateipfad.lower())
# Alle Dateien aus Verzeichnis und Unterordnern sammeln
def finde_dateien(verzeichnis):
dateien = []
for wurzel, _, dateiliste in os.walk(verzeichnis):
for datei in dateiliste:
if datei.lower().endswith(erlaubte_endungen):
voller_pfad = os.path.join(wurzel, datei)
relativer_pfad = os.path.relpath(voller_pfad, skript_verzeichnis)
dateien.append(relativer_pfad)
return sorted(dateien, key=datei_sortierschluessel)
# Dateien finden und sortieren
dateien = finde_dateien(skript_verzeichnis)
# M3U-Datei schreiben
with open(m3u_datei, 'w', encoding='utf-8') as playlist:
playlist.write("#EXTM3U\n")
for relativer_pfad in dateien:
voller_pfad = os.path.join(skript_verzeichnis, relativer_pfad)
info, _, duration = get_mp3_info(voller_pfad) if relativer_pfad.lower().endswith('.mp3') else (relativer_pfad, None, 0)
playlist.write(f"#EXTINF:{duration},{info}\n{relativer_pfad}\n")
# Falls MP3, Metadaten in der Konsole ausgeben
if relativer_pfad.lower().endswith('.mp3'):
print(f"{relativer_pfad}: {info}")
print(f"Playlist erfolgreich erstellt: {m3u_datei}")
Fazit: Das Skript ist eine einfache, aber effektive Lösung, um ein Webradio im lokalen Netzwerk oder über das Internet zu streamen. Es ist nicht perfekt – die fehlenden Metadaten und die Client-Anzeige sind Schwächen, die ich in Zukunft vielleicht noch angehe. Aber für den Moment erfüllt es seinen Zweck: unkompliziertes Streaming mit einer benutzerfreundlichen Oberfläche. Für alle, die ein kleines Bastelprojekt suchen oder einfach nur ihre MP3-Sammlung im Netzwerk abspielen wollen, ist das Skript ein guter Startpunkt. Viel Spaß beim Streamen!
Anmerkung: Die Skripte und Texte einschließlich der Formatierung wurden fast vollständig von Grok 3 verfasst und ließen sich als HTML direkt in WordPress kopieren. Vollautomatisch geht es noch nicht. Dieser Artikel dient auch als Beispiel, was bereits im März 2025 mit einem Chatbot machbar ist.
DATENSCHUTZEINSTELLUNGEN - Diese Webseite verwendet Cookies. Wir verwenden Cookies, um Inhalte und Anzeigen zu personalisieren, Funktionen für soziale Medien anbieten zu können und die Zugriffe auf unsere Website zu analysieren. Sie geben Einwilligung zu unseren Cookies, wenn Sie unsere Webseite weiterhin nutzen. Für den Betrieb der Website nicht notwendige Cookies können Sie hier in den Einstellungen deaktivieren:
This website uses cookies to improve your experience while you navigate through the website. Out of these, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may affect your browsing experience.
Necessary cookies are absolutely essential for the website to function properly. These cookies ensure basic functionalities and security features of the website, anonymously.
Cookie
Dauer
Beschreibung
cookielawinfo-checkbox-analytics
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Analytics".
cookielawinfo-checkbox-functional
11 months
The cookie is set by GDPR cookie consent to record the user consent for the cookies in the category "Functional".
cookielawinfo-checkbox-necessary
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary".
cookielawinfo-checkbox-others
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Other.
cookielawinfo-checkbox-performance
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Performance".
viewed_cookie_policy
11 months
The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data.
Functional cookies help to perform certain functionalities like sharing the content of the website on social media platforms, collect feedbacks, and other third-party features.
Performance cookies are used to understand and analyze the key performance indexes of the website which helps in delivering a better user experience for the visitors.
Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics the number of visitors, bounce rate, traffic source, etc.
Advertisement cookies are used to provide visitors with relevant ads and marketing campaigns. These cookies track visitors across websites and collect information to provide customized ads.