Skip to content

Commit

Permalink
Experimental support for AVIs on the SDCard
Browse files Browse the repository at this point in the history
  • Loading branch information
cgreening committed Sep 22, 2023
1 parent acd437e commit 42d20a2
Show file tree
Hide file tree
Showing 29 changed files with 816 additions and 92 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.DS_Store
.DS_Store
*.avi
Binary file added experiments/parse_avi
Binary file not shown.
112 changes: 112 additions & 0 deletions experiments/parse_avi.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
char chunkId[4];
unsigned int chunkSize;
} ChunkHeader;

void readChunk(FILE *file, ChunkHeader *header) {
fread(&header->chunkId, 4, 1, file);
fread(&header->chunkSize, 4, 1, file);
printf("ChunkId %c%c%c%c, size %u\n",
header->chunkId[0], header->chunkId[1],
header->chunkId[2], header->chunkId[3],
header->chunkSize);
}

void processMovieList(FILE *fp, unsigned int chunkSize) {
ChunkHeader header;
while (chunkSize > 0) {
readChunk(fp, &header);
if (strncmp(header.chunkId, "00dc", 4) == 0) {
printf("Found video frame.\n");
// skip the frame data bytes
fseek(fp, header.chunkSize, SEEK_CUR);
} else if (strncmp(header.chunkId, "01wb", 4) == 0) {
printf("Found audio data.\n");
// skip the audio data bytes
fseek(fp, header.chunkSize, SEEK_CUR);
} else {
// skip the chunk data bytes
fseek(fp, header.chunkSize, SEEK_CUR);
}
chunkSize -= 8 + header.chunkSize;
// handle any padding bytes
if (header.chunkSize % 2 != 0) {
fseek(fp, 1, SEEK_CUR);
chunkSize--;
}
}
}

void processListChunk(FILE *fp, unsigned int chunkSize) {
char listType[4];
fread(&listType, 4, 1, fp);
chunkSize -= 4;
printf("LIST type %c%c%c%c\n",
listType[0], listType[1],
listType[2], listType[3]);
// check for the movi list - contains the video frames and audio data
if (strncmp(listType, "movi", 4) == 0) {
printf("Found movi list.\n");
processMovieList(fp, chunkSize);
} else {
// skip the rest of the bytes
fseek(fp, chunkSize, SEEK_CUR);
}
}

int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <avi_file>\n", argv[0]);
return 1;
}

FILE *file = fopen(argv[1], "rb");
if (!file) {
printf("Failed to open file.\n");
return 1;
}

ChunkHeader header;

// Read RIFF header
readChunk(file, &header);
if (strncmp(header.chunkId, "RIFF", 4) != 0) {
printf("Not a valid AVI file.\n");
return 1;
} else {
printf("RIFF header found.\n");
}


// next four bytes are the RIFF type which should be 'AVI '
char riffType[4];
fread(&riffType, 4, 1, file);
if (strncmp(riffType, "AVI ", 4) != 0) {
printf("Not a valid AVI file.\n");
return 1;
} else {
printf("RIFF Type is AVI.\n");
}

// now read each chunk until the end of the file

while (!feof(file) && !ferror(file)) {
readChunk(file, &header);
if (feof(file) || ferror(file)) {
break;
}
// is it a LIST chunk?
if (strncmp(header.chunkId, "LIST", 4) == 0) {
processListChunk(file, header.chunkSize);
} else {
// skip the chunk data bytes
fseek(file, header.chunkSize, SEEK_CUR);
}
}
fclose(file);
return 0;
}
20 changes: 19 additions & 1 deletion player/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,22 @@ You'll also need some way of getting sound out. I recommend the MAX98357A breako

The code should work with pretty much any ESP32 board, but I've only tested it on a few.

There is currently a bug in the I2S code when using DAC for audio output - you'll need to use an older version of the Arduino ESP32 Core - check the Cheap Yellow Display settings in `platformio.ini` for the correct version.
## Exerpimental support for AVI files on an SDCard

