Mini Lego TV

This commit is contained in:
Eric
2022-09-13 14:17:10 -07:00
parent ba4e460a28
commit 50ab079cdb
4 changed files with 735 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
/////////////////////////////////////////////////////////////////
/*
Mini Lego TV based on ESP32
For More Information: https://youtu.be/2TOVohmUqOE
Created by Eric N. (ThatProject)
*/
/////////////////////////////////////////////////////////////////
// This project is based on a Mini Retro TV.
// https://www.instructables.com/Mini-Retro-TV/
/////////////////////////////////////////////////////////////////
/***
* Required libraries:
* https://github.com/moononournation/Arduino_GFX.git
* https://github.com/pschatzmann/arduino-libhelix.git
* https://github.com/bitbank2/JPEGDEC.git
*/
/***
* Added libraries:
* https://github.com/vshymanskyy/Preferences
* https://github.com/fbiego/CST816S
*/
#define FPS 30
#define MJPEG_BUFFER_SIZE (288 * 240 * 2 / 8)
#define AUDIOASSIGNCORE 1
#define DECODEASSIGNCORE 0
#define DRAWASSIGNCORE 1
#include "config.h"
#include <WiFi.h>
#include <FS.h>
#include <SD.h>
#include <Preferences.h>
Preferences preferences;
#define APP_NAME "video_player"
#define K_VIDEO_INDEX "video_index"
#define BASE_PATH "/Videos/"
#define AAC_FILENAME "/44100.aac"
#define MJPEG_FILENAME "/288_30fps.mjpeg"
#define VIDEO_COUNT 20
/* Arduino_GFX */
#include <Arduino_GFX_Library.h>
Arduino_DataBus *bus = new Arduino_ESP32SPI(DC, CS, SCK, MOSI, MISO, VSPI);
Arduino_GFX *gfx = new Arduino_ST7789(bus, RST, 1, true, ST7789_TFTWIDTH, ST7789_TFTHEIGHT, 0, 20, 0, 20);
/* Audio */
#include "esp32_audio_task.h"
/* MJPEG Video */
#include "mjpeg_decode_draw_task.h"
/* Touch */
#include <CST816S.h>
CST816S touch(TOUCH_SDA, TOUCH_SCL, TOUCH_RST, TOUCH_IRQ);
/* Variables */
static int next_frame = 0;
static int skipped_frames = 0;
static unsigned long start_ms, curr_ms, next_frame_ms;
static unsigned int video_idx = 1;
// pixel drawing callback
static int drawMCU(JPEGDRAW *pDraw) {
unsigned long s = millis();
gfx->draw16bitRGBBitmap(pDraw->x, pDraw->y, pDraw->pPixels, pDraw->iWidth, pDraw->iHeight);
total_show_video_ms += millis() - s;
return 1;
} /* drawMCU() */
void setup() {
disableCore0WDT();
WiFi.mode(WIFI_OFF);
Serial.begin(115200);
// Init Display
gfx->begin(80000000);
gfx->fillScreen(BLACK);
pinMode(BLK, OUTPUT);
digitalWrite(BLK, HIGH);
xTaskCreate(
touchTask,
"touchTask",
2000,
NULL,
1,
NULL);
Serial.println("Init I2S");
esp_err_t ret_val = i2s_init(I2S_NUM_0, 44100, I2S_MCLK, I2S_SCLK, I2S_LRCLK, I2S_DOUT, I2S_DIN);
if (ret_val != ESP_OK) {
Serial.printf("i2s_init failed: %d\n", ret_val);
gfx->println("i2s_init failed");
return;
}
i2s_zero_dma_buffer(I2S_NUM_0);
Serial.println("Init FS");
SPIClass spi = SPIClass(HSPI);
spi.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS, spi, 80000000)) {
Serial.println("ERROR: File system mount failed!");
gfx->println("ERROR: File system mount failed!");
return;
}
preferences.begin(APP_NAME, false);
video_idx = preferences.getUInt(K_VIDEO_INDEX, 1);
Serial.printf("videoIndex: %d\n", video_idx);
gfx->setCursor(20, 20);
gfx->setTextColor(GREEN);
gfx->setTextSize(6, 6, 0);
gfx->printf("CH %d", video_idx);
delay(1000);
playVideoWithAudio(video_idx);
}
void loop() {
}
void touchTask(void *parameter) {
touch.begin();
while (1) {
if (touch.available()) {
Serial.printf("x: %d, y: %d\n", touch.data.x, touch.data.y);
switch (touch.data.gestureID) {
case SWIPE_LEFT:
Serial.println("SWIPE_LEFT");
break;
case SWIPE_RIGHT:
Serial.println("SWIPE_RIGHT");
break;
case SWIPE_UP:
Serial.println("SWIPE_UP (RIGHT)");
videoController(-1);
break;
case SWIPE_DOWN:
Serial.println("SWIPE_DOWN (LEFT)");
videoController(1);
break;
case SINGLE_CLICK:
Serial.println("SINGLE_CLICK");
break;
case DOUBLE_CLICK:
Serial.println("DOUBLE_CLICK");
break;
case LONG_PRESS:
Serial.println("LONG_PRESS");
break;
}
}
vTaskDelay(1000);
}
}
void playVideoWithAudio(int channel) {
char aFilePath[40];
sprintf(aFilePath, "%s%d%s", BASE_PATH, channel, AAC_FILENAME);
File aFile = SD.open(aFilePath);
if (!aFile || aFile.isDirectory()) {
Serial.printf("ERROR: Failed to open %s file for reading\n", aFilePath);
gfx->printf("ERROR: Failed to open %s file for reading\n", aFilePath);
return;
}
char vFilePath[40];
sprintf(vFilePath, "%s%d%s", BASE_PATH, channel, MJPEG_FILENAME);
File vFile = SD.open(vFilePath);
if (!vFile || vFile.isDirectory()) {
Serial.printf("ERROR: Failed to open %s file for reading\n", vFilePath);
gfx->printf("ERROR: Failed to open %s file for reading\n", vFilePath);
return;
}
Serial.println("Init video");
mjpeg_setup(&vFile, MJPEG_BUFFER_SIZE, drawMCU, false, DECODEASSIGNCORE, DRAWASSIGNCORE);
Serial.println("Start play audio task");
BaseType_t ret = aac_player_task_start(&aFile, AUDIOASSIGNCORE);
if (ret != pdPASS) {
Serial.printf("Audio player task start failed: %d\n", ret);
gfx->printf("Audio player task start failed: %d\n", ret);
}
Serial.println("Start play video");
start_ms = millis();
curr_ms = millis();
next_frame_ms = start_ms + (++next_frame * 1000 / FPS / 2);
while (vFile.available() && mjpeg_read_frame()) // Read video
{
total_read_video_ms += millis() - curr_ms;
curr_ms = millis();
if (millis() < next_frame_ms) // check show frame or skip frame
{
// Play video
mjpeg_draw_frame();
total_decode_video_ms += millis() - curr_ms;
curr_ms = millis();
} else {
++skipped_frames;
Serial.println("Skip frame");
}
while (millis() < next_frame_ms) {
vTaskDelay(pdMS_TO_TICKS(1));
}
curr_ms = millis();
next_frame_ms = start_ms + (++next_frame * 1000 / FPS);
}
int time_used = millis() - start_ms;
int total_frames = next_frame - 1;
Serial.println("AV end");
vFile.close();
aFile.close();
videoController(1);
}
void videoController(int next) {
video_idx += next;
if (video_idx <= 0) {
video_idx = VIDEO_COUNT;
} else if (video_idx > VIDEO_COUNT) {
video_idx = 1;
}
Serial.printf("video_idx : %d\n", video_idx);
preferences.putUInt(K_VIDEO_INDEX, video_idx);
preferences.end();
esp_restart();
}

