<Project-4 2mp4jpg> Python Coding Flask应用:将视频与图片转换为MP4/JPG应用 主要用到 ffmpeg PIL

#原因#

在twitter保存的图片格式是jfif,不能放到“剪映”里当素材。
截屏软件的视频保存格式只能选webm,同样在“剪映”也不能用。
用Python搞定它。
如下图:

上篇文章使用 ffmpeg, 这个就有转码的功能,那就再拼一个。 这个要移到 QNAP NAS,在container 上运行。

文件转换系统功能介绍

  1. 文件上传:用户可以通过 Web 界面上传图片和视频文件。
  2. 文件转换
    1. 图片:将上传的图片转换为 JPEG 格式。
    2. 视频:将视频转换为 MP4 格式,自动支持硬件加速(如果环境支持)或使用软件编码进行转换。
  3. 进度监控:用户可以通过 /stream 路由实时监控视频转换过程,进度会显示在网页上。
  4. 文件预览:转换完成后,用户可以在 Web 界面预览转换后的图片或视频。
  5. 文件下载:转换完成后,用户可以从浏览器下载已转换的文件。
  6. 自动清理:系统会自动清理上传和转换文件夹中超过 30 天的旧文件。
  7. 端口占用:   9002  (cobalt 9001 github-下载Youtube视频 算是对齐)

主要组件:

5个文件

  1. 主程序 (app.py)

  2. 文件上传 (upload.html):

    用户上传图片或视频文件,应用程序会保存文件到 uploads/ 文件夹。
  3. 处理页面 (processing.html):

    文件上传后,用户会跳转到一个页面,显示文件转换过程的实时进度。
    FFmpeg 的输出会实时显示在网页上,供用户查看转换进度。
  4. 下载页面 (download.html):

    转换完成后,用户可以直接在页面中预览图片或视频。
    用户可以选择下载文件或上传其他文件进行转换。
  5. Python 后端
    run_ffmpeg():处理视频转换的功能。该函数会检测硬件加速(如 NVENC 或 AMF)是否可用,如果不可用则会回退到软件编码(libx264)。
    process_image_file():该函数用于将图片文件转换为 JPEG 格式,使用 Python Imaging Library (PIL)。
    stream():实时流式传输 FFmpeg 输出,为用户提供转换进度反馈。
    download_file():处理已转换文件的下载。
    clean_old_files():清理 uploads/converted/ 目录中超过 30 天的文件。

3个目录

        模板文件存放:
                ./templates

        下面2个会自动创建:
        上传文件目录
                 ./uploads
        转换后文件目录
                ./converted

Docker配置

    应用程序通过 Docker 容器化,确保它可以在不同的环境中一致运行

  1. Dockerfile:用于构建 Flask 应用的容器,并安装所有依赖项,包括 FFmpeg。

    Dockerfile 的关键部分:
            ffmpeg:确保在容器内安装 FFmpeg。
            上面介绍的5+3

  2. 容器名称2mp4jpg 便于识别管理与项目同名

用户流程:

  1. 上传文件:用户访问首页并上传图片或视频文件。
  2. 文件转换:上传后,用户会跳转到转换进度页面,实时查看转换状态。
  3. 文件下载:转换完成后,用户会进入下载页面,在该页面可以预览或下载已转换的文件。

实现效果

上面是jfif图片格式to JPG, 下面是webm视频2 MP4

代码讲解

1. 配置部分

from flask import Flask, request, render_template, send_file, redirect, url_for, Response
import os
import uuid
import subprocess
import threading
from queue import Queue
import traceback
from PIL import Image
import time
from flask_apscheduler import APScheduler
import filetype  # 替代 imghdr

导入必要的模块

  • Flask:用于创建 Web 应用程序。
  • osuuid:用于处理文件路径和生成唯一文件名。
  • subprocess:用于调用 FFmpeg 进行视频转换。
  • threading:使用线程来处理视频转换任务。
  • Queue:用于存储 FFmpeg 的输出信息。
  • PIL.Image:用于处理图片转换。
  • APScheduler:用于定期清理旧文件。


