使用ESP32S3在OLED屏幕播放视频

HW 发布于 19 天前 195 次阅读


练习项目,使用ESP32S3 -N16R8、4引脚OLED

代码相关的懒得开仓库了,直接在这里放代码得了

使用Arduino开发环境


思路:使用Python生成oled文件,然后用LittleFS将文件上传到ESP32

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import sys
import subprocess
import tkinter as tk
from tkinter import filedialog, messagebox

# ============ 画面 / 播放参数 ============
W = 128
H = 64
FPS = 10
INVERT = False      # True 时两种模式都做黑白反转

# ============ 输出模式 ============
# 'bw'    : 纯黑白二值,与原脚本行为完全一致
# 'gray3' : 3 级灰度 —— 黑 / 灰(50% 网点) / 白
MODE = 'gray3'

# ---- MODE='bw' 参数(原脚本参数,原样保留)----
THRESHOLD = 128     # 二值化阈值 0-255

# ---- MODE='gray3' 参数 ----
# 亮度分区:
#   [0,        T_BLACK) -> 黑       (实心不点亮)
#   [T_BLACK,  T_WHITE) -> 灰       (50% 网点)
#   [T_WHITE,  255]     -> 白       (实心点亮)
#
# 调整建议:
#   想更接近原来的纯黑白观感 → 把 T_BLACK 拉高、T_WHITE 也拉高,
#                              让灰带尽量窄(如 140 / 180)
#   想让中灰层次更丰富       → 把两者拉开(如 80 / 200)
#   想让画面整体偏暗         → T_BLACK 降低、T_WHITE 也降低
T_BLACK = 100
T_WHITE = 170


def read_exact(stream, n):
    buf = bytearray()
    while len(buf) < n:
        chunk = stream.read(n - len(buf))
        if not chunk:
            break
        buf.extend(chunk)
    return bytes(buf)


def check_ffmpeg():
    try:
        subprocess.run(
            ['ffmpeg', '-version'],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            check=True,
        )
        return True
    except (OSError, subprocess.CalledProcessError):
        return False


def video_to_oled(input_path, output_path):
    # 纯黑白模式沿用原来的点采样;灰度模式换 lanczos,让灰带边界更平滑
    scale_flags = 'neighbor' if MODE == 'bw' else 'lanczos'
    cmd = [
        'ffmpeg',
        '-i', input_path,
        '-vf', f'fps={FPS},scale={W}:{H}:flags={scale_flags}',
        '-f', 'rawvideo',
        '-pix_fmt', 'gray',
        '-'
    ]
    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.DEVNULL,
    )

    row_bytes = W // 8
    frame_pixels = W * H
    frame_bytes = W * H // 8
    frame_count = 0

    with open(output_path, 'wb') as f:
        while True:
            raw = read_exact(proc.stdout, frame_pixels)
            if len(raw) < frame_pixels:
                break

            xbm = bytearray(frame_bytes)

            if MODE == 'bw':
                # ===== 原逻辑,未做任何改动 =====
                for y in range(H):
                    in_base = y * W
                    out_base = y * row_bytes
                    for x in range(W):
                        v = raw[in_base + x]
                        on = (v > THRESHOLD) != INVERT
                        if on:
                            xbm[out_base + (x >> 3)] |= 1 << (x & 7)
            else:
                # ===== 3 级灰度 =====
                for y in range(H):
                    in_base = y * W
                    out_base = y * row_bytes
                    for x in range(W):
                        v = raw[in_base + x]
                        if v < T_BLACK:
                            on = False                     # 黑
                        elif v >= T_WHITE:
                            on = True                      # 白
                        else:
                            on = ((x + y) & 1) == 0        # 灰:棋盘 50% 网点
                        if INVERT:
                            on = not on
                        if on:
                            xbm[out_base + (x >> 3)] |= 1 << (x & 7)

            f.write(xbm)
            frame_count += 1

    proc.wait()
    if proc.returncode != 0:
        raise RuntimeError('ffmpeg 执行失败,请检查视频文件或 ffmpeg 安装。')
    return frame_count


def main():
    if not check_ffmpeg():
        print('未找到 ffmpeg,请先安装并加入 PATH。')
        sys.exit(1)

    root = tk.Tk()
    root.withdraw()

    input_path = filedialog.askopenfilename(
        title='选择视频文件',
        filetypes=[('视频文件', '*.mp4 *.avi *.mov *.mkv *.webm'), ('所有文件', '*.*')]
    )
    if not input_path:
        return

    default_name = os.path.splitext(os.path.basename(input_path))[0] + '.oled'
    output_path = filedialog.asksaveasfilename(
        title='保存 OLED 帧序列',
        defaultextension='.oled',
        initialfile=default_name,
        filetypes=[('OLED 帧序列', '*.oled'), ('所有文件', '*.*')]
    )
    if not output_path:
        return

    try:
        n = video_to_oled(input_path, output_path)
        size = os.path.getsize(output_path)

        if MODE == 'bw':
            desc = f'纯黑白(阈值 {THRESHOLD})'
        else:
            desc = f'3 级灰度(黑<{T_BLACK},灰,白≥{T_WHITE})'

        messagebox.showinfo(
            '完成',
            f'已生成 {n} 帧\n'
            f'文件:{output_path}\n'
            f'大小:{size} 字节 ({size/1024:.1f} KB)\n'
            f'分辨率:{W}x{H},帧率:{FPS} FPS\n'
            f'模式:{desc}'
        )
    except Exception as e:
        messagebox.showerror('错误', str(e))


if __name__ == '__main__':
    main()

#include 
#include 
#include 
#include 

U8G2_SSD1309_128X64_NONAME0_F_HW_I2C oled(
  U8G2_R0,
  U8X8_PIN_NONE,
  9,  // SCL
  8   // SDA
);

const int W = 128;
const int H = 64;
const int FRAME_SIZE = W * H / 8;  // 1024
const int FPS = 10;                // 与生成时一致

File videoFile;
uint8_t frame[FRAME_SIZE];
unsigned long lastFrameTime = 0;

void setup() {
  Serial.begin(115200);

  Wire.setClock(400000);  // 提高 I2C 速度,若不稳定改回 100000
  oled.begin();

  if (!LittleFS.begin(true)) {  // true: 挂载失败时格式化
    Serial.println("LittleFS 挂载失败");
    return;
  }

  videoFile = LittleFS.open("/danking.oled", "r");
  if (!videoFile) {
    Serial.println("找不到 /danking.oled");
    return;
  }

  Serial.printf("视频文件大小: %u 字节\n", videoFile.size());
  Serial.printf("总帧数: %u\n", videoFile.size() / FRAME_SIZE);
}

void loop() {
  if (!videoFile) return;

  unsigned long now = millis();
  if (now - lastFrameTime < 1000 / FPS) return;
  lastFrameTime = now;

  size_t n = videoFile.read(frame, FRAME_SIZE);
  if (n < FRAME_SIZE) {
    // 读到结尾,回到开头循环播放
    videoFile.seek(0);
    return;
  }

  oled.clearBuffer();
  oled.drawXBM(0, 0, W, H, frame);
  oled.sendBuffer();
}

此作者没有提供个人介绍。
最后更新于 2026-10-19