# -*- coding: utf-8 -*-
# !/usr/bin/env python
# Software: PyCharm
# __author__ == "YU HAIPENG"
# fileName: VideoHelp.py
# Month: 五月
# time: 2021/5/22 17:06
"""
https://ffmpeg.org/ffmpeg-filters.html 文档
https://blog.csdn.net/weixin_42081389/article/details/100543007
ffmpeg -y -i 待转换mp4文件路径 -vcodec copy -acodec copy -vbsf h264_mp4toannexb 目标ts文件
ffmpeg -i 待转换ts文件路径 -c copy -map 0 -f segment -segment_list 目标m3u8文件 -segment_time 单个切片时长 目标ts切片文件名称
通用选项
-L license
-h 帮助
-fromats 显示可用的格式,编解码的,协议的。。。
-f fmt 强迫采用格式fmt
-I filename 输入文件
-y 覆盖输出文件
-t duration 设置纪录时间 hh:mm:ss[.xxx]格式的记录时间也支持
-ss position 搜索到指定的时间 [-]hh:mm:ss[.xxx]的格式也支持
-title string 设置标题
-author string 设置作者
-copyright string 设置版权
-comment string 设置评论
-target type 设置目标文件类型(vcd,svcd,dvd) 所有的格式选项(比特率,编解码以及缓冲区大小)自动设置 ,只需要输入如下的就可以了:
ffmpeg -i myfile.avi -target vcd /tmp/vcd.mpg
-hq 激活高质量设置
-itsoffset offset 设置以秒为基准的时间偏移,该选项影响所有后面的输入文件。
该偏移被加到输入文件的时戳,定义一个正偏移意味着相应的流被延迟了 offset秒。 [-]hh:mm:ss[.xxx]的格式也支持
视频选项
-b bitrate 设置比特率,缺省200kb/s
-r fps 设置帧频 缺省25
-s size 设置帧大小 格式为WXH 缺省160X128.下面的简写也可以直接使用:
Sqcif 128X96 qcif 176X144 cif 252X288 4cif 704X576
-aspect aspect 设置横纵比 4:3 16:9 或 1.3333 1.7777
-croptop size 设置顶部切除带大小 像素单位
-cropbottom size –cropleft size –cropright size
-padtop size 设置顶部补齐的大小 像素单位
-padbottom size –padleft size –padright size –padcolor color 设置补齐条颜色(hex,6个16进制的数,红:绿:兰排列,比如 000000代表黑色)
-vn 不做视频记录
-bt tolerance 设置视频码率容忍度kbit/s
-maxrate bitrate设置最大视频码率容忍度
-minrate bitreate 设置最小视频码率容忍度
-bufsize size 设置码率控制缓冲区大小
-vcodec codec 强制使用codec编解码方式。 如果用copy表示原始编解码数据必须被拷贝。
-sameq 使用同样视频质量作为源(VBR)
-pass n 选择处理遍数(1或者2)。两遍编码非常有用。第一遍生成统计信息,第二遍生成精确的请求的码率
-passlogfile file 选择两遍的纪录文件名为file
高级视频选项
-g gop_size 设置图像组大小
-intra 仅适用帧内编码
-qscale q 使用固定的视频量化标度(VBR)
-qmin q 最小视频量化标度(VBR)
-qmax q 最大视频量化标度(VBR)
-qdiff q 量化标度间最大偏差 (VBR)
-qblur blur 视频量化标度柔化(VBR)
-qcomp compression 视频量化标度压缩(VBR)
-rc_init_cplx complexity 一遍编码的初始复杂度
-b_qfactor factor 在p和b帧间的qp因子
-i_qfactor factor 在p和i帧间的qp因子
-b_qoffset offset 在p和b帧间的qp偏差
-i_qoffset offset 在p和i帧间的qp偏差
-rc_eq equation 设置码率控制方程 默认tex^qComp
-rc_override override 特定间隔下的速率控制重载
-me method 设置运动估计的方法 可用方法有 zero phods log x1 epzs(缺省) full
-dct_algo algo 设置dct的算法 可用的有 0 FF_DCT_AUTO 缺省的DCT 1 FF_DCT_FASTINT 2 FF_DCT_INT 3 FF_DCT_MMX 4 FF_DCT_MLIB 5 FF_DCT_ALTIVEC
-idct_algo algo 设置idct算法。可用的有 0 FF_IDCT_AUTO 缺省的IDCT 1 FF_IDCT_INT 2 FF_IDCT_SIMPLE 3 FF_IDCT_SIMPLEMMX 4 FF_IDCT_LIBMPEG2MMX 5 FF_IDCT_PS2 6 FF_IDCT_MLIB 7 FF_IDCT_ARM 8 FF_IDCT_ALTIVEC 9 FF_IDCT_SH4 10 FF_IDCT_SIMPLEARM
-er n 设置错误残留为n 1 FF_ER_CAREFULL 缺省 2 FF_ER_COMPLIANT 3 FF_ER_AGGRESSIVE 4 FF_ER_VERY_AGGRESSIVE
-ec bit_mask 设置错误掩蔽为bit_mask,该值为如下值的位掩码 1 FF_EC_GUESS_MVS (default=enabled) 2 FF_EC_DEBLOCK (default=enabled)
-bf frames 使用frames B 帧,支持mpeg1,mpeg2,mpeg4
-mbd mode 宏块决策 0 FF_MB_DECISION_SIMPLE 使用mb_cmp 1 FF_MB_DECISION_BITS 2 FF_MB_DECISION_RD
-4mv 使用4个运动矢量 仅用于mpeg4
-part 使用数据划分 仅用于mpeg4
-bug param 绕过没有被自动监测到编码器的问题
-strict strictness 跟标准的严格性
-aic 使能高级帧内编码 h263+
-umv 使能无限运动矢量 h263+
-deinterlace 不采用交织方法
-interlace 强迫交织法编码 仅对mpeg2和mpeg4有效。当你的输入是交织的并且你想要保持交织以最小图像损失的时候采用该选项。可选的方法是不交织,但是损失更大
-psnr 计算压缩帧的psnr
-vstats 输出视频编码统计到vstats_hhmmss.log
-vhook module 插入视频处理模块 module 包括了模块名和参数,用空格分开
音频选项
-ab bitrate 设置音频码率
-ar freq 设置音频采样率
-ac channels 设置通道 缺省为1
-an 不使能音频纪录
-acodec codec 使用codec编解码
音频/视频捕获选项
-vd device 设置视频捕获设备。比如/dev/video0
-vc channel 设置视频捕获通道 DV1394专用
-tvstd standard 设置电视标准 NTSC PAL(SECAM)
-dv1394 设置DV1394捕获
-av device 设置音频设备 比如/dev/dsp
高级选项
-map file:stream 设置输入流映射
-debug 打印特定调试信息
-benchmark 为基准测试加入时间
-hex 倾倒每一个输入包
-bitexact 仅使用位精确算法 用于编解码测试
-ps size 设置包大小,以bits为单位
-re 以本地帧频读数据,主要用于模拟捕获设备
-loop 循环输入流。只工作于图像流,用于ffserver测试
# todo example
1、avi转MP4命令:
ffmpeg -i .\\Video.avi -c copy -map 0 video.mp4
或
ffmpeg -i .\\Video.avi -c:v libx264 -crf 19 -preset slow -c:a aac -b:a 192k -ac 2 video.mp4
//剪切视频
ffmpeg -ss 0:1:30 -t 0:0:50 -i 1.avi -vcodec copy -acodec copy 3.mp4
//-r 提取图像的频率,-ss 开始时间,-t 持续时间
MP4转ts
ffmpeg -i .\video.mp4 output.ts
视频压缩
1)ffmpeg -i 123_ffmpeg.mp4 (压缩的文件更大更清晰,一般情况下不用)
2)ffmpeg.exe -i 123.MP4 -b:v 700k 1231_ffmpeg.mp4(压缩的更小,相对模糊一些)
"""
import os
from re import compile
import filetype
import platform
import subprocess
import requests
from requests.utils import requote_uri
import time
import shutil
from concurrent.futures import ThreadPoolExecutor, as_completed
from requests.packages.urllib3.exceptions import InsecureRequestWarning
__all__ = ["main_path", "get_desktop", "get_home",
"OtherFormatToMp4", "SliceM3u8ToMp4", "M3u8ToMp4",
"MP4ToM3u8", "MP4ToTs"
]
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
split_reg = compile(r'[\\|/]')
reg = compile(r".*%0+\dd\.ts")
center_num = 40
def main_path(path: str):
"""
路径总函数
@param path:
@return:
"""
if not isinstance(path, str):
raise ValueError("path err")
current_path = os.getcwd()
if path.startswith('..'):
path = _wne_path(_parse(path, current_path))
elif path.startswith('.'):
path = _wne_path(path[1:], current_path)
elif path:
path = _wne_path(path)
else:
raise ValueError('文件路径错误')
return path
def _parse(path: str, current_path):
"""
解析 .. 路径
:param path:
:param current_path:
:return:
"""
new_path_args = list(filter(lambda x: x != '', _get_path_params(path)))
row = 0
while row < len(new_path_args):
if new_path_args[row] == '..':
current_path = os.path.dirname(current_path)
new_path_args.remove(new_path_args[row])
row -= 1
else:
break
row += 1
return os.path.join(current_path, *new_path_args)
def _wne_path(new_path: str, current_path=None):
new_path_args = _get_path_params(new_path)
if current_path:
path = os.path.join(current_path, *new_path_args)
else:
sys_str = get_sys()
if sys_str == "Windows":
if new_path_args[0].find(':') != -1:
new_path_args[0] += os.sep
path = os.path.join(*new_path_args)
elif sys_str in ["Linux", "Mac", "Darwin"]:
if new_path_args[0] == '':
new_path_args[0] = os.sep
path = os.path.join(*new_path_args)
else:
path = new_path
return path
def get_sys():
"""
平台
@return:
"""
sys_str = platform.system()
return sys_str
def get_home(path='~'):
"""
家目录
@return:
"""
return os.path.expanduser(path)
def _get_path_params(path):
"""
路径参数
@param path:
@return:
"""
return split_reg.split(path)
def get_desktop():
"""
桌面路径
@return:
"""
return get_home(f"~{os.sep}DeskTop")
def guss_file_type(file_path):
"""
file_path
@param file_path:
@return:
"""
kind = filetype.guess_extension(file_path)
return kind
def m3u8_to_mp4_slice_download(m3u8_urls: list, sep=100, **kwargs):
"""
分片下载
@param m3u8_urls: urls 数组 过大时可用分片下载 传入的 m3u8_urls必须排好序 果ts文件名 或 ts 文件路径
@param sep:
@param kwargs: M3u8ToMp4 参数
@return:
"""
n = 1
mp4_path_name = kwargs.pop("mp4_path_name", None) or "temp"
if mp4_path_name.endswith(".mp4"):
mp4_path_name = os.path.splitext(mp4_path_name)[0]
m3u_pt, save_mp4_pt = (None, None)
for start in range(0, len(m3u8_urls), sep):
new_list = m3u8_urls[start:start + sep]
m3u_pt, save_mp4_pt = M3u8ToMp4(
new_list,
mp4_path_name=f"{mp4_path_name}{str(n).zfill(5)}.mp4",
**kwargs).run()
n += 1
return m3u_pt, save_mp4_pt
def get_log(self):
if self.logger is None:
import logging
logging.basicConfig(
format='PROCESS ID:%(process)d: %(asctime)s-%(name)s-%(levelname)s '
'-[line:%(lineno)d]: %(message)s', level=logging.INFO)
logger = logging.getLogger('video_log')
self.logger = logger
class OtherFormatToMp4:
"""
别的格式转mp4
todo 请先安装 ffmpeg 程序
"""
def __init__(
self,
origin_video_path,
save_mp4_path,
logger=None,
change_timeout=None):
"""
@param origin_video_path: 原视频路径 ../xx or ./x or abs_path
@param save_mp4_path: 生成文件路径
@param change_timeout: 生成文件路径
@param logger
"""
self.origin_video_path = main_path(origin_video_path)
self.save_mp4_path = main_path(save_mp4_path)
self.logger = logger
self.change_timeout = 180 if change_timeout is None else change_timeout
get_log(self)
def run(self):
return self.to_mp4()
def to_mp4(self):
try:
params = self.get_change_params()
self.logger.info("change start %s", self.origin_video_path)
self.__change_to_mp4(params)
self.logger.info("change end %s", self.origin_video_path)
finally:
for handler in self.logger.handlers:
if hasattr(handler, "close"):
handler.close() # noqa
self.logger.handlers.clear()
self.logger.info("over")
def get_change_params(self):
if not self.save_mp4_path.endswith(".mp4"):
self.save_mp4_path = f"{main_path(self.save_mp4_path)}.mp4"
self.make_folder(self.save_mp4_path)
cmd = F"ffmpeg -y -i {self.origin_video_path} " \
F"-vcodec copy -acodec copy {self.save_mp4_path}"
return cmd
def __change_to_mp4(self, cmd):
log_info = "other video to mp4 %s %s path %s"
self.logger.info(
log_info,
"start",
'--'.center(center_num, '-'),
cmd.rsplit("-acodec copy")[-1]
)
subprocess.check_output(cmd, shell=True, timeout=self.change_timeout)
self.logger.info(
log_info,
"end",
'--'.center(center_num, '-'),
cmd.rsplit("-acodec copy")[-1]
)
@staticmethod
def make_folder(save_mp4_path):
a, b = os.path.split(save_mp4_path)
if not a:
return
if not os.path.isdir(a):
os.makedirs(a)
class SliceM3u8ToMp4:
# todo 命令太长导致错误,所以分片下载数据
def __init__(
self,
m3u8_path,
# 请求相关 --------
request_bool=False,
req_headers=None, req_method='GET',
req_params: dict = None, req_data: dict = None,
req_before_func=None, req_after_func=None,
req_repeat_times=10, req_sleep=None,
# 解密相关 --------
decrypt_params: dict = None,
decrypt_callback=None,
decrypt_decode_before_func=None,
decrypt_decode_after_func=None,
# ---------------
thread_pool=None,
logger=None,
sort_func=None,
mp4_path_name=None,
change_timeout=None,
debug=False,
sep=50,
sep_mp4=50
# ---------------
):
self.m3u8_path = m3u8_path
if 0 > sep or sep > 100:
sep = 50
self.params = dict(
request_bool=request_bool,
decrypt_params=decrypt_params,
decrypt_callback=decrypt_callback,
decrypt_decode_before_func=decrypt_decode_before_func,
decrypt_decode_after_func=decrypt_decode_after_func,
req_headers=req_headers,
req_method=req_method,
req_params=req_params,
req_data=req_data,
req_before_func=req_before_func,
req_after_func=req_after_func,
req_repeat_times=req_repeat_times,
req_sleep=req_sleep,
thread_pool=thread_pool,
logger=logger,
sort_func=sort_func,
mp4_path_name=mp4_path_name,
change_timeout=change_timeout,
debug=debug,
sep=sep
)
self.sep_mp4 = sep_mp4
if self.sep_mp4 < 2:
self.sep_mp4 = 2
elif sep_mp4 > 100:
self.sep_mp4 = 50
self.__t_temp_save = None
def run(self):
return self.to_mp4()
@property
def t_temp_save(self):
if self.__t_temp_save is None:
self.__t_temp_save = f".{os.sep}temp_save_{self.get_uuid}"
return self.__t_temp_save
def to_mp4(self):
if not self.m3u8_path or not isinstance(self.m3u8_path, list):
return
for x in self.m3u8_path: # type:str
if not x.startswith('http'):
raise ValueError("must be url %s" % x)
ori_mp4_name = self.params['mp4_path_name']
if not ori_mp4_name:
ori_mp4_name = f".{os.sep}temp_{self.get_uuid}.mp4"
self.params['mp4_path_name'] = f"{self.t_temp_save}" \
f"{os.sep}" \
f"{os.path.split(ori_mp4_name)[-1]}"
m3u8_path, mp4_path_name = m3u8_to_mp4_slice_download(
self.m3u8_path,
**self.params
)
dirname = os.path.split(mp4_path_name)[0]
MP4ToTs(logger=self.params['logger']).run(True, dirname)
self.save_mp3(dirname, ori_mp4_name)
def mp3_slice_synthesis(self, dirname, dir_list, temp_mp4_save):
name = dir_list[0]
temp_mp4 = main_path(f"./temp_{self.get_uuid}")
for x in dir_list:
self.move_file(os.path.join(dirname, x), os.path.join(temp_mp4, x))
M3u8ToMp4(
temp_mp4, thread_pool=self.params['thread_pool'] or 30, change_timeout=60 * 30,
logger=self.params['logger'],
mp4_path_name=os.path.join(temp_mp4_save, os.path.splitext(name)[0]),
sort_func=self.params['sort_func'], debug=self.params['debug']
).run(remove_ts=True)
def save_mp3(self, dirname, ori_mp4_name):
mp3_list = os.listdir(dirname)
if len(os.listdir(dirname)) > self.sep_mp4:
temp_mp4_save = main_path(f"./temp_{self.get_uuid}_save")
for i in range(0, len(mp3_list), self.sep_mp4):
self.mp3_slice_synthesis(dirname, mp3_list[i: i + self.sep_mp4], temp_mp4_save)
MP4ToTs(logger=self.params['logger']).run(True, temp_mp4_save)
for x in os.listdir(temp_mp4_save):
self.move_file(os.path.join(temp_mp4_save, x), os.path.join(dirname, x))
os.rmdir(temp_mp4_save)
return self.save_mp3(dirname, ori_mp4_name)
else:
M3u8ToMp4(
dirname, thread_pool=self.params['thread_pool'] or 30, change_timeout=60 * 30,
mp4_path_name=ori_mp4_name, logger=self.params['logger'],
sort_func=self.params['sort_func'], debug=self.params['debug']
).run(remove_ts=True)
@staticmethod
def move_file(src_file, dst_file):
if os.path.isfile(src_file):
f_path, _ = os.path.split(dst_file) # 分离文件名和路径
if not os.path.exists(f_path):
os.makedirs(f_path) # 创建路径
shutil.move(src_file, dst_file) # 移动文
@property
def get_uuid(self):
import uuid
if hasattr(self, "_uuid"):
return self._uuid
self._uuid = uuid.uuid4().hex[:7] # noqa
return self._uuid
class M3u8ToMp4:
"""ts_文件 m3u8文件转mp4"""
def __init__(
self,
m3u8_path,
request_bool=False,
decrypt_params: dict = None,
decrypt_callback=None,
decrypt_decode_before_func=None,
decrypt_decode_after_func=None,
req_headers=None,
req_method='GET',
req_params: dict = None,
req_data: dict = None,
req_before_func=None,
req_after_func=None,
thread_pool=None,
logger=None,
sort_func=None,
mp4_path_name=None,
change_timeout=None,
req_sleep=None,
debug=False,
req_repeat_times=10,
write_error_info: bool = False
):
"""
todo 请先安装 ffmpeg 程序 (本转化为调用ffmpeg程序转化)
@param m3u8_path: [url, url] or folder
todo if m3u8_path is urls -> request_bool must be True urls must be sort
todo 如果 m3u8_path url集合长度大于10000, 最好分片下载 调用 m3u8_to_mp4_slice_download
@param request_bool: 让本程序发送请求 m3u8_path 必须是完整的 url 集合 bool
@param req_method: request_bool 为 true 生效 req 参数 - > dict
@param req_params: request_bool 为 true 生效 req 参数 - > dict
@param req_data: request_bool 为 true 生效 req 参数 - > dict
@param req_headers: 请求头参数
@param req_before_func request_bool=True 生效
入参 req_params, req_data 必须返回处理后的 req_params, req_data
@param req_after_func request_bool=True 生效
入参 请求后的返回对象 response 必须返回处理后的 response
@param decrypt_params: 解密函数所要的参数 字典格式
@param decrypt_callback: 解密函数 必须返回 bytes 解码后的数据
@param decrypt_decode_before_func: request_bool=True 生效
解密前的调用的函数 入参 data, index(当前请求标识), decrypt_params 必须返回 data, kwargs (解密参数)
@param decrypt_decode_after_func: request_bool=True 生效
解密后的调用的函数 入参 解密后的 data 必须返回处理后的 data
@param thread_pool: request_bool 为 true 时启用的线程池个数
@param sort_func: ts文件的 排序规则 返回排好序的list 默认排序sort
@param mp4_path_name: 转化后mp4的存储路径 xxx/xxx.mp4
@param logger: 日志记录器
@param change_timeout: 超时设置 默认 360 s
@param req_repeat_times: req 重复请求次数
@param write_error_info: 错误文件内容写入文本
"""
self.change_timeout = 360 if change_timeout is None else change_timeout
if not request_bool:
if not isinstance(m3u8_path, str):
raise FileNotFoundError(f"this path m3u8_path is not folder")
m3u8_path = main_path(m3u8_path)
assert ' ' not in m3u8_path
if not os.path.isdir(m3u8_path):
raise FileNotFoundError(f"this path m3u8_path is not folder")
self.m3u8_path = m3u8_path
self.decrypt_callback = decrypt_callback
self.decrypt_params = decrypt_params
self.sort_func = sort_func
self.req_sleep = req_sleep
self.logger = logger
self.mp4_path_name = mp4_path_name.replace(
" ", '') if mp4_path_name else mp4_path_name
self.req_headers = req_headers or {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36"
}
self._remove_ts = False
self.__ts_len = None
self.request_bool = request_bool
self.thread_pool = thread_pool
self.req_method = req_method
self.req_params = req_params
self.req_data = req_data
self.decrypt_decode_before_func = decrypt_decode_before_func
self.decrypt_decode_after_func = decrypt_decode_after_func
self.req_before_func = req_before_func
self.req_after_func = req_after_func
self.__debug = debug
self.req_repeat_times = req_repeat_times
self.__write_error_info = bool(write_error_info)
get_log(self)
def req(self, remove_ts=None):
if not self.m3u8_path or not isinstance(self.m3u8_path, list):
raise ValueError(f"this m3u8_path is not list or empty")
self.logger.info("req start %s", "--".center(center_num, '-'))
self._remove_ts = True if remove_ts is None else remove_ts
if self.__debug is True:
self.m3u8_path = self.m3u8_path[:1]
self._remove_ts = False
temp_path = main_path(f"./temp_{self.get_uuid}")
self.logger.info("temp_path %s", temp_path)
if not os.path.isdir(temp_path):
os.makedirs(temp_path)
with ThreadPoolExecutor(self.thread_pool) as thread:
all_task = [
thread.submit(
self._requests,
index,
url) for index, url in enumerate(
self.m3u8_path,
1)]
all_t = list()
for future in as_completed(all_task):
data, index, _res_url = future.result()
if callable(self.decrypt_callback):
new_decrypt_params = self.decrypt_params.copy() if self.decrypt_params else dict()
try:
if callable(self.decrypt_decode_before_func):
data, new_decrypt_params = self.decrypt_decode_before_func(
data, index, new_decrypt_params)
data = self.decrypt_callback(data, **new_decrypt_params)
if callable(self.decrypt_decode_after_func):
data = self.decrypt_decode_after_func(data)
except Exception as e:
self.logger.error("url requests error %s", _res_url)
all_t.append(thread.submit(self._save, data, index, temp_path, error={
"e": str(e),
'error_url': _res_url
}))
continue
all_t.append(thread.submit(self._save, data, index, temp_path))
for a in as_completed(all_t):
self.logger.info(f"file %s download %s" % a.result())
self.m3u8_path = temp_path
def _save(self, data, index, temp_path, error=None):
if self.__ts_len is None:
self.__ts_len = 1
name = self.get_ts_name(index)
if error:
if self.__write_error_info is False:
return 'no path', "ok"
name = name.rstrip(".ts") + ".txt"
ts_path = os.path.join(temp_path, name)
with open(ts_path, mode='wb') as f:
f.write(data)
if error:
_num_ = 10 + len(error['error_url'])
fm_t = f"{'-'.center(_num_, '-')}".encode()
f.write(b'\r\n')
f.write(b'\r\n')
f.write(fm_t)
f.write(b'\r\n---')
f.write("***-err info-***".center(_num_ - 6, ' ').encode())
f.write(b'---\r\n---')
f.write(error['e'].center(_num_ - 6, ' ').encode())
f.write(b'---\r\n---')
f.write(error['error_url'].center(_num_ - 6, ' ').encode())
f.write(b'---\r\n---')
f.write("***-err info end-***".center(_num_ - 6, ' ').encode())
f.write(b'---\r\n')
f.write(fm_t)
self.logger.info("save ts name %s", name)
return os.path.basename(ts_path), "ok"
def get_ts_name(self, index: int):
return f"{self.get_uuid}_{str(index).zfill(7)}.ts"
@property
def get_uuid(self):
import uuid
if hasattr(self, "_uuid"):
return self._uuid
self._uuid = uuid.uuid4().hex[:7] # noqa
return self._uuid
def _requests(self, index, url, connection_times=0, read_timeout_times=0):
with requests.session() as session:
session.headers.update(self.req_headers)
params = self.req_params.copy() if self.req_params else dict()
data = self.req_data.copy() if self.req_data else dict()
if callable(self.req_before_func):
params, data = self.req_before_func(params, data)
self.logger.info("request url %s start", url)
req_func = getattr(session, self.req_method.lower())
if self.req_sleep and isinstance(self.req_sleep, (int, float)):
time.sleep(self.req_sleep)
try:
res = req_func(
requote_uri(url),
verify=False,
timeout=30,
data=data,
params=params
)
except requests.exceptions.ConnectionError as e:
self.logger.info("ConnectionError %s", connection_times + 1)
if connection_times > self.req_repeat_times:
raise e
time.sleep(1)
return self._requests(index, url, connection_times=connection_times + 1)
except requests.exceptions.ReadTimeout as e:
self.logger.info("ReadTimeout %s", read_timeout_times + 1)
if read_timeout_times > self.req_repeat_times:
raise e
time.sleep(1)
return self._requests(index, url, read_timeout_times=read_timeout_times + 1)
except requests.exceptions.ChunkedEncodingError as e:
self.logger.info("网络发生变化 %s", read_timeout_times + 1)
if read_timeout_times > self.req_repeat_times:
raise e
time.sleep(5)
return self._requests(index, url, read_timeout_times=read_timeout_times + 1)
self.logger.info("request url %s end", url)
if callable(self.req_after_func):
res = self.req_after_func(res)
return res.content, index, res.url
def run(self, **kwargs):
return self.to_mp4(**kwargs)
def to_mp4(self, **kwargs):
self.logger.info("start %s", "--".center(center_num, '-'))
try:
remove_ts = kwargs.pop("remove_ts", None)
if self.request_bool:
self.req(remove_ts)
else:
self._remove_ts = remove_ts or False
self.parse()
finally:
if self.logger:
for handler in self.logger.handlers:
if hasattr(handler, "close"):
handler.close() # noqa
self.logger.handlers.clear()
self.logger.info("end %s", "--".center(center_num, '-'))
return self.m3u8_path, self.mp4_path_name
def parse(self):
ts_path_list = []
if self.__ts_len is None:
if callable(self.sort_func):
ts_path_list = self.sort_func(ts_path_list)
else:
ts_path_list = self.default_sort_func
else:
ts_path_list = self.default_sort_func
self.__change_to_mp4(ts_path_list)
@property
def default_sort_func(self):
return list(
filter(
lambda x: x.endswith(".ts"),
sorted(
os.listdir(
self.m3u8_path))))
def __change_to_mp4(self, ts_path_list):
p = None
if self.mp4_path_name is None:
self.mp4_path_name = main_path(
os.path.join(
os.path.dirname(self.m3u8_path),
f"{os.path.basename(self.m3u8_path)}_{self.get_uuid}.mp4"
)
)
self.logger.info(f"mp4_path_name {self.mp4_path_name}")
else:
if not self.mp4_path_name.endswith(".mp4"):
self.mp4_path_name = f"{main_path(self.mp4_path_name)}.mp4"
self.mp4_path_name = main_path(self.mp4_path_name)
p, file_name = os.path.split(self.mp4_path_name)
if p:
if not os.path.isdir(p):
try:
os.makedirs(p)
except FileExistsError:
pass
if file_name == ".mp4":
file_name = f"{self.get_uuid}{file_name}"
self.mp4_path_name = os.path.join(p, file_name)
self.logger.info(f"mp4_path_name {self.mp4_path_name}")
if not ts_path_list:
if p and not os.listdir(p):
os.rmdir(p)
if not os.listdir(self.m3u8_path):
os.rmdir(self.m3u8_path)
self.logger.error("ts file is empty ")
return
# todo cmd 中不能带有 ffmpeg 不能解析的空格
cmd = self._get_cmd(self.m3u8_path, ts_path_list, self.mp4_path_name)
try:
# todo 转化开始
subprocess.check_output(
cmd, shell=True, timeout=self.change_timeout)
finally:
if self._remove_ts:
self.logger.info("原始文件删除中 %s", '--'.center(center_num, '-'))
for n in ts_path_list:
os.unlink(os.path.join(self.m3u8_path, n))
if not os.listdir(self.m3u8_path):
os.rmdir(self.m3u8_path)
self.logger.info("原始文件删除成功 %s", '--'.center(center_num, '-'))
return
@staticmethod
def _get_cmd(path, name_list, mp4_path_name, to_encoding=None):
if not to_encoding:
if get_sys() == "Windows":
to_encoding = "gbk"
else:
to_encoding = 'utf8'
out_list = 'concat:'
for n in name_list:
out_list += os.path.join(path,
n).encode(to_encoding).decode(to_encoding) + '|'
cmd = f'ffmpeg -i "{out_list.rstrip("|")}" -y -acodec copy -vcodec copy -absf aac_adtstoasc' \
f' {mp4_path_name}'
return cmd
class MP4ToM3u8:
def __init__(
self,
ts_time=5,
change_timeout=30,
logger=None,
*,
encrypt=False,
key_len=16
):
"""
@param ts_time: ts 切片时长
@param change_timeout: 转换超时时长
@param encrypt:
@param key_len:
"""
self.ts_time = ts_time
self.change_timeout = change_timeout
self._enc_info_path = "enc.key"
self.key_len = key_len
self.encrypt = encrypt
self.logger = logger
get_log(self)
@staticmethod
def __get_key(length=16):
b = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'
import random
return ''.join(random.sample(b, length)).encode('utf8')
def run(self, *args, **kwargs):
"""
路径中空格不要出现
@param args:
@param kwargs:
@return:
"""
return self.mp4_to_m3u8(*args, **kwargs)
def mp4_to_m3u8(
self,
mp4_path: str,
save_m3u8_path=None,
ts_time=None,
cmd=''):
"""
@param mp4_path: mp4 路径
@param save_m3u8_path: 保存
@param ts_time:
@param cmd:
@return:
"""
assert ' ' not in mp4_path
if save_m3u8_path:
assert ' ' not in save_m3u8_path
if ".m3u8" not in save_m3u8_path:
tp_a, tp_b = os.path.split(save_m3u8_path)
old = save_m3u8_path
save_m3u8_path = os.path.join(tp_a, tp_b, f"{tp_b}.m3u8")
else:
tp_a, tp_b = os.path.splitext(save_m3u8_path)
old = tp_a
tp_a, tp_b = os.path.split(tp_a)
save_m3u8_path = os.path.join(tp_a, tp_b, f"{tp_b}.m3u8")
if not os.path.isdir(old):
os.makedirs(old)
self.logger.info("start to m3u8 %s", '--'.center(center_num, '-'))
mp4_path = main_path(mp4_path)
kind = guss_file_type(mp4_path)
create_dir = False
if kind != "mp4":
raise ValueError("file not mp4")
if not save_m3u8_path:
create_dir = True
save_m3u8_path = os.path.splitext(mp4_path)[0]
if not save_m3u8_path.endswith(".m3u8"):
save_m3u8_path += ".m3u8"
if create_dir:
if not os.path.isdir(save_m3u8_path):
os.makedirs(save_m3u8_path)
save_m3u8_path = os.path.join(
save_m3u8_path, os.path.split(save_m3u8_path)[-1])
save_ts_file_path = os.path.splitext(save_m3u8_path)[0]
else:
save_ts_file_path = save_m3u8_path
if not cmd:
if not save_ts_file_path.endswith(".ts"):
if save_ts_file_path.endswith(".m3u8"):
save_ts_file_path = os.path.splitext(save_m3u8_path)[0]
save_ts_file_path += ".ts"
f_path, f_name = os.path.split(save_ts_file_path)
if not reg.search(f_name):
f_na_li = list(os.path.splitext(f_name))
f_na_li.insert(1, "%07d")
f_name = ''.join(f_na_li)
del f_na_li
save_ts_file_path = os.path.join(f_path, f_name)
del f_name
del f_path
cmd = F"ffmpeg -i " \
F"{mp4_path} " \
F"-f segment -segment_time " \
F"{ts_time or self.ts_time} " \
F"-segment_format mpegts -segment_list " \
F"{save_m3u8_path} " \
F"-c copy -bsf:v h264_mp4toannexb -map 0 " \
F"{save_ts_file_path} -y"
if os.path.exists(save_m3u8_path):
return
# todo 转化开始
subprocess.check_output(
cmd, shell=True, timeout=self.change_timeout)
if self.encrypt:
with open(os.path.join(os.path.split(save_m3u8_path)[0], self._enc_info_path), 'w') as f:
f.write(f"key:{self.__get_key(self.key_len)}\n")
f.write(f"iv:{self.__get_key()}\n")
self.logger.info("start to m3u8 end %s", '--'.center(center_num, '-'))
class MP4ToTs:
def __init__(self, change_timeout=None, thread_pool=None, logger=None):
self.change_timeout = change_timeout or 60 * 30
self.save_ts_path = []
self.thread_pool = thread_pool or 30
self.logger = logger
self._remove_ts = False
get_log(self)
def run(self, remove_ts=False, *args, **kwargs):
self._remove_ts = remove_ts
return self.mp4_to_ts(*args, **kwargs)
def get_mp4_list(self, folder, save_ts_path, video_format='.mp4'):
"""
@param folder:
@param save_ts_path:
@param video_format:
@return:
"""
li = []
for index, file in enumerate(os.listdir(folder), 1):
if os.path.splitext(file)[-1] != video_format:
continue
if save_ts_path:
a, b = os.path.splitext(save_ts_path)
self.save_ts_path.append(f"{a}{str(index).zfill(3)}{b}")
else:
a = os.path.splitext(file)[0]
self.save_ts_path.append(f"{a}.ts")
li.append(file)
return li
def mp4_to_ts(self, mp4_path: str, save_ts_path=None):
"""
@param mp4_path:
@param save_ts_path:
@return:
"""
assert ' ' not in mp4_path
if save_ts_path:
assert ' ' not in save_ts_path
if ".ts" not in save_ts_path:
save_ts_path += '.ts'
self.logger.info("start to ts %s", '--'.center(center_num, '-'))
mp4_path = main_path(mp4_path)
if os.path.isdir(mp4_path):
mp4_path_list = self.get_mp4_list(mp4_path, save_ts_path)
else:
if not save_ts_path:
self.save_ts_path.append(os.path.splitext(mp4_path)[0] + ".ts")
else:
self.save_ts_path.append(save_ts_path)
mp4_path_list = [mp4_path]
with ThreadPoolExecutor(self.thread_pool) as thread:
thread_list = []
for index, mp4_file in enumerate(mp4_path_list):
mp4_path_file = os.path.join(mp4_path, mp4_file)
kind = guss_file_type(mp4_path_file)
if kind != "mp4":
raise ValueError("file not mp4")
save_ts_file_path = self.save_ts_path[index]
folder_file = os.path.split(save_ts_file_path)[0]
if not save_ts_path and not folder_file:
save_ts_file_path = os.path.join(mp4_path, save_ts_file_path)
folder_file = os.path.split(save_ts_file_path)[0]
if folder_file and not os.path.isdir(folder_file):
os.makedirs(folder_file)
cmd = F"ffmpeg -y -i {mp4_path_file} " \
F"-vcodec copy -acodec copy -vbsf " \
F"h264_mp4toannexb {save_ts_file_path}"
if os.path.exists(save_ts_file_path):
continue
# todo 转化开始
_t_ = thread.submit(
self.__change,
cmd,
save_ts_file_path,
shell=True,
timeout=self.change_timeout
)
thread_list.append(_t_)
for k in as_completed(thread_list):
self.logger.info("file save ok %s ", k.result())
if self._remove_ts:
for mp4_file in mp4_path_list:
mp4_path_file = os.path.join(mp4_path, mp4_file)
os.unlink(mp4_path_file)
if not os.listdir(mp4_path):
os.rmdir(mp4_path)
self.logger.info("start to ts end %s", '--'.center(center_num, '-'))
@staticmethod
def __change(cmd, mp4_path_file, **kwargs):
subprocess.check_output(cmd, **kwargs)
return mp4_path_file