{"id":7174,"date":"2026-10-22T10:18:17","date_gmt":"2026-10-22T08:18:17","guid":{"rendered":"https:\/\/www.pschatzmann.ch\/home\/?p=7174"},"modified":"2026-10-22T10:48:25","modified_gmt":"2026-10-22T08:48:25","slug":"arduino-audio-tools-streaming-video-and-audio-via-a-webserver","status":"publish","type":"post","link":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/","title":{"rendered":"Arduino Audio Tools: Streaming Video and Audio via a Webserver"},"content":{"rendered":"<p>If you have ever streamed video from an ESP32, you have most likely used the <strong>CameraWebServer example<\/strong>: the camera delivers JPEG images, which the web server pushes one after the other to the browser. This works, but there is <strong>no sound<\/strong>, the bandwidth is high, and the result is not a real video stream that you could record or play in VLC.<\/p>\n<p>A real stream needs a <strong>video codec such as H.264, an audio codec such as AAC, and a container <\/strong>that combines both with correct timestamps &#8211; tasks normally left to desktop tools like ffmpeg. The building blocks that exist for microcontrollers are scattered and chip-specific, and none of them knows about the others.<\/p>\n<p><em>This is the gap my libraries fill!<\/em><\/p>\n<p>This blog shows how to stream live video from a camera, together with sound from a microphone, from a <strong>microcontroller<\/strong> to <strong>VLC, ffplay or a web browser<\/strong> over HTTP. It is based on the <a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-tools\/tree\/main\/examples\/examples-videos\/stream\/webserver-video-ext\">webserver-video-ext.ino<\/a> example that can be found in the <a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-tools\">Arduino Audio Tools library<\/a>. For the container and codec basics see the <a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-tools\/wiki\/Video\">Video Overview<\/a>.<\/p>\n<p>The example has been tested on a <a href=\"https:\/\/docs.waveshare.com\/ESP32-P4-WIFI6\">ESP32-P4-WIFI6 board<\/a>:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2026\/10\/ESP32P4-300x227.jpg\" alt=\"\" width=\"300\" height=\"227\" class=\"alignnone size-medium wp-image-7179\" srcset=\"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2026\/10\/ESP32P4-300x227.jpg 300w, https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2026\/10\/ESP32P4.jpg 391w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/p>\n<p><strong>Contents<\/strong><\/p>\n<ul>\n<li><a href=\"#choosing-the-formats\">Choosing the Formats<\/a><\/li>\n<li><a href=\"#the-pipeline\">The Pipeline<\/a><\/li>\n<li><a href=\"#live-streaming-what-is-different\">Live Streaming: What Is Different<\/a><\/li>\n<li><a href=\"#watching-the-stream\">Watching the Stream<\/a><\/li>\n<li><a href=\"#the-example-step-by-step\">The Example Step by Step<\/a><\/li>\n<li><a href=\"#exchanging-the-components\">Exchanging the Components<\/a><\/li>\n<li><a href=\"#performance\">Performance<\/a><\/li>\n<li><a href=\"#troubleshooting\">Troubleshooting<\/a><\/li>\n<\/ul>\n<h3 id=\"choosing-the-formats\">Choosing the Formats<\/h3>\n<p>A live stream has no end and viewers can join at any time. This rules out containers that need an index or a header describing the whole file (MP4) or that are meant to be read from the start (AVI). The natural choice is the <strong>MPEG Transport Stream (MTS \/ TS)<\/strong>: it consists of independent 188 byte packets, and <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_muxer_m_t_s.html\">MuxerMTS<\/a> repeats the stream description (PAT\/PMT) before every keyframe, so a viewer that joins in the middle of the stream finds everything it needs to start decoding.<\/p>\n<p>The usual codecs in a transport stream are:<\/p>\n<ul>\n<li><strong>Video: H.264<\/strong> &#8211; supported by every player and by browsers.<\/li>\n<li><strong>Audio: AAC<\/strong> &#8211; in ADTS frames, as <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_muxer_m_t_s.html\">MuxerMTS<\/a> expects them.<\/li>\n<\/ul>\n<p>MTS can also carry MJPEG, but there is no standard way to signal it in a transport stream, so generic players and browsers won&#39;t recognize it.<\/p>\n<h3 id=\"the-pipeline\">The Pipeline<\/h3>\n<pre><code>camera  -&gt; CameraFrameSourceTinyCamera -&gt; H264EncoderESP32P4 --\\\r\n                                                                 &gt; MuxerMTS -&gt; ExtensionStream -&gt; HTTP clients\r\nmic     -&gt; I2SCodecStream              -&gt; AACEncoderVO -------\/\r\n<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Component<\/th>\n<th>Class<\/th>\n<th>Library<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Camera<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_camera_frame_source_tiny_camera.html\">CameraFrameSourceTinyCamera<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/TinyCamera\">TinyCamera<\/a><\/td>\n<\/tr>\n<tr>\n<td>H.264 encoder (ESP32-P4 hardware)<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_h264_encoder_e_s_p32_p4.html\">H264EncoderESP32P4<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/codec-h264-ESP32P4\">codec-h264-ESP32P4<\/a><\/td>\n<\/tr>\n<tr>\n<td>H.264 encoder (software, any board)<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_h264_encoder.html\">H264Encoder<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/TinyH264\">TinyH264<\/a><\/td>\n<\/tr>\n<tr>\n<td>Microphone<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_i2_s_codec_stream.html\">I2SCodecStream<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-driver\">arduino-audio-driver<\/a><\/td>\n<\/tr>\n<tr>\n<td>AAC encoder<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_a_a_c_encoder_v_o.html\">AACEncoderVO<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/codec-vo-aacenc\">codec-vo-aacenc<\/a><\/td>\n<\/tr>\n<tr>\n<td>Container<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_muxer_m_t_s.html\">MuxerMTS<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-tools\">arduino-audio-tools<\/a><\/td>\n<\/tr>\n<tr>\n<td>Driver of the pipeline<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_video_muxer_with_tasks.html\">VideoMuxerWithTasks<\/a> or <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_video_muxer.html\">VideoMuxer<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-tools\">arduino-audio-tools<\/a><\/td>\n<\/tr>\n<tr>\n<td>Web server<\/td>\n<td><code>HttpServer<\/code> and <code>ExtensionStream<\/code><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/TinyHttp\">TinyHttp<\/a><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h4 id=\"videomuxer-or-videomuxerwithtasks\">VideoMuxer or VideoMuxerWithTasks?<\/h4>\n<p>For <strong>video only<\/strong>, <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_video_muxer.html\">VideoMuxer<\/a> is good enough: it is driven by calling <code>copy()<\/code> in <code>loop()<\/code>, which captures, encodes and writes one frame at the configured frame rate, and needs no tasks and no mutex.<\/p>\n<p>To stream <strong>video and audio<\/strong> together from live sources, use <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_video_muxer_with_tasks.html\">VideoMuxerWithTasks<\/a>. The microphone delivers its samples continuously and only has a small buffer, while capturing and encoding a video frame takes a long time in comparison. In a single loop the microphone isn&#39;t read while a frame is being encoded, so its buffer overflows and audio is lost &#8211; and the video frames are delayed by the audio processing in turn. <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_video_muxer_with_tasks.html\">VideoMuxerWithTasks<\/a> runs one task for each track: the video task captures and encodes a frame at the configured frame rate, and the audio task is paced by the blocking reads from the microphone. Both run independently, so no video frames and no audio are lost.<\/p>\n<h3 id=\"live-streaming-what-is-different\">Live Streaming: What Is Different<\/h3>\n<h4 id=\"timestamps-from-real-time\">Timestamps from real time<\/h4>\n<p>By default <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_muxer_m_t_s.html\">MuxerMTS<\/a> calculates the video timestamps from the frame number and the configured frame rate. This is right for a file, but wrong for a live source that delivers fewer frames than configured: the player then shows the frames faster than they arrive, and its latency grows steadily (we measured +5 seconds after 30 seconds, with 6.6 instead of 8 frames\/s). For a live source call<\/p>\n<pre><code class=\"language-C++\">mts.setRealTime(true);\r\n<\/code><\/pre>\n<p>so that the video timestamps are taken from the actual capture time. The latency then stays constant, whatever frame rate the board achieves.<\/p>\n<h4 id=\"keyframes\">Keyframes<\/h4>\n<p>A viewer can only start decoding at a keyframe (IDR frame). Set the keyframe interval (GOP) to about one second, e.g. <code>encoder.setGop(fps)<\/code>, so that a new viewer sees a picture after one second at the latest. Players report the frames before the first keyframe as errors (e.g. ffmpeg: &quot;non-existing PPS 0 referenced&quot;): this is expected and harmless.<\/p>\n<h4 id=\"multiple-viewers\">Multiple viewers<\/h4>\n<p>TinyHttp&#39;s <code>ExtensionStream<\/code> keeps a list of all clients that requested its URL and sends everything written to it to each of them. Since it is a Stream, it can be used directly as output of the muxer: each frame is encoded only once, however many viewers are connected. A slow client slows down all of them, since the writes are blocking.<\/p>\n<h4 id=\"tasks-and-the-mutex\">Tasks and the mutex<\/h4>\n<p>The video task, the audio task and the web server in <code>loop()<\/code> all use the muxer and the client list of the <code>ExtensionStream<\/code>, which are not thread safe: the web server adds new clients to the list while the tasks are writing to it. Pass one mutex to <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_video_muxer_with_tasks.html\">VideoMuxerWithTasks<\/a> and lock the same mutex in <code>loop()<\/code> around <code>server.copy()<\/code>. Wait outside of the lock, and switch off TinyHttp&#39;s own wait (<code>server.setNoConnectDelay(0)<\/code>), so that the tasks never wait for a sleeping <code>loop()<\/code>.<\/p>\n<h4 id=\"wifi-power-save\">WiFi power save<\/h4>\n<p>WiFi power save delays and throttles the outgoing data. TinyHttp&#39;s <code>server.begin(port, ssid, password)<\/code> (and AudioServer) switch it off on ESP32, so you don&#39;t need to call <code>WiFi.setSleep(false)<\/code> yourself.<\/p>\n<h3 id=\"watching-the-stream\">Watching the Stream<\/h3>\n<h4 id=\"vlc-ffplay-mpv\">VLC, ffplay, mpv<\/h4>\n<p>Open the stream URL, e.g. <code>http:\/\/192.168.1.33\/stream.ts<\/code>. The players buffer the network stream, which adds to the latency: VLC by 1 second by default. You can reduce this with<\/p>\n<pre><code>vlc --network-caching=200 http:\/\/192.168.1.33\/stream.ts\r\nffplay -fflags nobuffer -flags low_delay -framedrop http:\/\/192.168.1.33\/stream.ts\r\n<\/code><\/pre>\n<h4 id=\"web-browser\">Web browser<\/h4>\n<p>Browsers can&#39;t play an MPEG transport stream in a <code>&lt;video&gt;<\/code> element. <a href=\"https:\/\/github.com\/xqq\/mpegts.js\">mpegts.js<\/a> converts it in the browser into a format the video element can play (using Media Source Extensions). The example serves a small page at <code>\/<\/code> which loads mpegts.js from a CDN (so the browser needs internet access) and plays <code>\/stream.ts<\/code> in live mode with low latency settings:<\/p>\n<pre><code class=\"language-html\">&lt;script src=&quot;https:\/\/cdn.jsdelivr.net\/npm\/mpegts.js@1.7.3\/dist\/mpegts.js&quot;&gt;&lt;\/script&gt;\r\n&lt;video id=&quot;video&quot; muted autoplay playsinline controls&gt;&lt;\/video&gt;\r\n&lt;script&gt;\r\nconst player = mpegts.createPlayer(\r\n    {type: &#39;mpegts&#39;, isLive: true, url: location.origin + &#39;\/stream.ts&#39;},\r\n    {enableStashBuffer: false, liveBufferLatencyChasing: true});\r\nplayer.attachMediaElement(document.getElementById(&#39;video&#39;));\r\nplayer.load();\r\nplayer.play();\r\n&lt;\/script&gt;\r\n<\/code><\/pre>\n<p>Browsers only autoplay muted videos: use the player controls to switch the sound on. Browsers without Media Source Extensions (e.g. Safari on older iPhones) can&#39;t play the stream this way.<\/p>\n<h3 id=\"the-example-step-by-step\">The Example Step by Step<\/h3>\n<p>The <a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-tools\/tree\/main\/examples\/examples-videos\/stream\/webserver-video-ext\">webserver-video-ext.ino<\/a> example was written for a Waveshare ESP32-P4-WIFI6 with its OV5647 MIPI-CSI camera. The objects:<\/p>\n<pre><code class=\"language-C++\">\/\/ web server: serves the player page and the MPEG transport stream\r\nWiFiServer wifi;\r\nHttpServer server(wifi);\r\nExtensionStream out_streams(&quot;\/stream.ts&quot;, tinyhttp::T_GET, &quot;video\/mp2t&quot;);\r\n\r\n\/\/ Muxer\r\nMuxerMTS mts(out_streams);\r\nVideoMuxerWithTasks video(mts);\r\nMutex mutex;  \/\/ shared by the tasks and the web server\r\n\r\n\/\/ video pipeline: camera -&gt; H.264 encoder -&gt; MuxerMTS\r\nCameraFrameSourceTinyCamera camera;\r\nH264EncoderESP32P4 encoder;\r\n\r\n\/\/ audio pipeline: microphone -&gt; AAC encoder -&gt; MuxerMTS\r\nI2SCodecStream mic(WaveshareESP32P4WIFI6);\r\nAACEncoderVO aac;\r\n<\/code><\/pre>\n<p>In <code>setup()<\/code> the microphone is started first (16 kHz mono), then the camera (320&#215;240 RGB565). <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_camera_frame_source_tiny_camera.html\">CameraFrameSourceTinyCamera<\/a> captures a first frame in <code>begin()<\/code> to determine the size and format, which configure the encoder:<\/p>\n<pre><code class=\"language-C++\">VideoInfo info = camera.videoInfo();\r\nencoder.setSize(info.width, info.height);\r\nencoder.setVideoFormat(VideoFormat::RGB565);\r\nencoder.setFrameRate(kFps);\r\nencoder.setGop(kFps);  \/\/ a keyframe every second\r\nmts.setRealTime(true);\r\n<\/code><\/pre>\n<p>Then the web server is set up and started, and finally the tasks:<\/p>\n<pre><code class=\"language-C++\">server.on(&quot;\/&quot;, tinyhttp::T_GET, &quot;text\/html&quot;, kPage);\r\nserver.addExtension(out_streams);\r\nserver.setNoConnectDelay(0);\r\nserver.begin(80, ssid, password);\r\n\r\nvideo.setVideoSource(camera);\r\nvideo.setVideoEncoder(encoder);\r\nvideo.setAudioSource(mic);\r\nvideo.setAudioEncoder(aac);\r\nvideo.setMutex(mutex);\r\nvideo.setTaskParameters(16 * 1024, 2);  \/\/ stack in bytes on ESP32\r\nvideo.begin();\r\n<\/code><\/pre>\n<p><code>loop()<\/code> only serves the HTTP requests:<\/p>\n<pre><code class=\"language-C++\">void loop() {\r\n  {\r\n    LockGuard guard(mutex);\r\n    server.copy();\r\n  }\r\n  delay(10);\r\n}\r\n<\/code><\/pre>\n<p>On this board the camera and the audio codec share one I2C bus: the audio driver starts it with <code>Wire<\/code>, and the camera uses the same bus via <code>config.sccb_i2c_bus = (i2c_master_bus_handle_t)i2cBusHandle(0);<\/code> instead of creating a second I2C master on the same pins.<\/p>\n<p>The board needs <code>esp32:esp32:esp32p4:ChipVariant=postv3,PSRAM=enabled,PartitionScheme=huge_app<\/code>: it has a chip of revision 3, and the default &quot;Before v3.00&quot; build doesn&#39;t start.<\/p>\n<p>The simpler <a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-tools\/tree\/main\/examples\/examples-videos\/stream\/webserver-video\">webserver-video.ino<\/a> streams video only, to one client at a time, using <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_video_muxer.html\">VideoMuxer<\/a>, the software <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_h264_encoder.html\">H264Encoder<\/a> and AudioServer.<\/p>\n<h3 id=\"exchanging-the-components\">Exchanging the Components<\/h3>\n<p>Every step of the pipeline is a separate, exchangeable component with a common API: the encoders implement <code>AudioEncoder<\/code> or <code>VideoEncoder<\/code>, the containers implement <code>Muxer<\/code>, and the muxer writes to any Arduino <code>Print<\/code>. So you can replace each part of the example without touching the rest &#8211; usually by changing a single line.<\/p>\n<h4 id=\"audio-encoders\">Audio encoders<\/h4>\n<p>Any <code>AudioEncoder<\/code> can be passed to <code>video.setAudioEncoder()<\/code>. For a video container, it must write exactly one complete audio frame per <code>write()<\/code>, since the muxers calculate the audio timestamps from the number of frames.<\/p>\n<table>\n<thead>\n<tr>\n<th>Format<\/th>\n<th>Class<\/th>\n<th>Library<\/th>\n<th>Notes<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>AAC<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_a_a_c_encoder_v_o.html\">AACEncoderVO<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/codec-vo-aacenc\">codec-vo-aacenc<\/a><\/td>\n<td>used in the example<\/td>\n<\/tr>\n<tr>\n<td>AAC<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_a_a_c_encoder_f_d_k.html\">AACEncoderFDK<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/codec-fdk-aac\">codec-fdk-aac<\/a><\/td>\n<td>Fraunhofer FDK AAC<\/td>\n<\/tr>\n<tr>\n<td>AAC<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_a_a_c_encoder_f_a_a_c.html\">AACEncoderFAAC<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/codec-faac\">codec-faac<\/a><\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td>MP3<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_m_p3_encoder_shine.html\">MP3EncoderShine<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/codec-shine\">codec-shine<\/a><\/td>\n<td>fast fixed point encoder<\/td>\n<\/tr>\n<tr>\n<td>MP3<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_m_p3_encoder_l_a_m_e.html\">MP3EncoderLAME<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/codec-lame\">codec-lame<\/a><\/td>\n<td>needs <code>MP3EncoderLAME mp3(true)<\/code>: LAME delivers its output in arbitrary chunks, the flag splits it into complete frames<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>E.g. to send MP3 instead of AAC:<\/p>\n<pre><code class=\"language-C++\">#include &quot;AudioTools\/AudioCodecs\/CodecMP3Shine.h&quot;\r\nMP3EncoderShine mp3;\r\n...\r\nvideo.setAudioEncoder(mp3);\r\n<\/code><\/pre>\n<p><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_muxer_m_t_s.html\">MuxerMTS<\/a> expects AAC in ADTS frames (the default of all AAC encoders above), <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_muxer_m_p4.html\">MuxerMP4<\/a> raw AAC frames: use e.g. <code>aac.setAdts(false)<\/code>. Without an encoder (<a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_video_muxer_with_tasks.html\">VideoMuxerWithTasks<\/a> without <code>setAudioEncoder()<\/code>) the audio is written as uncompressed PCM, which AVI and MP4 support.<\/p>\n<h4 id=\"containers\">Containers<\/h4>\n<p>Replace <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_muxer_m_t_s.html\">MuxerMTS<\/a> by another <code>Muxer<\/code>:<\/p>\n<table>\n<thead>\n<tr>\n<th>Container<\/th>\n<th>Class<\/th>\n<th>Video<\/th>\n<th>Audio<\/th>\n<th>Plays in<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>MPEG-TS (<code>video\/mp2t<\/code>)<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_muxer_m_t_s.html\">MuxerMTS<\/a><\/td>\n<td>H.264, MPEG-1, (MJPEG)<\/td>\n<td>AAC, MP3<\/td>\n<td>VLC, ffplay, mpv, browser with mpegts.js<\/td>\n<\/tr>\n<tr>\n<td>Fragmented MP4 (<code>video\/mp4<\/code>)<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_muxer_m_p4.html\">MuxerMP4<\/a><\/td>\n<td>H.264, MJPEG, raw<\/td>\n<td>AAC (raw), PCM<\/td>\n<td>VLC, ffplay, mpv<\/td>\n<\/tr>\n<tr>\n<td>AVI (<code>video\/avi<\/code>)<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_muxer_a_v_i.html\">MuxerAVI<\/a><\/td>\n<td>MJPEG, H.264, raw<\/td>\n<td>PCM, MP3, AAC<\/td>\n<td>VLC, ffplay, mpv &#8211; not in browsers<\/td>\n<\/tr>\n<tr>\n<td>MPEG Program Stream (<code>video\/mpeg<\/code>)<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_muxer_m_p_g.html\">MuxerMPG<\/a><\/td>\n<td>MPEG-1<\/td>\n<td>MP2, MP3<\/td>\n<td>VLC, ffplay, mpv<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>All of them are streaming writers that never need to seek, so they can write to a network client. But only MPEG-TS repeats its stream description before every keyframe: the other ones write their header only once in <code>begin()<\/code>. So a viewer can&#39;t join a running MP4 or AVI stream &#8211; instead, start a new muxer session for each client, as the <a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-tools\/tree\/main\/examples\/examples-videos\/stream\/webserver-video\">webserver-video.ino<\/a> example does (<code>video.begin()<\/code> in the callback that serves a client). Use <code>mimeVideo()<\/code> of the muxer as content type of the reply.<\/p>\n<h4 id=\"video-encoders\">Video encoders<\/h4>\n<p>Any <code>VideoEncoder<\/code> can be passed to <code>video.setVideoEncoder()<\/code>:<\/p>\n<table>\n<thead>\n<tr>\n<th>Format<\/th>\n<th>Class<\/th>\n<th>Library<\/th>\n<th>Platform<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>H.264<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_h264_encoder_e_s_p32_p4.html\">H264EncoderESP32P4<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/codec-h264-ESP32P4\">codec-h264-ESP32P4<\/a><\/td>\n<td>ESP32-P4, hardware encoder (used in the example)<\/td>\n<\/tr>\n<tr>\n<td>H.264<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_h264_encoder_e_s_p32_s3.html\">H264EncoderESP32S3<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/codec-h264-ESP32S3\">codec-h264-ESP32S3<\/a><\/td>\n<td>ESP32-S3, Espressif&#39;s software encoder<\/td>\n<\/tr>\n<tr>\n<td>H.264<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_h264_encoder.html\">H264Encoder<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/TinyH264\">TinyH264<\/a><\/td>\n<td>any platform, software encoder<\/td>\n<\/tr>\n<tr>\n<td>MPEG-1<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_m_p_g_encoder.html\">MPGEncoder<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/TinyMPG\">TinyMPG<\/a><\/td>\n<td>any platform, for MuxerMPG or MuxerMTS<\/td>\n<\/tr>\n<tr>\n<td>MJPEG<\/td>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_m_j_p_e_g_encoder.html\">MJPEGEncoder<\/a><\/td>\n<td><a href=\"https:\/\/github.com\/pschatzmann\/TinyJPEG\">TinyJPEG<\/a><\/td>\n<td>any platform, software encoder &#8211; or no encoder at all, see below<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>E.g. the portable software encoder instead of the ESP32-P4 hardware:<\/p>\n<pre><code class=\"language-C++\">#include &quot;AudioTools\/Video\/CodecH264.h&quot;\r\nH264Encoder encoder;\r\n...\r\nencoder.setSize(info.width, info.height);\r\nencoder.setVideoFormat(VideoFormat::RGB565);\r\nencoder.setQp(28);\r\nencoder.setKeyframeInterval(kFps);\r\n<\/code><\/pre>\n<h4 id=\"mjpeg-directly-from-the-camera\">MJPEG directly from the camera<\/h4>\n<p>For MJPEG you don&#39;t need an encoder &#8211; and no special class either &#8211; if the camera delivers JPEG images itself: the sensor&#39;s own JPEG output, or on the ESP32-P4 its hardware JPEG encoder. Request JPEG from the camera and don&#39;t call <code>setVideoEncoder()<\/code>: <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_video_muxer.html\">VideoMuxer<\/a> and <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_video_muxer_with_tasks.html\">VideoMuxerWithTasks<\/a> then pass each image to the muxer as it is, as one frame. <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_camera_frame_source_tiny_camera.html\">CameraFrameSourceTinyCamera<\/a> reports these frames as <code>VideoFormat::MJPEG<\/code>, so the muxer is configured for MJPEG automatically:<\/p>\n<pre><code class=\"language-C++\">MuxerAVI avi(out_streams);  \/\/ or MuxerMP4\r\nVideoMuxerWithTasks video(avi);\r\n...\r\nconfig.pixel_format = PIXFORMAT_JPEG;\r\ncamera.begin(config, kFps);\r\nvideo.setVideoSource(camera);  \/\/ no setVideoEncoder()\r\n<\/code><\/pre>\n<p>Use <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_m_j_p_e_g_encoder.html\">MJPEGEncoder<\/a> instead if the camera only delivers raw pictures (e.g. RGB565). Either way the JPEG images can be put into an AVI or MP4 container: both have been verified to produce files that ffmpeg decodes without errors. MPEG-TS can carry them too, but since there is no standard stream type for MJPEG in a transport stream, generic players don&#39;t recognize it (ffmpeg e.g. sees only a data stream) and browsers can&#39;t play it &#8211; use AVI or MP4 for MJPEG. Keep in mind that a viewer can&#39;t join a running AVI or MP4 stream (see <a href=\"#containers\">Containers<\/a>).<\/p>\n<h4 id=\"web-servers\">Web servers<\/h4>\n<p>The muxer writes to any <code>Print<\/code>, so the server is exchangeable as well:<\/p>\n<ul>\n<li><strong>TinyHttp<\/strong> (<code>HttpServer<\/code> + <code>ExtensionStream<\/code>, used in the example): serves several pages and any number of viewers of the same stream.<\/li>\n<li><strong>AudioServer<\/strong> (<a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/group__http.html\">AudioServerWiFi<\/a>, <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/group__http.html\">AudioServerEthernet<\/a>): part of this library, serves one client at a time with a callback that receives the client&#39;s <code>Print<\/code> &#8211; see <a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-tools\/tree\/main\/examples\/examples-videos\/stream\/webserver-video\">webserver-video.ino<\/a>. With AudioServerEthernet the stream goes over Ethernet instead of WiFi.<\/li>\n<li><strong>Any other server<\/strong>: a plain <code>WiFiServer<\/code>\/<code>EthernetServer<\/code>, or any web server that gives you access to the client: send the HTTP reply header and pass the client to <code>mts.setOutput()<\/code>.<\/li>\n<li><strong>No HTTP at all<\/strong>: the <a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-tools\/wiki\/RTSP-Server\">RTSP Server<\/a> can stream H.264, MJPEG and MPEG-TS as well.<\/li>\n<\/ul>\n<h3 id=\"performance\">Performance<\/h3>\n<p>Measured on the Waveshare ESP32-P4-WIFI6 at 320&#215;240, streamed over WiFi:<\/p>\n<table>\n<thead>\n<tr>\n<th>Encoder<\/th>\n<th>Frames\/s<\/th>\n<th>Notes<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_h264_encoder.html\">H264Encoder<\/a> (TinyH264, software)<\/td>\n<td>6.6<\/td>\n<td>fixed QP 28, about 1.9 KB per frame<\/td>\n<\/tr>\n<tr>\n<td><a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_h264_encoder_e_s_p32_p4.html\">H264EncoderESP32P4<\/a> (hardware)<\/td>\n<td>8<\/td>\n<td>the configured 8 frames\/s; more are possible<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The hardware encoder is far faster than the software one, so with it the limits are the camera capture and scaling and the WiFi throughput. AAC at 16 kHz mono adds about 35 kbit\/s.<\/p>\n<h3 id=\"troubleshooting\">Troubleshooting<\/h3>\n<ul>\n<li><strong>The latency grows over time<\/strong>: the timestamps don&#39;t follow the real time &#8211; call <code>mts.setRealTime(true)<\/code> (see above).<\/li>\n<li><strong>Task watchdog<\/strong> (&quot;IDLE0 did not reset the watchdog&quot;): a task that never waits doesn&#39;t let the idle task run. <a href=\"https:\/\/pschatzmann.github.io\/arduino-audio-tools\/classaudio__tools_1_1_video_muxer_with_tasks.html\">VideoMuxerWithTasks<\/a> waits at least 1 ms per frame, even if encoding is slower than the frame rate.<\/li>\n<li><strong>Stack overflow in the video task<\/strong>: an H.264 encoder needs a lot of stack &#8211; set it with <code>setTaskParameters()<\/code> (on ESP32 the size is in bytes).<\/li>\n<li><strong>Errors at the start of playback<\/strong>: the frames before the first keyframe can&#39;t be decoded &#8211; see <a href=\"#keyframes\">Keyframes<\/a>.<\/li>\n<li><strong>No sound in the browser<\/strong>: the video starts muted &#8211; unmute it with the player controls.<\/li>\n<\/ul>\n<p><em>The latest, update version of this document can be found in the <a href=\"https:\/\/github.com\/pschatzmann\/arduino-audio-tools\/wiki\/Streaming-Video-and-Audio-via-a-Webserver\">Wiki of the AudioTools<\/a>.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you have ever streamed video from an ESP32, you have most likely used the CameraWebServer example: the camera delivers JPEG images, which the web server pushes one after the other to the browser. This works, but there is no sound, the bandwidth is high, and the result is not [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":7175,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_import_markdown_pro_load_document_selector":0,"_import_markdown_pro_submit_text_textarea":"","_exactmetrics_skip_tracking":false,"footnotes":""},"categories":[20],"tags":[67],"class_list":["post-7174","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-arduino","tag-video"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Arduino Audio Tools: Streaming Video and Audio via a Webserver - Phil Schatzmann<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Arduino Audio Tools: Streaming Video and Audio via a Webserver - Phil Schatzmann\" \/>\n<meta property=\"og:description\" content=\"If you have ever streamed video from an ESP32, you have most likely used the CameraWebServer example: the camera delivers JPEG images, which the web server pushes one after the other to the browser. This works, but there is no sound, the bandwidth is high, and the result is not [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/\" \/>\n<meta property=\"og:site_name\" content=\"Phil Schatzmann\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-22T08:18:17+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-22T08:48:25+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2026\/10\/Streaming-Media-Distribution-Icon.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1254\" \/>\n\t<meta property=\"og:image:height\" content=\"1254\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"pschatzmann\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"pschatzmann\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"10 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/\"},\"author\":{\"name\":\"pschatzmann\",\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/#\\\/schema\\\/person\\\/73a53638a4e34e8373405fd737dac9b1\"},\"headline\":\"Arduino Audio Tools: Streaming Video and Audio via a Webserver\",\"datePublished\":\"2026-10-22T08:18:17+00:00\",\"dateModified\":\"2026-10-22T08:48:25+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/\"},\"wordCount\":2069,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/#\\\/schema\\\/person\\\/73a53638a4e34e8373405fd737dac9b1\"},\"image\":{\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.pschatzmann.ch\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/Streaming-Media-Distribution-Icon.jpg\",\"keywords\":[\"Video\"],\"articleSection\":[\"Arduino\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/\",\"url\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/\",\"name\":\"Arduino Audio Tools: Streaming Video and Audio via a Webserver - Phil Schatzmann\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.pschatzmann.ch\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/Streaming-Media-Distribution-Icon.jpg\",\"datePublished\":\"2026-10-22T08:18:17+00:00\",\"dateModified\":\"2026-10-22T08:48:25+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/#primaryimage\",\"url\":\"https:\\\/\\\/www.pschatzmann.ch\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/Streaming-Media-Distribution-Icon.jpg\",\"contentUrl\":\"https:\\\/\\\/www.pschatzmann.ch\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/Streaming-Media-Distribution-Icon.jpg\",\"width\":1254,\"height\":1254},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/2026\\\/10\\\/22\\\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Arduino Audio Tools: Streaming Video and Audio via a Webserver\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/#website\",\"url\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/\",\"name\":\"Phil Schatzmann Consulting\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/#\\\/schema\\\/person\\\/73a53638a4e34e8373405fd737dac9b1\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/home\\\/#\\\/schema\\\/person\\\/73a53638a4e34e8373405fd737dac9b1\",\"name\":\"pschatzmann\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/wp-content\\\/uploads\\\/2022\\\/08\\\/pschatzmann.png\",\"url\":\"https:\\\/\\\/www.pschatzmann.ch\\\/wp-content\\\/uploads\\\/2022\\\/08\\\/pschatzmann.png\",\"contentUrl\":\"https:\\\/\\\/www.pschatzmann.ch\\\/wp-content\\\/uploads\\\/2022\\\/08\\\/pschatzmann.png\",\"width\":305,\"height\":305,\"caption\":\"pschatzmann\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.pschatzmann.ch\\\/wp-content\\\/uploads\\\/2022\\\/08\\\/pschatzmann.png\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Arduino Audio Tools: Streaming Video and Audio via a Webserver - Phil Schatzmann","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/","og_locale":"en_US","og_type":"article","og_title":"Arduino Audio Tools: Streaming Video and Audio via a Webserver - Phil Schatzmann","og_description":"If you have ever streamed video from an ESP32, you have most likely used the CameraWebServer example: the camera delivers JPEG images, which the web server pushes one after the other to the browser. This works, but there is no sound, the bandwidth is high, and the result is not [&hellip;]","og_url":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/","og_site_name":"Phil Schatzmann","article_published_time":"2026-10-22T08:18:17+00:00","article_modified_time":"2026-10-22T08:48:25+00:00","og_image":[{"width":1254,"height":1254,"url":"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2026\/10\/Streaming-Media-Distribution-Icon.jpg","type":"image\/jpeg"}],"author":"pschatzmann","twitter_card":"summary_large_image","twitter_misc":{"Written by":"pschatzmann","Est. reading time":"10 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/#article","isPartOf":{"@id":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/"},"author":{"name":"pschatzmann","@id":"https:\/\/www.pschatzmann.ch\/home\/#\/schema\/person\/73a53638a4e34e8373405fd737dac9b1"},"headline":"Arduino Audio Tools: Streaming Video and Audio via a Webserver","datePublished":"2026-10-22T08:18:17+00:00","dateModified":"2026-10-22T08:48:25+00:00","mainEntityOfPage":{"@id":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/"},"wordCount":2069,"commentCount":0,"publisher":{"@id":"https:\/\/www.pschatzmann.ch\/home\/#\/schema\/person\/73a53638a4e34e8373405fd737dac9b1"},"image":{"@id":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/#primaryimage"},"thumbnailUrl":"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2026\/10\/Streaming-Media-Distribution-Icon.jpg","keywords":["Video"],"articleSection":["Arduino"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/","url":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/","name":"Arduino Audio Tools: Streaming Video and Audio via a Webserver - Phil Schatzmann","isPartOf":{"@id":"https:\/\/www.pschatzmann.ch\/home\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/#primaryimage"},"image":{"@id":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/#primaryimage"},"thumbnailUrl":"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2026\/10\/Streaming-Media-Distribution-Icon.jpg","datePublished":"2026-10-22T08:18:17+00:00","dateModified":"2026-10-22T08:48:25+00:00","breadcrumb":{"@id":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/#primaryimage","url":"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2026\/10\/Streaming-Media-Distribution-Icon.jpg","contentUrl":"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2026\/10\/Streaming-Media-Distribution-Icon.jpg","width":1254,"height":1254},{"@type":"BreadcrumbList","@id":"https:\/\/www.pschatzmann.ch\/home\/2026\/10\/22\/arduino-audio-tools-streaming-video-and-audio-via-a-webserver\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.pschatzmann.ch\/home\/"},{"@type":"ListItem","position":2,"name":"Arduino Audio Tools: Streaming Video and Audio via a Webserver"}]},{"@type":"WebSite","@id":"https:\/\/www.pschatzmann.ch\/home\/#website","url":"https:\/\/www.pschatzmann.ch\/home\/","name":"Phil Schatzmann Consulting","description":"","publisher":{"@id":"https:\/\/www.pschatzmann.ch\/home\/#\/schema\/person\/73a53638a4e34e8373405fd737dac9b1"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.pschatzmann.ch\/home\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.pschatzmann.ch\/home\/#\/schema\/person\/73a53638a4e34e8373405fd737dac9b1","name":"pschatzmann","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2022\/08\/pschatzmann.png","url":"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2022\/08\/pschatzmann.png","contentUrl":"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2022\/08\/pschatzmann.png","width":305,"height":305,"caption":"pschatzmann"},"logo":{"@id":"https:\/\/www.pschatzmann.ch\/wp-content\/uploads\/2022\/08\/pschatzmann.png"}}]}},"post_mailing_queue_ids":[],"_links":{"self":[{"href":"https:\/\/www.pschatzmann.ch\/home\/wp-json\/wp\/v2\/posts\/7174","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.pschatzmann.ch\/home\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.pschatzmann.ch\/home\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.pschatzmann.ch\/home\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.pschatzmann.ch\/home\/wp-json\/wp\/v2\/comments?post=7174"}],"version-history":[{"count":10,"href":"https:\/\/www.pschatzmann.ch\/home\/wp-json\/wp\/v2\/posts\/7174\/revisions"}],"predecessor-version":[{"id":7186,"href":"https:\/\/www.pschatzmann.ch\/home\/wp-json\/wp\/v2\/posts\/7174\/revisions\/7186"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.pschatzmann.ch\/home\/wp-json\/wp\/v2\/media\/7175"}],"wp:attachment":[{"href":"https:\/\/www.pschatzmann.ch\/home\/wp-json\/wp\/v2\/media?parent=7174"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.pschatzmann.ch\/home\/wp-json\/wp\/v2\/categories?post=7174"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.pschatzmann.ch\/home\/wp-json\/wp\/v2\/tags?post=7174"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}