View File

@@ -0,0 +1,23 @@
#define CS 5
#define SCK 18
#define MOSI 23
#define MISO -1
#define DC 27
#define RST 33
#define BLK 22
#define I2S_MCLK -1
#define I2S_SCLK 25
#define I2S_LRCLK 26
#define I2S_DOUT 32
#define I2S_DIN -1
#define SD_SCK 14
#define SD_MOSI 15
#define SD_MISO 4
#define SD_CS 13
#define TOUCH_SDA 17
#define TOUCH_SCL 16
#define TOUCH_RST 19
#define TOUCH_IRQ 39

View File

@@ -0,0 +1,163 @@
#include "driver/i2s.h"
#include "AACDecoderHelix.h"
#include "MP3DecoderHelix.h"
static unsigned long total_read_audio_ms = 0;
static unsigned long total_decode_audio_ms = 0;
static unsigned long total_play_audio_ms = 0;
static i2s_port_t _i2s_num;
static esp_err_t i2s_init(i2s_port_t i2s_num, uint32_t sample_rate,
int mck_io_num, /*!< MCK in out pin. Note that ESP32 supports setting MCK on GPIO0/GPIO1/GPIO3 only*/
int bck_io_num, /*!< BCK in out pin*/
int ws_io_num, /*!< WS in out pin*/
int data_out_num, /*!< DATA out pin*/
int data_in_num /*!< DATA in pin*/
)
{
_i2s_num = i2s_num;
esp_err_t ret_val = ESP_OK;
i2s_config_t i2s_config;
i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX);
i2s_config.sample_rate = sample_rate;
i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
i2s_config.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT;
i2s_config.communication_format = I2S_COMM_FORMAT_STAND_I2S;
i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1;
i2s_config.dma_buf_count = 8;
i2s_config.dma_buf_len = 160;
i2s_config.use_apll = false;
i2s_config.tx_desc_auto_clear = true;
i2s_config.fixed_mclk = 0;
i2s_config.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT;
i2s_config.bits_per_chan = I2S_BITS_PER_CHAN_16BIT;
i2s_pin_config_t pin_config;
pin_config.mck_io_num = mck_io_num;
pin_config.bck_io_num = bck_io_num;
pin_config.ws_io_num = ws_io_num;
pin_config.data_out_num = data_out_num;
pin_config.data_in_num = data_in_num;
ret_val |= i2s_driver_install(i2s_num, &i2s_config, 0, NULL);
ret_val |= i2s_set_pin(i2s_num, &pin_config);
return ret_val;
}
static int _samprate = 0;
static void aacAudioDataCallback(AACFrameInfo &info, int16_t *pwm_buffer, size_t len)
{
unsigned long s = millis();
if (_samprate != info.sampRateOut)
{
// log_i("bitRate: %d, nChans: %d, sampRateCore: %d, sampRateOut: %d, bitsPerSample: %d, outputSamps: %d, profile: %d, tnsUsed: %d, pnsUsed: %d",
// info.bitRate, info.nChans, info.sampRateCore, info.sampRateOut, info.bitsPerSample, info.outputSamps, info.profile, info.tnsUsed, info.pnsUsed);
i2s_set_clk(_i2s_num, info.sampRateOut /* sample_rate */, info.bitsPerSample /* bits_cfg */, (info.nChans == 2) ? I2S_CHANNEL_STEREO : I2S_CHANNEL_MONO /* channel */);
_samprate = info.sampRateOut;
}
size_t i2s_bytes_written = 0;
i2s_write(_i2s_num, pwm_buffer, len * 2, &i2s_bytes_written, portMAX_DELAY);
// log_i("len: %d, i2s_bytes_written: %d", len, i2s_bytes_written);
total_play_audio_ms += millis() - s;
}
static void mp3AudioDataCallback(MP3FrameInfo &info, int16_t *pwm_buffer, size_t len)
{
unsigned long s = millis();
if (_samprate != info.samprate)
{
log_i("bitrate: %d, nChans: %d, samprate: %d, bitsPerSample: %d, outputSamps: %d, layer: %d, version: %d",
info.bitrate, info.nChans, info.samprate, info.bitsPerSample, info.outputSamps, info.layer, info.version);
i2s_set_clk(_i2s_num, info.samprate /* sample_rate */, info.bitsPerSample /* bits_cfg */, (info.nChans == 2) ? I2S_CHANNEL_STEREO : I2S_CHANNEL_MONO /* channel */);
_samprate = info.samprate;
}
size_t i2s_bytes_written = 0;
i2s_write(_i2s_num, pwm_buffer, len * 2, &i2s_bytes_written, portMAX_DELAY);
// log_i("len: %d, i2s_bytes_written: %d", len, i2s_bytes_written);
total_play_audio_ms += millis() - s;
}
static uint8_t _frame[MP3_MAX_FRAME_SIZE]; // MP3_MAX_FRAME_SIZE is smaller, so always use MP3_MAX_FRAME_SIZE
static libhelix::AACDecoderHelix _aac(aacAudioDataCallback);
static void aac_player_task(void *pvParam)
{
Stream *input = (Stream *)pvParam;
int r, w;
unsigned long ms = millis();
while (r = input->readBytes(_frame, MP3_MAX_FRAME_SIZE))
{
total_read_audio_ms += millis() - ms;
ms = millis();
while (r > 0)
{
w = _aac.write(_frame, r);
// log_i("r: %d, w: %d\n", r, w);
r -= w;
}
total_decode_audio_ms += millis() - ms;
ms = millis();
}
log_i("AAC stop.");
vTaskDelete(NULL);
}
static libhelix::MP3DecoderHelix _mp3(mp3AudioDataCallback);
static void mp3_player_task(void *pvParam)
{
Stream *input = (Stream *)pvParam;
int r, w;
unsigned long ms = millis();
while (r = input->readBytes(_frame, MP3_MAX_FRAME_SIZE))
{
total_read_audio_ms += millis() - ms;
ms = millis();
while (r > 0)
{
w = _mp3.write(_frame, r);
// log_i("r: %d, w: %d\n", r, w);
r -= w;
}
total_decode_audio_ms += millis() - ms;
ms = millis();
}
log_i("MP3 stop.");
vTaskDelete(NULL);
}
static BaseType_t aac_player_task_start(Stream *input, BaseType_t audioAssignCore)
{
_aac.begin();
return xTaskCreatePinnedToCore(
(TaskFunction_t)aac_player_task,
(const char *const)"AAC Player Task",
(const uint32_t)2000,
(void *const)input,
(UBaseType_t)configMAX_PRIORITIES - 1,
(TaskHandle_t *const)NULL,
(const BaseType_t)audioAssignCore);
}
static BaseType_t mp3_player_task_start(Stream *input, BaseType_t audioAssignCore)
{
_mp3.begin();
return xTaskCreatePinnedToCore(
(TaskFunction_t)mp3_player_task,
(const char *const)"MP3 Player Task",
(const uint32_t)2000,
(void *const)input,
(UBaseType_t)configMAX_PRIORITIES - 1,
(TaskHandle_t *const)NULL,
(const BaseType_t)audioAssignCore);
}