2. 应用初始化

class Config:
    SCHEDULER_API_ENABLED = True

app = Flask(__name__)
app.config.from_object(Config())
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['CONVERTED_FOLDER'] = 'converted'

配置应用:设置上传和转换文件夹路径,并启动 Flask 应用。应用还启用了任务调度(APS Scheduler),用于定期清理旧文件。

3. 文件上传和处理

def upload_file():
    if request.method == 'POST':
        file = request.files['file']
        if file:
            unique_id = str(uuid.uuid4())
            input_filename = unique_id + '_' + file.filename
            input_filepath = os.path.join(app.config['UPLOAD_FOLDER'], input_filename)
            file.save(input_filepath)

            file_type = detect_file_type(input_filepath)

            if file_type == 'image':
                output_filename = unique_id + '_converted.jpg'
                output_filepath = os.path.join(app.config['CONVERTED_FOLDER'], output_filename)
                if process_image_file(input_filepath, output_filepath):
                    return render_template('download.html', filename=output_filename, file_type=file_type)
                else:
                    return '图片转换失败', 500
            elif file_type == 'video':
                output_filename = unique_id + '_converted.mp4'
                output_filepath = os.path.join(app.config['CONVERTED_FOLDER'], output_filename)
                output_queue = Queue()
                ffmpeg_output_queues[unique_id] = output_queue
                threading.Thread(target=run_ffmpeg, args=(input_filepath, output_filepath, unique_id)).start()
                return render_template('processing.html', uid=unique_id, original_filename=file.filename)
            else:
                return '不支持的文件类型', 400
    else:
        return render_template('upload.html')

功能描述

  • 用户上传文件后,程序根据文件类型(图片或视频)分别处理:
    • 图片:将其转换为 JPEG 格式。
    • 视频:使用 FFmpeg 转换为 MP4 格式,并将转换进度通过实时更新显示在前端。
  • 文件保存路径通过 uuid 保证文件名唯一,防止覆盖原有文件。

4. 文件转换(图片)

def process_image_file(input_filepath, output_filepath):
    try:
        with Image.open(input_filepath) as img:
            img = img.convert('RGB')  # 转换为 RGB 模式以确保兼容性
            img.save(output_filepath, 'JPEG')
        return True
    except Exception as e:
        print(f"图片处理失败: {e}")
        return False
  • 功能描述
    • 该函数处理图片转换,将上传的图片转换为 RGB 模式并保存为 JPEG 格式。

5. 视频转换

def run_ffmpeg(input_filepath, output_filepath, uid):
    encoder = get_available_encoder()
    command = [
        'ffmpeg',
        '-y',
        '-i', input_filepath,
        '-c:v', encoder,
        '-preset', 'fast',
        '-c:a', 'aac',
        '-b:a', '128k',
        '-progress', '-',
        '-nostats',
        '-hide_banner',
        output_filepath
    ]

    try:
        process = subprocess.Popen(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            universal_newlines=True,
            bufsize=1
        )
        for line in process.stdout:
            line = line.strip()
            ffmpeg_output_queues[uid].put(line + '\n')
            print(line)
        process.stdout.close()
        process.wait()

        if process.returncode != 0:
            ffmpeg_output_queues[uid].put(f"error: FFmpeg 转换失败,错误代码: {process.returncode}")
        else:
            ffmpeg_output_queues[uid].put('DONE')
    except Exception as e:
        ffmpeg_output_queues[uid].put(f"error: 运行 FFmpeg 失败: {e}")
    finally:
        ffmpeg_output_queues[uid].put(None)

功能描述

  • 使用 subprocess.Popen 调用 FFmpeg 进行视频转换。过程中使用线程,避免阻塞主线程,并将 FFmpeg 的输出实时推送到前端。

