157 lines
6.1 KiB
C++
157 lines
6.1 KiB
C++
/**
|
|
* @file sd-avi-audio-video.ino
|
|
* @brief Plays a local .avi file's audio + H.264 video tracks from the
|
|
* microSD card of the Hosyond 2.8" ESP32-S3 Display (see that board's
|
|
* audio-out/lcd-test/player-sdmmc examples): DemuxerAVI demuxes, H264Decoder
|
|
* (TinyH264) decodes video to the ILI9341 panel, mp3Decoder/aacDecoder/
|
|
* wavDecoder auto-select and decode the audio (PCM/AAC/MP3 all valid in
|
|
* AVI) to the ES8311/FM8002E speaker path via AudioBoardStream.
|
|
*
|
|
* Driven through AudioTools/Video/VideoPlayer.h instead of wiring
|
|
* CodecCopy, PacedVideoOutput, EncodedAudioStream and AudioTimeSourceStream
|
|
* together by hand - see VideoPlayer's own class comment for the pipeline
|
|
* it replaces (identical end to end; VideoPlayer just owns the wiring, and
|
|
* its copy() already keeps the video schedule's fps in sync with the
|
|
* demuxer's own parsed rate, so no manual polling is needed in loop()
|
|
* either). mp3/aac/wav are registered explicitly below since VideoPlayer's
|
|
* built-in audio multi-decoder starts empty (see its own class comment) -
|
|
* together they match what DecoderHelix bundles by default.
|
|
*
|
|
* Notes:
|
|
* - Video track must be H264/h264/X264/x264/avc1/AVC1 Annex-B - see
|
|
* sd-avi-video.ino for building a compatible file with ffmpeg.
|
|
* - The board's WS2812 RGB LED (IO42, see led-test) is set to a distinct
|
|
* color right before each setup stage starts, so if a stage hangs (never
|
|
* returns from begin()) rather than cleanly failing, the color still on
|
|
* the LED tells you which one - no serial capture needed.
|
|
* I transcoded the video with ffmpeg -i Casablanca.1942.720.x264.YIFY.mkv -vf
|
|
* "scale=176:144,fps=7" -pix_fmt yuv420p -c:v libx264 -profile:v baseline
|
|
* -level 3.0 -g 15 -crf 28 output176x144-v3.avi
|
|
*
|
|
* 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/adafruit/Adafruit_NeoPixel
|
|
*
|
|
* @author Phil Schatzmann
|
|
* @copyright GPLv3
|
|
*/
|
|
#include "AudioTools.h"
|
|
#include "AudioTools/AudioCodecs/CodecAACHelix.h"
|
|
#include "AudioTools/AudioCodecs/CodecMP3Helix.h"
|
|
#include "AudioTools/AudioCodecs/CodecWAV.h"
|
|
#include "AudioTools/AudioCodecs/ContainerAVI.h"
|
|
#include "AudioTools/AudioLibs/AudioBoardStream.h"
|
|
#include "AudioTools/Video/CodecH264.h"
|
|
// #include "AudioTools/Video/CodecH264ESP32S3.h" // alternative codec
|
|
#include "AudioTools/Video/OutputTinyGPU.h"
|
|
#include "AudioTools/Video/VideoPlayer.h"
|
|
#include "TinyGPU/Boards.h" // for LCDBoardESP32S3_2_8Display
|
|
#include <SD_MMC.h>
|
|
|
|
// ---- File on the SD card to play ----
|
|
const char* file_path = "/Videos/output176x144.avi";
|
|
|
|
DemuxerAVI aviDemuxer;
|
|
LCDBoardESP32S3_2_8Display board;
|
|
OutputTinyGPU tftOutput(board);
|
|
AudioBoardStream out(audio_driver::ESP32S3HosyondDisplay);
|
|
VideoPlayer player(aviDemuxer, tftOutput, out);
|
|
|
|
H264Decoder h264Decoder;
|
|
MP3DecoderHelix mp3Decoder;
|
|
AACDecoderHelix aacDecoder;
|
|
WAVDecoder wavDecoder;
|
|
|
|
File 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()
|
|
cfg.buffer_size = 1024;
|
|
cfg.buffer_count = 20; // 1024*20 = 20KB output buffer
|
|
if (!out.begin(cfg)) {
|
|
Serial.println("AudioBoardStream begin() failed");
|
|
stop();
|
|
}
|
|
out.setVolume(0.4f);
|
|
|
|
file = SD_MMC.open(file_path);
|
|
if (!file) {
|
|
Serial.print("Could not open ");
|
|
Serial.println(file_path);
|
|
stop();
|
|
}
|
|
Serial.print("opened ");
|
|
Serial.print(file_path);
|
|
Serial.print(": ");
|
|
Serial.print((unsigned)file.size());
|
|
Serial.println(" bytes");
|
|
|
|
if (!board.begin()) {
|
|
Serial.println("OutputTinyGPU begin() failed");
|
|
stop();
|
|
}
|
|
|
|
tftOutput.setRotation(DisplayRotation::kLandscape);
|
|
// On: upscale the decoded 176x144 frame to fill the 320x240 panel -
|
|
// costs more render time (3x the pixels vs. native size: 76800 vs
|
|
// 25344) than leaving this off. See PacedVideoOutput's avgFrameMs()/
|
|
// setScaleSingleBuffer() if render time needs to come back down.
|
|
tftOutput.setScaleToFit(true);
|
|
|
|
player.addVideoDecoder(h264Decoder);
|
|
player.addAudioDecoder(mp3Decoder, "audio/mpeg");
|
|
player.addAudioDecoder(aacDecoder, "audio/aac");
|
|
player.addAudioDecoder(wavDecoder, "audio/vnd.wave");
|
|
|
|
// Compensates for AudioBoardStream's own output buffering
|
|
// (cfg.buffer_size*cfg.buffer_count) - see
|
|
// PacedVideoOutput::setSchedulingDelayMs(); ~115ms matches the ~20KB
|
|
// output buffer above - tune if you change either.
|
|
player.setSchedulingDelayMs(115);
|
|
// Pin the render task to core 0 - loop() (SD reads + demuxing + the
|
|
// blocking out.write() into I2S) runs on core 1 by default. Without
|
|
// this, the video task could land on core 1 too and preempt loop() for
|
|
// the length of a slow render call. Call before begin()/first write().
|
|
player.setTaskParameters(4096, 2, 0);
|
|
// 40KB: this video's frames can individually run into several KB (a
|
|
// low-fps, bitrate-preserving transcode packs more data into each
|
|
// remaining frame) - increase further if drops/resyncs are still too
|
|
// frequent.
|
|
player.setQueueBytes(40 * 1024);
|
|
// Lowered from the 1.0 default: I-frame decode cost on this video is
|
|
// high enough that the per-GOP backlog compounds over time. Can't
|
|
// reduce I-frame decode cost itself (I-frames are never dropped this
|
|
// way), but sheds P-frames sooner in each cycle to keep the queue's
|
|
// average fill lower.
|
|
player.setCatchUpThresholdFrames(0.5f);
|
|
// Raised from the default 3: this video's I:P ratio and I-frame decode
|
|
// cost meant the queue could fill with 3 I-frames before the next
|
|
// P-frame ever rendered - 4 gives more room to absorb I-frame spikes
|
|
// without dropping P-frames unnecessarily.
|
|
player.setMaxQueuedIFrames(4);
|
|
|
|
if (!player.begin(file)) {
|
|
Serial.println("VideoPlayer begin() failed");
|
|
stop();
|
|
}
|
|
}
|
|
|
|
void loop() {
|
|
static uint32_t diagLast = 0;
|
|
if (millis() - diagLast > 1000) {
|
|
player.logTo(Serial);
|
|
diagLast = millis();
|
|
}
|
|
|
|
if (player.copy() == 0) {
|
|
Serial.println("Done");
|
|
file.close();
|
|
stop();
|
|
}
|
|
}
|