This is a work in progress, but it should be able to stream AVI files from an SDCard - see the cheap yellow display setup in the ini file for the config.

To create a compatible AVI file, you can use ffmpeg:

```
ffmpeg -i input.mp4 -vf "scale=320:240" -r 15 -c:v mjpeg -q:v 10 -acodec pcm_u8 -ar 16000 -ac 1 output.avi
```

* -i input.mp4: Specifies the input file named input.mp4.
* -vf "scale=320:240": Sets the video filter to scale the video to 320x240 resolution - this matches the CYD
* -r 15: Sets the frame rate to 15fps.
* -c:v mjpeg: Sets the video codec to MJPEG.
* -q:v 10: Sets the video quality (lower values mean higher quality; you can adjust this as needed) - range 2-31
* -acodec pcm_u8: Sets the audio codec to 8-bit PCM - NOTE - AVI files cannot contain pcm_s8 audio, so there's some extra processing in the audio pipeline to handle this.
* -ar 16000: Sets the audio sample rate to 16KHz.
* -ac 1: Sets the audio to mono (single channel).
* output.avi: Specifies the output file named output.avi.
11 changes: 8 additions & 3 deletions player/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ platform = espressif32
board = esp-wrover-kit
framework = arduino
platform_packages =
platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.5
platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.13
lib_deps =
SPI
bodmer/TFT_eSPI
Expand Down Expand Up @@ -92,17 +92,22 @@ build_flags =
-DSPI_TOUCH_FREQUENCY=2500000
; audio settings - cheap yellow display uses the DAC
-DUSE_DAC_AUDIO
; SD card
-DUSE_SDCARD
-DSD_CARD_MISO=GPIO_NUM_19
-DSD_CARD_MOSI=GPIO_NUM_23
-DSD_CARD_CLK=GPIO_NUM_18
-DSD_CARD_CS=GPIO_NUM_5
; decode exceptions
monitor_filters = esp32_exception_decoder
monitor_speed = 115200


[env:touch-down]
platform = espressif32
board = esp-wrover-kit
framework = arduino
platform_packages =
platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.5
platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.13
lib_deps =
SPI
bodmer/TFT_eSPI
Expand Down
189 changes: 189 additions & 0 deletions player/src/AVIParser/AVIParser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#include <Arduino.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "AVIParser.h"

typedef struct
{
char chunkId[4];
unsigned int chunkSize;
} ChunkHeader;

void readChunk(FILE *file, ChunkHeader *header)
{
fread(&header->chunkId, 4, 1, file);
fread(&header->chunkSize, 4, 1, file);
// Serial.printf("ChunkId %c%c%c%c, size %u\n",
// header->chunkId[0], header->chunkId[1],
// header->chunkId[2], header->chunkId[3],
// header->chunkSize);
}

AVIParser::AVIParser(std::string fname, AVIChunkType requiredChunkType): mFileName(fname), mRequiredChunkType(requiredChunkType)
{
}

AVIParser::~AVIParser()
{
if (mFile)
{
fclose(mFile);
}
}

bool AVIParser::isMoviListChunk(unsigned int chunkSize)
{
char listType[4];
fread(&listType, 4, 1, mFile);
chunkSize -= 4;
Serial.printf("LIST type %c%c%c%c\n",
listType[0], listType[1],
listType[2], listType[3]);
// check for the movi list - contains the video frames and audio data
if (strncmp(listType, "movi", 4) == 0)
{
Serial.printf("Found movi list.\n");
Serial.printf("List Chunk Length: %d\n", chunkSize);
mMoviListPosition = ftell(mFile);
mMoviListLength = chunkSize;
return true;
}
else
{
// skip the rest of the bytes
fseek(mFile, chunkSize, SEEK_CUR);
}
return false;
}