5.1 自动选择可用的硬件加速编码器

# 自动选择可用的硬件加速编码器
def check_nvenc_support():
    try:
        test_command = [
            'ffmpeg', '-y', '-f', 'lavfi', '-i', 'nullsrc=s=64x64:d=1', '-c:v', 'h264_nvenc', '-f', 'null', '-'
        ]
        subprocess.check_output(test_command, stderr=subprocess.STDOUT, universal_newlines=True)
        print("检测到可用的硬件编码器:h264_nvenc")
        return True
    except subprocess.CalledProcessError as e:
        print(f"NVENC 测试失败,回退到软件编码: {e}")
        return False

def check_amf_support():
    try:
        test_command = [
            'ffmpeg', '-y', '-f', 'lavfi', '-i', 'nullsrc=s=64x64:d=1', '-c:v', 'h264_amf', '-f', 'null', '-'
        ]
        subprocess.check_output(test_command, stderr=subprocess.STDOUT, universal_newlines=True)
        print("检测到可用的硬件编码器:h264_amf")
        return True
    except subprocess.CalledProcessError as e:
        print(f"AMF 测试失败,回退到软件编码: {e}")
        return False

def get_available_encoder():
    try:
        encoders_output = subprocess.check_output(['ffmpeg', '-encoders'], universal_newlines=True)
    except subprocess.CalledProcessError as e:
        print(f"获取编码器列表失败:{e}")
        return 'libx264'  # 默认使用软件编码器

    encoder_list = []
    for line in encoders_output.split('\n'):
        if line.startswith(' '):  # 编码器列表行以空格开头
            parts = line.strip().split()
            if len(parts) >= 2:
                encoder_name = parts[1]
                encoder_list.append(encoder_name)

    # 优先级列表,从高到低
    hardware_encoders = ['h264_nvenc', 'h264_amf', 'h264_qsv']

    if 'h264_nvenc' in encoder_list and check_nvenc_support():
        return 'h264_nvenc'
    elif 'h264_amf' in encoder_list and check_amf_support():
        return 'h264_amf'

    print("未检测到可用的硬件编码器,使用软件编码器 libx264")
    return 'libx264'

功能描述:

        判断能否使用硬件来加速视频转码

6. 文件类型检测

def detect_file_type(filepath):
    kind = filetype.guess(filepath)
    if kind is None:
        return 'unknown'
    elif kind.mime.startswith('image/'):
        return 'image'
    elif kind.mime.startswith('video/'):
        return 'video'
    else:
        return 'unknown'

功能描述

  • 该函数使用 filetype 模块检测文件类型,判断文件是图片还是视频,并根据文件类型采取不同的处理逻辑。

7. 进度实时显示

@app.route('/stream/<uid>')
def stream(uid):
    def generate():
        output_queue = ffmpeg_output_queues.get(uid)
        if output_queue is None:
            yield 'data: 无效的UID\n\n'
            return
        while True:
            line = output_queue.get()
            if line is None:
                break
            yield f'data: {line}\n\n'
            if line.strip() == 'DONE':
                break
        ffmpeg_output_queues.pop(uid, None)
    return Response(generate(), mimetype='text/event-stream')

功能描述

  • 通过 Server-Sent Events 实现与前端的实时通信,向前端推送 FFmpeg 的转换进度。

8. 下载文件页面

@app.route('/download/<filename>')
def download_file(filename):
    file_path = os.path.join(app.config['CONVERTED_FOLDER'], filename)
    print(f"下载页面 文件路径 文件已生成: {file_path}")

    if os.path.exists(file_path):
        return send_file(file_path, as_attachment=True)
    else:
        print(f"视频文件 不存在: {file_path}")
        return '视频文件不存在', 404

功能描述

  • 当用户点击下载链接时,该路由负责处理文件的下载请求。根据请求的 filename,程序查找对应文件,并通过 send_file 方法将文件发送给用户。如果文件不存在,则返回 404 错误页面。