View File

@@ -0,0 +1,300 @@
#define READ_BUFFER_SIZE 1024
// #define MAXOUTPUTSIZE (MAX_BUFFERED_PIXELS / 16 / 16)
#define MAXOUTPUTSIZE (288 / 3 / 16)
#define NUMBER_OF_DECODE_BUFFER 3
#define NUMBER_OF_DRAW_BUFFER 9
#include <FS.h>
#include <JPEGDEC.h>
typedef struct
{
int32_t size;
uint8_t *buf;
} mjpegBuf;
typedef struct
{
xQueueHandle xqh;
JPEG_DRAW_CALLBACK *drawFunc;
} paramDrawTask;
typedef struct
{
xQueueHandle xqh;
mjpegBuf *mBuf;
JPEG_DRAW_CALLBACK *drawFunc;
} paramDecodeTask;
static JPEGDRAW jpegdraws[NUMBER_OF_DRAW_BUFFER];
static int _draw_queue_cnt = 0;
static JPEGDEC _jpegDec;
static xQueueHandle _xqh;
static bool _useBigEndian;
static unsigned long total_read_video_ms = 0;
static unsigned long total_decode_video_ms = 0;
static unsigned long total_show_video_ms = 0;
Stream *_input;
int32_t _mjpegBufufSize;
uint8_t *_read_buf;
int32_t _mjpeg_buf_offset = 0;
TaskHandle_t _decodeTask;
TaskHandle_t _draw_task;
paramDecodeTask _pDecodeTask;
paramDrawTask _pDrawTask;
uint8_t *_mjpeg_buf;
uint8_t _mBufIdx = 0;
int32_t _inputindex = 0;
int32_t _buf_read;
int32_t _remain = 0;
mjpegBuf _mjpegBufs[NUMBER_OF_DECODE_BUFFER];
static int queueDrawMCU(JPEGDRAW *pDraw)
{
int len = pDraw->iWidth * pDraw->iHeight * 2;
JPEGDRAW *j = &jpegdraws[_draw_queue_cnt % NUMBER_OF_DRAW_BUFFER];
j->x = pDraw->x;
j->y = pDraw->y;
j->iWidth = pDraw->iWidth;
j->iHeight = pDraw->iHeight;
memcpy(j->pPixels, pDraw->pPixels, len);
// log_i("queueDrawMCU start.");
++_draw_queue_cnt;
xQueueSend(_xqh, &j, portMAX_DELAY);
// log_i("queueDrawMCU end.");
return 1;
}
static void decode_task(void *arg)
{
paramDecodeTask *p = (paramDecodeTask *)arg;
mjpegBuf *mBuf;
log_i("decode_task start.");
while (xQueueReceive(p->xqh, &mBuf, portMAX_DELAY))
{
// log_i("mBuf->size: %d", mBuf->size);
// log_i("mBuf->buf start: %X %X, end: %X, %X.", mBuf->buf[0], mBuf->buf[1], mBuf->buf[mBuf->size - 2], mBuf->buf[mBuf->size - 1]);
unsigned long s = millis();
_jpegDec.openRAM(mBuf->buf, mBuf->size, p->drawFunc);
// _jpegDec.setMaxOutputSize(MAXOUTPUTSIZE);
if (_useBigEndian)
{
_jpegDec.setPixelType(RGB565_BIG_ENDIAN);
}
_jpegDec.setMaxOutputSize(MAXOUTPUTSIZE);
_jpegDec.decode(0, 0, 0);
_jpegDec.close();
total_decode_video_ms += millis() - s;
}
vQueueDelete(p->xqh);
log_i("decode_task end.");
vTaskDelete(NULL);
}
static void draw_task(void *arg)
{
paramDrawTask *p = (paramDrawTask *)arg;
JPEGDRAW *pDraw;
log_i("draw_task start.");
while (xQueueReceive(p->xqh, &pDraw, portMAX_DELAY))
{
// log_i("draw_task work start: x: %d, y: %d, iWidth: %d, iHeight: %d.", pDraw->x, pDraw->y, pDraw->iWidth, pDraw->iHeight);
p->drawFunc(pDraw);
// log_i("draw_task work end.");
}
vQueueDelete(p->xqh);
log_i("draw_task end.");
vTaskDelete(NULL);
}
bool mjpeg_setup(Stream *input, int32_t mjpegBufufSize, JPEG_DRAW_CALLBACK *pfnDraw,
bool useBigEndian, BaseType_t decodeAssignCore, BaseType_t drawAssignCore)
{
_input = input;
_mjpegBufufSize = mjpegBufufSize;
_useBigEndian = useBigEndian;
for (int i = 0; i < NUMBER_OF_DECODE_BUFFER; ++i)
{
_mjpegBufs[i].buf = (uint8_t *)malloc(mjpegBufufSize);
if (_mjpegBufs[i].buf)
{
log_i("#%d decode buffer allocated.", i);
}
else
{
log_e("#%d decode buffer allocat failed.", i);
}
}
_mjpeg_buf = _mjpegBufs[_mBufIdx].buf;
if (!_read_buf)
{
_read_buf = (uint8_t *)malloc(READ_BUFFER_SIZE);
}
if (_read_buf)
{
log_i("Read buffer allocated.");
}
_xqh = xQueueCreate(NUMBER_OF_DRAW_BUFFER, sizeof(JPEGDRAW));
_pDrawTask.xqh = _xqh;
_pDrawTask.drawFunc = pfnDraw;
_pDecodeTask.xqh = xQueueCreate(NUMBER_OF_DECODE_BUFFER, sizeof(mjpegBuf));
_pDecodeTask.drawFunc = queueDrawMCU;
xTaskCreatePinnedToCore(
(TaskFunction_t)decode_task,
(const char *const)"MJPEG decode Task",
(const uint32_t)2000,
(void *const)&_pDecodeTask,
(UBaseType_t)configMAX_PRIORITIES - 1,
(TaskHandle_t *const)&_decodeTask,
(const BaseType_t)decodeAssignCore);
xTaskCreatePinnedToCore(
(TaskFunction_t)draw_task,
(const char *const)"MJPEG Draw Task",
(const uint32_t)2000,
(void *const)&_pDrawTask,
(UBaseType_t)configMAX_PRIORITIES - 1,
(TaskHandle_t *const)&_draw_task,
(const BaseType_t)drawAssignCore);
for (int i = 0; i < NUMBER_OF_DRAW_BUFFER; i++)
{
if (!jpegdraws[i].pPixels)
{
jpegdraws[i].pPixels = (uint16_t *)heap_caps_malloc(MAXOUTPUTSIZE * 16 * 16 * 2, MALLOC_CAP_DMA);
}
if (jpegdraws[i].pPixels)
{
log_i("#%d draw buffer allocated.", i);
}
else
{
log_e("#%d draw buffer allocat failed.", i);
}
}
return true;
}
bool mjpeg_read_frame()
{
if (_inputindex == 0)
{
_buf_read = _input->readBytes(_read_buf, READ_BUFFER_SIZE);
_inputindex += _buf_read;
}
_mjpeg_buf_offset = 0;
int i = 0;
bool found_FFD8 = false;
while ((_buf_read > 0) && (!found_FFD8))
{
i = 0;
while ((i < _buf_read) && (!found_FFD8))
{
if ((_read_buf[i] == 0xFF) && (_read_buf[i + 1] == 0xD8)) // JPEG header
{
// log_i("Found FFD8 at: %d.", i);
found_FFD8 = true;
}
++i;
}
if (found_FFD8)
{
--i;
}
else
{
_buf_read = _input->readBytes(_read_buf, READ_BUFFER_SIZE);
}
}
uint8_t *_p = _read_buf + i;
_buf_read -= i;
bool found_FFD9 = false;
if (_buf_read > 0)
{
i = 3;
while ((_buf_read > 0) && (!found_FFD9))
{
if ((_mjpeg_buf_offset > 0) && (_mjpeg_buf[_mjpeg_buf_offset - 1] == 0xFF) && (_p[0] == 0xD9)) // JPEG trailer
{
found_FFD9 = true;
}
else
{
while ((i < _buf_read) && (!found_FFD9))
{
if ((_p[i] == 0xFF) && (_p[i + 1] == 0xD9)) // JPEG trailer
{
found_FFD9 = true;
++i;
}
++i;
}
}
// log_i("i: %d", i);
memcpy(_mjpeg_buf + _mjpeg_buf_offset, _p, i);
_mjpeg_buf_offset += i;
int32_t o = _buf_read - i;
if (o > 0)
{
// log_i("o: %d", o);
memcpy(_read_buf, _p + i, o);
_buf_read = _input->readBytes(_read_buf + o, READ_BUFFER_SIZE - o);
_p = _read_buf;
_inputindex += _buf_read;
_buf_read += o;
// log_i("_buf_read: %d", _buf_read);
}
else
{
_buf_read = _input->readBytes(_read_buf, READ_BUFFER_SIZE);
_p = _read_buf;
_inputindex += _buf_read;
}
i = 0;
}
if (found_FFD9)
{
// log_i("Found FFD9 at: %d.", _mjpeg_buf_offset);
if (_mjpeg_buf_offset > _mjpegBufufSize) {
log_e("_mjpeg_buf_offset(%d) > _mjpegBufufSize (%d)", _mjpeg_buf_offset, _mjpegBufufSize);
}
return true;
}
}
return false;
}
bool mjpeg_draw_frame()
{
mjpegBuf *mBuf = &_mjpegBufs[_mBufIdx];
mBuf->size = _mjpeg_buf_offset;
// log_i("_mjpegBufs[%d].size: %d.", _mBufIdx, _mjpegBufs[_mBufIdx].size);
// log_i("_mjpegBufs[%d].buf start: %X %X, end: %X, %X.", _mjpegBufs, _mjpegBufs[_mBufId].buf[0], _mjpegBufs[_mBufIdx].buf[1], _mjpegBufs[_mBufIdx].buf[_mjpeg_buf_offset - 2], _mjpegBufs[_mBufIdx].buf[_mjpeg_buf_offset - 1]);
xQueueSend(_pDecodeTask.xqh, &mBuf, portMAX_DELAY);
++_mBufIdx;
if (_mBufIdx >= NUMBER_OF_DECODE_BUFFER)
{
_mBufIdx = 0;
}
_mjpeg_buf = _mjpegBufs[_mBufIdx].buf;
// log_i("queue decode_task end");
return true;
}