bool AVIParser::open()
{
mFile = fopen(mFileName.c_str(), "rb");
if (!mFile)
{
Serial.printf("Failed to open file.\n");
return false;
}
// check the file is valid
ChunkHeader header;
// Read RIFF header
readChunk(mFile, &header);
if (strncmp(header.chunkId, "RIFF", 4) != 0)
{
Serial.println("Not a valid AVI file.");
fclose(mFile);
mFile = NULL;
return false;
}
else
{
Serial.printf("RIFF header found.\n");
}
// next four bytes are the RIFF type which should be 'AVI '
char riffType[4];
fread(&riffType, 4, 1, mFile);
if (strncmp(riffType, "AVI ", 4) != 0)
{
Serial.println("Not a valid AVI file.");
fclose(mFile);
mFile = NULL;
return false;
}
else
{
Serial.println("RIFF Type is AVI.");
}

// now read each chunk and find the movi list
while (!feof(mFile) && !ferror(mFile))
{
readChunk(mFile, &header);
if (feof(mFile) || ferror(mFile))
{
break;
}
// is it a LIST chunk?
if (strncmp(header.chunkId, "LIST", 4) == 0)
{
if (isMoviListChunk(header.chunkSize))
{
break;
}
}
else
{
// skip the chunk data bytes
fseek(mFile, header.chunkSize, SEEK_CUR);
}
}
// did we find the list?
if (mMoviListPosition == 0)
{
Serial.printf("Failed to find the movi list.\n");
fclose(mFile);
mFile = NULL;
return false;
}
// keep the file open for reading the frames
return true;
}

size_t AVIParser::getNextChunk(uint8_t **buffer, size_t &bufferLength)
{
// check if the file is open
if (!mFile)
{
Serial.println("No file open.");
return 0;
}
// did we find the movi list?
if (mMoviListPosition == 0) {
Serial.println("No movi list found.");
return 0;
}
// get the next chunk of data from the list
ChunkHeader header;
while (mMoviListLength > 0)
{
readChunk(mFile, &header);
mMoviListLength -= 8;
bool isVideoChunk = strncmp(header.chunkId, "00dc", 4) == 0;
bool isAudioChunk = strncmp(header.chunkId, "01wb", 4) == 0;
if (mRequiredChunkType == AVIChunkType::VIDEO && isVideoChunk ||
mRequiredChunkType == AVIChunkType::AUDIO && isAudioChunk)
{
// we've got the required chunk - copy it into the provided buffer
// reallocate the buffer if necessary
if (header.chunkSize > bufferLength)
{
*buffer = (uint8_t *)realloc(*buffer, header.chunkSize);
}
// copy the chunk data
fread(*buffer, header.chunkSize, 1, mFile);
mMoviListLength -= header.chunkSize;
// handle any padding bytes
if (header.chunkSize % 2 != 0)
{
fseek(mFile, 1, SEEK_CUR);
mMoviListLength--;
}
return header.chunkSize;
}
else
{
// the data is not what was required - skip over the chunk
fseek(mFile, header.chunkSize, SEEK_CUR);
mMoviListLength -= header.chunkSize;
}
// handle any padding bytes
if (header.chunkSize % 2 != 0)
{
fseek(mFile, 1, SEEK_CUR);
mMoviListLength--;
}
}
// no more chunks
Serial.println("No more data");
return 0;
}
24 changes: 24 additions & 0 deletions player/src/AVIParser/AVIParser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

enum class AVIChunkType
{
VIDEO, AUDIO
};

class AVIParser
{
private:
std::string mFileName;
AVIChunkType mRequiredChunkType;
FILE *mFile = NULL;
long mMoviListPosition = 0;
long mMoviListLength;

bool isMoviListChunk(unsigned int chunkSize);

public:
AVIParser(std::string fname, AVIChunkType requiredChunkType);
~AVIParser();
bool open();
size_t getNextChunk(uint8_t **buffer, size_t &bufferLength);
};
Loading

0 comments on commit 42d20a2

Please sign in to comment.