9. 自动选择硬件加速编码器

def get_available_encoder():
    try:
        encoders_output = subprocess.check_output(['ffmpeg', '-encoders'], universal_newlines=True)
    except subprocess.CalledProcessError as e:
        print(f"获取编码器列表失败:{e}")
        return 'libx264'  # 默认使用软件编码器

    encoder_list = []
    for line in encoders_output.split('\n'):
        if line.startswith(' '):  # 编码器列表行以空格开头
            parts = line.strip().split()
            if len(parts) >= 2:
                encoder_name = parts[1]
                encoder_list.append(encoder_name)

    # 优先级列表,从高到低
    hardware_encoders = ['h264_nvenc', 'h264_amf', 'h264_qsv']

    if 'h264_nvenc' in encoder_list and check_nvenc_support():
        return 'h264_nvenc'
    elif 'h264_amf' in encoder_list and check_amf_support():
        return 'h264_amf'

    print("未检测到可用的硬件编码器,使用软件编码器 libx264")
    return 'libx264'

功能描述

  • 该函数会自动检测系统中是否安装了硬件加速编码器(如 h264_nvench264_amf)。如果硬件编码器可用,则优先使用硬件加速,否则使用默认的 libx264 软件编码器。
  • subprocess.check_output 会执行 FFmpeg 命令来列出系统中的编码器,并根据编码器的可用性返回适当的编码器名称。

10. 定时清理旧文件

def clean_old_files(folder, days=30):
    now = time.time()
    cutoff = now - days * 86400  # 30 天的时间戳

    if not os.path.isdir(folder):
        return

    for filename in os.listdir(folder):
        file_path = os.path.join(folder, filename)
        if os.path.isfile(file_path):
            file_mtime = os.path.getmtime(file_path)
            if file_mtime < cutoff:
                try:
                    os.remove(file_path)
                    print(f"已删除文件: {file_path}")
                except Exception as e:
                    print(f"删除文件时出错: {file_path}, 错误: {e}")
  • 功能描述
    • 该函数用于定期清理超过一定时间的文件。cutoff 用于计算文件的最后修改时间与当前时间的差,如果文件的修改时间超过了设定的天数(默认 30 天),该文件会被删除。

11. 调度器任务

@scheduler.task('interval', id='clean_old_files_job', days=1)
def scheduled_clean_old_files():
    clean_old_files(app.config['UPLOAD_FOLDER'], days=30)
    clean_old_files(app.config['CONVERTED_FOLDER'], days=30)
    print("定期清理任务已执行。")

功能描述

  • 这是 APScheduler 的定时任务,每天执行一次。任务内容为调用 clean_old_files 函数,定期清理上传文件夹和转换文件夹中的旧文件。

12. 处理进度显示

<script>
    var outputDiv = document.getElementById('output');
    var eventSource = new EventSource('/stream/{{ uid }}');
    eventSource.onmessage = function(e) {
        var message = e.data.trim();
        if (message === 'DONE') {
            eventSource.close();
            outputDiv.innerHTML += '\n转换完成!';
            var downloadFilename = '{{ uid }}_converted.mp4';
            window.location.href = '/download/' + encodeURIComponent(downloadFilename);
        } else {
            outputDiv.innerHTML += message + '\n';
            outputDiv.scrollTop = outputDiv.scrollHeight;
        }
        if (message.startsWith('error:')) {
            outputDiv.innerHTML += `<span style="color:red;">转换失败: ${message.substring(6)}</span>`;
            eventSource.close();
        }
    };
</script>

功能描述

  • 该前端代码通过 EventSource 来接收来自后端的 FFmpeg 输出信息,实时更新在页面上。当收到 DONE 消息时,页面会自动跳转到下载链接。
  • 如果转换失败,输出错误消息并显示在页面上。

