#原因#
在twitter保存的图片格式是jfif,不能放到“剪映”里当素材。
截屏软件的视频保存格式只能选webm,同样在“剪映”也不能用。
用Python搞定它。
如下图:
上篇文章使用 ffmpeg, 这个就有转码的功能,那就再拼一个。 这个要移到 QNAP NAS,在container 上运行。
文件转换系统功能介绍
- 文件上传:用户可以通过 Web 界面上传图片和视频文件。
- 文件转换:
- 图片:将上传的图片转换为 JPEG 格式。
- 视频:将视频转换为 MP4 格式,自动支持硬件加速(如果环境支持)或使用软件编码进行转换。
- 进度监控:用户可以通过
/stream
路由实时监控视频转换过程,进度会显示在网页上。 - 文件预览:转换完成后,用户可以在 Web 界面预览转换后的图片或视频。
- 文件下载:转换完成后,用户可以从浏览器下载已转换的文件。
- 自动清理:系统会自动清理上传和转换文件夹中超过 30 天的旧文件。
-
端口占用: 9002 (cobalt 9001 github-下载Youtube视频 算是对齐)
主要组件:
5个文件
-
主程序 (app.py)
-
文件上传 (
用户上传图片或视频文件,应用程序会保存文件到upload.html
):uploads/
文件夹。 -
处理页面 (
文件上传后,用户会跳转到一个页面,显示文件转换过程的实时进度。processing.html
):
FFmpeg 的输出会实时显示在网页上,供用户查看转换进度。 -
下载页面 (
转换完成后,用户可以直接在页面中预览图片或视频。download.html
):
用户可以选择下载文件或上传其他文件进行转换。 -
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 容器化,确保它可以在不同的环境中一致运行
-
Dockerfile:用于构建 Flask 应用的容器,并安装所有依赖项,包括 FFmpeg。
Dockerfile 的关键部分:
ffmpeg
:确保在容器内安装 FFmpeg。
上面介绍的5+3 -
容器名称:
2mp4jpg
便于识别管理与项目同名
用户流程:
- 上传文件:用户访问首页并上传图片或视频文件。
- 文件转换:上传后,用户会跳转到转换进度页面,实时查看转换状态。
- 文件下载:转换完成后,用户会进入下载页面,在该页面可以预览或下载已转换的文件。
实现效果
上面是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 应用程序。os
、uuid
:用于处理文件路径和生成唯一文件名。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_nvenc
和h264_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