BirdNet-goMikrofon/Arduino-audio-tools-main/arduino-audio-tools-main/examples/examples-videos/measure/play-measure-fps/play-sd-measure-fps/sd-measure-fps.ino
2026-09-26 01:20:55 +02:00

159 lines
6.3 KiB
C++

/**
* @file sd-measure-fps.ino
* @brief Benchmarks real, achievable video playback throughput for a
* local AVI/MP4/MPG file on the Hosyond 2.8" ESP32-S3 Display (see that
* board's audio-out/lcd-test/player-sdmmc examples), using
* LCDBoardESP32S3_2_8Display/OutputTinyGPU as the default display output:
* no audio decode, no PacedVideoOutput pacing/audio clock - just demux ->
* decode -> display, back to back, as fast as CodecCopy can feed bytes
* in. Reports the average fps actually achieved (frames decoded / wall-
* clock time spent on them), plus a breakdown by frame type (I vs P).
*
* The number this answers is "how fast can this board actually play this
* file if nothing were pacing it" - exactly the ceiling PacedVideoOutput
* itself needs to stay under (see its own inputFPS()/outputFPS()/
* avgFrameMs() diagnostics, and the "Audio/Video Synchronization" wiki
* chapter) to avoid ever falling behind and dropping/resyncing during
* real playback with audio. Run this first against your own file/board
* to know what frame rate is actually achievable before picking a
* PacedVideoOutput::setSchedulingDelayMs()/setQueueBytes()/... tuning for
* one of the sd-*-video.ino playback examples.
*
* Generic across every container/codec this library ships a decoder for:
* MultiVideoDemuxerFull auto-detects AVI/MP4/MPG from the file's own
* signature, MultiVideoDecoderFull auto-detects H264/MJPEG/MPEG-1 from
* the container's parsed format (falling back to content-sniffing if the
* container doesn't report one) - point file_path at any file those
* cover and this just works, no per-format sketch needed.
*
* OutputFPSMeter (AudioTools/Video/OutputFPSMeter.h) wraps the decoder
* (not the final pixel target): it times each write() call and
* classifies it I vs P via the decoder's own isKeyFrame(), the same way
* PacedVideoOutput classifies frames for its own frameCountI()/
* frameCountP() stats - see its own class comment for the full picture.
* tftOutput (a real display) is the decoder's own output below, so - as
* that class comment explains - every decoder pushes each picture to it
* synchronously and OutputTinyGPU's own SPI/QSPI write blocks until the
* transfer actually completes, meaning the reported numbers already
* include real display cost, not just decode. Swap tftOutput for
* NullVideoOutput (commented out below) instead if you want to isolate
* pure codec throughput from display cost.
*
* Dependencies (install via Library Manager):
* - https://github.com/pschatzmann/arduino-audio-driver
* - https://github.com/pschatzmann/TinyGPU
* - https://github.com/pschatzmann/TinyH264
* - https://github.com/pschatzmann/TinyMPG
* - https://github.com/pschatzmann/TinyJPEG
*
* @author Phil Schatzmann
* @copyright GPLv3
*/
#include <SD_MMC.h>
#include "AudioTools.h"
#include "AudioTools/AudioLibs/AudioBoardStream.h"
#include "AudioTools/Video/CodecH264.h"
#include "AudioTools/Video/CodecJPEG.h"
#include "AudioTools/Video/CodecMPG.h"
#include "AudioTools/Video/MultiVideoDecoderFull.h"
#include "AudioTools/Video/MultiVideoDemuxerFull.h"
#include "AudioTools/Video/OutputFPSMeter.h"
#include "AudioTools/Video/OutputTinyGPU.h"
#include "TinyGPU/Boards.h"
// ---- File on the SD card to measure - any .avi/.mp4/.mpg this library
// can decode ----
const char* file_path = "/Videos/output176x144.avi";
//const char* file_path = "/Videos/output176x144.mp4"; // x264
//const char* file_path = "/Videos/output176x144-mjpeg.avi"; // mjpeg
//const char* file_path = "/Videos/output176x144.mpg";
// Uncomment to isolate pure codec throughput from display cost (see the
// file comment above) - swap tftOutput for this in videoDecoder.setOutput()
// below if you use it.
// class NullVideoOutput : public VideoOutput {
// public:
// size_t write(const uint8_t *data, size_t len) override { return len; }
// };
// NullVideoOutput nullOutput;
LCDBoardESP32S3_2_8Display board;
OutputTinyGPU tftOutput(board); // default output target - see file comment
// Only used to bring up the shared SD_MMC bus (cfg.sdmmc_active below) -
// this sketch decodes no audio, so the codec chip itself stays idle.
AudioBoardStream out(audio_driver::ESP32S3HosyondDisplay);
MultiVideoDecoderFull videoDecoder;
OutputFPSMeter meter(videoDecoder);
MultiVideoDemuxerFull demuxer; // auto-detects AVI/MP4/MPG
File file;
CodecCopy copier(demuxer, file);
void setup() {
Serial.begin(115200);
AudioToolsLogger.begin(Serial, AudioToolsLogLevel::Info);
auto cfg = out.defaultConfig(TX_MODE);
cfg.sdmmc_active = true; // board's begin() calls SD_MMC.setPins()+begin()
if (!out.begin(cfg)) {
Serial.println("AudioBoardStream begin() failed");
stop();
}
file = SD_MMC.open(file_path);
if (!file) {
Serial.print("Could not open ");
Serial.println(file_path);
stop();
}
if (!board.begin()) {
Serial.println("OutputTinyGPU begin() failed");
stop();
}
tftOutput.setRotation(DisplayRotation::kLandscape);
tftOutput.setScaleToFit(true);
tftOutput.setVideoInfoSource(demuxer);
videoDecoder.setOutput(tftOutput);
// Exact codec detection from the container's own parsed format, same
// pattern as every sd-*-video.ino example - falls back to content-
// sniffing (VideoDecoder::isValid()) for any container that doesn't
// report one.
videoDecoder.setVideoInfoSource(demuxer);
if (!videoDecoder.begin()) {
Serial.println("MultiVideoDecoderFull begin() failed");
stop();
}
// No setOutputAudio() call - any audio track is still parsed by the
// demuxer (chunk headers etc.), its payload bytes just go nowhere.
// This sketch measures video decode(+display) throughput only.
demuxer.setOutputVideo(meter);
if (!demuxer.begin()) {
Serial.println("MultiVideoDemuxerFull begin() failed");
stop();
}
Serial.println("Decoding at full speed (no pacing, no audio) ...");
}
void loop() {
static uint32_t diagLast = 0;
if (file && millis() - diagLast > 1000) {
meter.logTo(Serial);
diagLast = millis();
}
// file is only ever truthy up to the point it's closed below - once
// that happens this whole block is skipped on every subsequent loop()
// call, so "Done"/the final report print exactly once.
if (file && !copier.copy()) {
Serial.println("Done");
file.close();
Serial.println("---- final result ----");
meter.logTo(Serial);
}
}