小结

  • 主要功能
    • 该应用允许用户上传图片和视频文件,自动检测文件类型并进行适当的转换。
    • 图片文件会被转换为 JPEG 格式,视频文件会被转换为 MP4 格式,转换过程会实时反馈给用户。
    • 转换完成后,用户可以预览并下载转换后的文件。
  • 技术点
    • 使用 Flask 框架搭建 Web 应用程序。
    • 使用 FFmpeg 进行视频文件的转换,并支持硬件加速。
    • 使用 APScheduler 定时清理旧文件。
    • 实现了前后端的实时通信,通过 Server-Sent Events 更新前端的转换进度。

代码:

        复制全部代码,配置所需环境,放在对应的目录下面,即可使用。

1. 目录结构:

- app.py
- templates/
    - upload.html
    - processing.html
    - download.html
    - download_page.html
- uploads/
- converted/
- Dockerfile
- requirements.txt
- docker-compose.yml (QNAP上用不到,可以不用准备)

2. 主程序 app.py

from flask import Flask, request, render_template, send_file, redirect, url_for, Response
import os
import uuid
import subprocess
import threading
from queue import Queue
import traceback
from PIL import Image
import time
from flask_apscheduler import APScheduler
import filetype  # 替代 imghdr

# 定义配置类
class Config:
    SCHEDULER_API_ENABLED = True

app = Flask(__name__)
app.config.from_object(Config())
app.config['UPLOAD_FOLDER'] = os.path.abspath('uploads')
app.config['CONVERTED_FOLDER'] = os.path.abspath('converted')

# 确保上传和转换文件夹存在
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(app.config['CONVERTED_FOLDER'], exist_ok=True)

# 初始化调度器
scheduler = APScheduler()
scheduler.init_app(app)
scheduler.start()

# 添加定时任务,每天清理一次超过 30 天的旧文件
@scheduler.task('interval', id='clean_old_files_job', days=1)
def scheduled_clean_old_files():
    clean_old_files(app.config['UPLOAD_FOLDER'], days=30)
    clean_old_files(app.config['CONVERTED_FOLDER'], days=30)
    print("定期清理任务已执行。")

# 全局变量,存储 FFmpeg 输出
ffmpeg_output_queues = {}

# 函数:清理旧文件
def clean_old_files(folder, days=30):
    now = time.time()
    cutoff = now - days * 86400  # 30 天的时间戳

    if not os.path.isdir(folder):
        return

    for filename in os.listdir(folder):
        file_path = os.path.join(folder, filename)
        if os.path.isfile(file_path):
            file_mtime = os.path.getmtime(file_path)
            if file_mtime < cutoff:
                try:
                    os.remove(file_path)
                    print(f"已删除文件: {file_path}")
                except Exception as e:
                    print(f"删除文件时出错: {file_path}, 错误: {e}")

# 函数:检测文件类型
def detect_file_type(filepath):
    kind = filetype.guess(filepath)
    if kind is None:
        return 'unknown'
    elif kind.mime.startswith('image/'):  # 确保这是图片类型
        return 'image'
    elif kind.mime.startswith('video/'):  # 确保这是视频类型
        return 'video'
    else:
        return 'unknown'

# 处理图片文件
def process_image_file(input_filepath, output_filepath):
    try:
        with Image.open(input_filepath) as img:
            img = img.convert('RGB')  # 转换为 RGB 模式以确保兼容性
            img.save(output_filepath, 'JPEG')
        return True
    except Exception as e:
        print(f"图片处理失败: {e}")
        return False

# 自动选择可用的硬件加速编码器
def check_nvenc_support():
    try:
        test_command = [
            'ffmpeg', '-y', '-f', 'lavfi', '-i', 'nullsrc=s=64x64:d=1', '-c:v', 'h264_nvenc', '-f', 'null', '-'
        ]
        subprocess.check_output(test_command, stderr=subprocess.STDOUT, universal_newlines=True)
        print("检测到可用的硬件编码器:h264_nvenc")
        return True
    except subprocess.CalledProcessError as e:
        print(f"NVENC 测试失败,回退到软件编码: {e}")
        return False

def check_amf_support():
    try:
        test_command = [
            'ffmpeg', '-y', '-f', 'lavfi', '-i', 'nullsrc=s=64x64:d=1', '-c:v', 'h264_amf', '-f', 'null', '-'
        ]
        subprocess.check_output(test_command, stderr=subprocess.STDOUT, universal_newlines=True)
        print("检测到可用的硬件编码器:h264_amf")
        return True
    except subprocess.CalledProcessError as e:
        print(f"AMF 测试失败,回退到软件编码: {e}")
        return False

def get_available_encoder():
    try:
        encoders_output = subprocess.check_output(['ffmpeg', '-encoders'], universal_newlines=True)
    except subprocess.CalledProcessError as e:
        print(f"获取编码器列表失败:{e}")
        return 'libx264'  # 默认使用软件编码器

    encoder_list = []
    for line in encoders_output.split('\n'):
        if line.startswith(' '):  # 编码器列表行以空格开头
            parts = line.strip().split()
            if len(parts) >= 2:
                encoder_name = parts[1]
                encoder_list.append(encoder_name)

    # 优先级列表,从高到低
    hardware_encoders = ['h264_nvenc', 'h264_amf', 'h264_qsv']

    if 'h264_nvenc' in encoder_list and check_nvenc_support():
        return 'h264_nvenc'
    elif 'h264_amf' in encoder_list and check_amf_support():
        return 'h264_amf'

    print("未检测到可用的硬件编码器,使用软件编码器 libx264")
    return 'libx264'

def run_ffmpeg(input_filepath, output_filepath, uid):
    encoder = get_available_encoder()
    print(f"使用编码器: {encoder}")

    if encoder in ['h264_nvenc', 'h264_amf', 'h264_qsv']:
        print(f"使用硬件编码器: {encoder}")
    else:
        print(f"使用软件编码器: {encoder}")

    # 构建 FFmpeg 命令
    command = [
        'ffmpeg',
        '-y',
        '-i', input_filepath,
        '-c:v', encoder,  # 自动选择硬件或软件编码器
        '-preset', 'fast',
        '-c:a', 'aac',
        '-b:a', '128k',
        '-progress', '-',  # 将进度信息输出到标准输出
        '-nostats',
        '-hide_banner',
        output_filepath
    ]

    try:
        # 启动 FFmpeg 进程
        process = subprocess.Popen(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            universal_newlines=True,
            bufsize=1  # 行缓冲
        )

        # 读取输出并放入队列
        for line in process.stdout:
            line = line.strip()
            ffmpeg_output_queues[uid].put(line + '\n')
            print(line)  # 打印 FFmpeg 输出到控制台,帮助排查问题

        # 等待进程结束
        process.stdout.close()
        process.wait()

        # 检查是否成功完成
        if process.returncode != 0:
            error_message = f"FFmpeg 转换失败,错误代码: {process.returncode}"
            print(error_message)
            ffmpeg_output_queues[uid].put(f"error:{error_message}\n")
        else:
            success_message = f"FFmpeg 转换成功,文件已生成: {output_filepath}"
            print(success_message)
            ffmpeg_output_queues[uid].put('DONE') # 确保发送了 'DONE'
    except Exception as e:
        error_message = f"运行 FFmpeg 失败: {e}"
        print(error_message)
        ffmpeg_output_queues[uid].put(f"error:{error_message}\n")
    finally:
        ffmpeg_output_queues[uid].put(None)  # 队列结束标记

# 上传和处理文件
@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        file = request.files['file']
        if file:
            unique_id = str(uuid.uuid4())
            input_filename = unique_id + '_' + file.filename
            input_filepath = os.path.join(app.config['UPLOAD_FOLDER'], input_filename)
            file.save(input_filepath)

            # 检测文件类型
            file_type = detect_file_type(input_filepath)

            if file_type == 'image':
                output_filename = unique_id + '_converted.jpg'
                output_filepath = os.path.join(app.config['CONVERTED_FOLDER'], output_filename)

                print(f"文件将保存到: {output_filepath}")

                # 处理图片文件
                if process_image_file(input_filepath, output_filepath):
                    return render_template('download.html', filename=output_filename, file_type=file_type)
                else:
                    return '图片转换失败', 500
            elif file_type == 'video':
                output_filename = unique_id + '_converted.mp4'
                output_filepath = os.path.join(app.config['CONVERTED_FOLDER'], output_filename)

                # 创建一个队列来存储 FFmpeg 输出
                output_queue = Queue()
                ffmpeg_output_queues[unique_id] = output_queue

                # 启动 FFmpeg 进程和线程来读取输出
                threading.Thread(target=run_ffmpeg, args=(input_filepath, output_filepath, unique_id)).start()

                return render_template('processing.html', uid=unique_id, original_filename=file.filename)
            else:
                return '不支持的文件类型', 400
    else:
        return render_template('upload.html')

# 路由:下载文件页面
@app.route('/download/<filename>')
def download_file(filename):
    file_path = os.path.join(app.config['CONVERTED_FOLDER'], filename)
    print(f"下载 文件名: {filename}")
    print(f"下载页面 文件路径 文件已生成: {file_path}")

    if os.path.exists(file_path):
        return send_file(file_path, as_attachment=True)
    else:
        print(f"视频文件 不存在: {file_path}")
        return '视频文件不存在', 404

# 路由:下载已转换的文件
@app.route('/download_file/<filename>')
def send_converted_file(filename):
    file_path = os.path.join(app.config['CONVERTED_FOLDER'], filename)
    if os.path.exists(file_path):
        return send_file(file_path, as_attachment=False)
    else:
        return '图片文件不存在', 404

# Stream FFmpeg output
@app.route('/stream/<uid>')
def stream(uid):
    def generate():
        output_queue = ffmpeg_output_queues.get(uid)
        if output_queue is None:
            yield 'data: 无效的UID\n\n'
            return
        while True:
            line = output_queue.get()
            if line is None:
                break
            yield f'data: {line}\n\n'
            if line.strip() == 'DONE':
                break
        # 清理队列
        ffmpeg_output_queues.pop(uid, None)
    return Response(generate(), mimetype='text/event-stream')

@app.route('/download_page/<filename>')
def download_page(filename):
    return render_template('download_page.html', filename=filename)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9002, threaded=True)

2. 模板目录下文件

 1. 上传页面 ./templates/upload.html

<!doctype html>
<html>
<head>
    <title>文件转换为 MP4 或 JPG</title>
</head>
<body>
    <h1>上传视频或图片文件进行转换</h1>
    <form method="post" enctype="multipart/form-data">
        <input type="file" name="file" accept="video/*,image/*" required>
        <input type="submit" value="上传并转换">
    </form>
</body>
</html>

2. 处理页面 ./templates/procesing.html

<!doctype html>
<html>
<head>
    <title>视频转换中</title>
    <style>
        #output {
            width: 100%;
            height: 300px;
            border: 1px solid #ccc;
            overflow-y: scroll;
            white-space: pre-wrap;
            background-color: #f8f8f8;
            padding: 10px;
        }
    </style>
</head>
<body>
    <h1>视频正在转换中,请稍候...</h1>
    <div id="output"></div>

    <script>
        var outputDiv = document.getElementById('output');
        var eventSource = new EventSource('/stream/{{ uid }}');
        eventSource.onmessage = function(e) {
            var message = e.data.trim();
            if (message === 'DONE') {
                eventSource.close();
                outputDiv.innerHTML += '\n转换完成!';

                // 跳转到下载页面,不再自动下载
                var downloadFilename = '{{ uid }}_converted.mp4';
                window.location.href = '/download_page/' + encodeURIComponent(downloadFilename);
            } else {
                outputDiv.innerHTML += message + '\n';
                outputDiv.scrollTop = outputDiv.scrollHeight;
            }
        };
    </script>
</body>
</html>


3. 图片下载页面 ./templates/download.html

<!doctype html>
<html>
<head>
    <title>文件转换成功</title>
    <style>
        /* 设置视频和图片的预览尺寸 */
        .preview {
            max-width: 20%; /* 将宽度设置为原始尺寸的 1/5 */
            height: auto;
        }
    </style>
</head>
<body>
    <h1>文件转换成功!</h1>

    {% if file_type == 'image' %}
        <h2>图片预览:</h2>
        <img src="{{ url_for('send_converted_file', filename=filename) }}" alt="图片预览" class="preview">
    {% elif file_type == 'video' %}
        <h2>视频预览:</h2>
        <video controls class="preview">
            <source src="{{ url_for('send_converted_file', filename=filename) }}" type="video/mp4">
            您的浏览器不支持视频播放。
        </video>
    {% else %}
        <p>无法预览此类型的文件。</p>
    {% endif %}

    <p>点击下面的链接下载转换后的文件:</p>
    <a href="{{ url_for('download_file', filename=filename) }}" download>下载文件</a>
    <br><br>
    <a href="{{ url_for('upload_file') }}">转换另一个文件</a>
</body>
</html>


4. 视频下载页面 ./templates/download_page.html

<!doctype html>
<html>
<head>
    <title>视频转换成功</title>
    <style>
        video {
            max-width: 100%;
        }
    </style>
</head>
<body>
    <h1>视频转换成功!</h1>

    <h2>视频预览:</h2>
    <video controls>
        <source src="{{ url_for('send_converted_file', filename=filename) }}" type="video/mp4">
        您的浏览器不支持视频播放。
    </video>

    <p>点击下面的链接下载转换后的文件:</p>
    <a href="{{ url_for('send_converted_file', filename=filename) }}" download>下载文件</a>

    <br><br>
    <a href="{{ url_for('upload_file') }}">上传新文件转换</a>
</body>
</html>

3. 移到NAS

Docker要用到至少两个文件 Dockerfile 与 requirements.txt

1. Dockerfile

        注意首字母是大写

# 使用官方 Python 基础镜像
FROM python:3.12-slim

# 设置工作目录
WORKDIR /app

# 安装 ffmpeg 及其依赖
RUN apt-get update && apt-get install -y ffmpeg && apt-get clean

# 将 requirements.txt 文件复制到容器中
COPY requirements.txt .

# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt

# 将当前目录的所有文件复制到容器中的 /app 目录
COPY . .

# 暴露 Flask 应用的端口
EXPOSE 9002

# 设置环境变量让 Flask 以生产模式运行
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0

# 启动 Flask 应用
CMD ["flask", "run", "--host=0.0.0.0", "--port=9002"]

2. requirements.txt

Flask==2.3.3
filetype==1.1.0
Pillow==10.0.0
flask-apscheduler==1.12.0

ffmpeg要安装在容器上层,但用NAS qpkg ffmpeg 安装后,没有成功。只能在docker 里去安装,如果 你用的是windows系统:pip3 install ffmpeg

QNAP NAS里的container 2mp4jpg 里,执行命令:

apt-get update
apt-get install -y ffmpeg

SSH链接到NAS:

我的这些代码本就是存在NAS上,故省去复制这一过程。

在shell下并移到文件所在目录,用下面命令来创建容器image: 2mp4jpg 

# 创建image
docker build -t 2mp4jpg
#赋予端口 我用的是 9002
docker run -d -p 9002:9002 flask-app
# 进入docker
docker exec -it 2mp4jpg /bin/bash
   # 安装ffmpeg 见上面 两行代码


#等用

离开IT行业整整11年

9/19/2024

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值