python实现的B站直播录制工具
作者:Redlnn 发布时间:2023-05-29 00:51:48
标签:python,直播,录制,b站
目录
项目地址:
前言
使用方式
主要代码
blive_record.py
config.py(配置文件)
项目地址:
https://github.com/Redlnn/blive_record
前言
作者: Red_lnn
不允许将本项目运用于非法以及违反B站用户协议的用途
仅支持单个主播,多个主播请复制多份并分开单独启动
运行时如要停止录制并退出,请按键盘 Ctrl+C
如要修改录制设置,请以纯文本方式打开.py文件
利用 FFmpeg 直接抓取主播推送的流,无需打开浏览器
有新功能需求请直接提 Pull requests
建议录制为 flv 格式(默认),以防止意外中断导致录制文件损坏,若要进行剪辑可使用 FFmpeg 转换为 mp4 文件后再倒入到剪辑软件(使用 FFmpeg 转换 flv 为 mp4 : ffmpeg -i {input}.flv -c:v copy -c:a copy {output}.mp4)
使用方式
1.安装 Python(>=3.7) 并设置环境变量
2.打开终端或命令行进入本脚本所在目录
3.通过 pip 安装必须的第三方库
Windows:
pip install -r requirements.txt
Linux:
python3 -m pip install -r requirements.txt
4.下载 ffmpeg 并正确设置环境变量(下载地址)
5.Windows 直接双击运行start.bat
6.Linux 先运行 chmod +x start.sh 再运行 ./start.sh
主要代码
blive_record.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
"""
*--------------------------------------*
B站直播录播姬 By: Red_lnn
仅支持单个主播,多个主播请复制多份并分开单独启动
运行时如要停止录制并退出,请按键盘 Ctrl+C
如要修改录制设置,请以纯文本方式打开.py文件
利用ffmpeg直接抓取主播推送的流,不需要打开浏览器
*--------------------------------------*
"""
# import ffmpy3 # noqa
import logging
import os
import signal
import sys
import threading
import time
import traceback
from json import loads
from logging import handlers
from subprocess import PIPE, Popen, STDOUT
import requests
from regex import match
# 导入配置
from config import * # noqa
record_status = False # 录制状态,True为录制中
kill_times = 0 # 尝试强制结束FFmpeg的次数
logging.addLevelName(15, 'FFmpeg') # 自定义FFmpeg的日志级别
logger = logging.getLogger('Record')
logger.setLevel(logging.DEBUG)
fms = '[%(asctime)s %(levelname)s] %(message)s'
# datefmt = "%Y-%m-%d %H:%M:%S"
datefmt = "%H:%M:%S"
default_handler = logging.StreamHandler(sys.stdout)
if debug:
default_handler.setLevel(logging.DEBUG)
elif verbose:
default_handler.setLevel(15)
else:
default_handler.setLevel(logging.INFO)
default_handler.setFormatter(logging.Formatter(fms, datefmt=datefmt))
logger.addHandler(default_handler)
if save_log:
# file_handler = logging.FileHandler("debug.log", mode='w+', encoding='utf-8')
if not os.path.exists(os.path.join('logs')):
os.mkdir(os.path.join('logs'))
file_handler = handlers.TimedRotatingFileHandler(os.path.join('logs', 'debug.log'), 'midnight', encoding='utf-8')
if debug:
default_handler.setLevel(logging.DEBUG)
else:
default_handler.setLevel(15)
file_handler.setFormatter(logging.Formatter(fms, datefmt=datefmt))
logger.addHandler(file_handler)
def get_timestamp() -> int:
"""
获取当前时间戳
"""
return int(time.time())
def get_time() -> str:
"""
获取格式化后的时间
"""
time_now = get_timestamp()
time_local = time.localtime(time_now)
dt = time.strftime("%Y%m%d_%H%M%S", time_local)
return dt
def record():
"""
录制过程中要执行的检测与判断
"""
global p, record_status, last_record_time, kill_times # noqa
while True:
line = p.stdout.readline().decode()
p.stdout.flush()
logger.log(15, line.rstrip())
if match('video:[0-9kmgB]* audio:[0-9kmgB]* subtitle:[0-9kmgB]*', line) or 'Exiting normally' in line:
record_status = False # 如果FFmpeg正常结束录制则退出本循环
break
elif match('frame=[0-9]', line) or 'Opening' in line:
last_record_time = get_timestamp() # 获取最后录制的时间
elif 'Failed to read handshake response' in line:
time.sleep(5) # FFmpeg读取m3u8流失败,等个5s康康会不会恢复
continue
time_diff = get_timestamp() - last_record_time # 计算上次录制到目前的时间差
if time_diff >= 65:
logger.error('最后一次录制到目前已超65s,将尝试发送终止信号')
logger.debug(f'间隔时间:{time_diff}s')
kill_times += 1
p.send_signal(signal.SIGTERM) # 若最后一次录制到目前已超过65s,则认为FFmpeg卡死,尝试发送终止信号
time.sleep(0.5)
if kill_times >= 3:
logger.critical('由于无法结束FFmpeg进程,将尝试自我了结')
sys.exit(1)
if 'Immediate exit requested' in line:
logger.info('FFmpeg已被强制结束')
break
if p.poll() is not None: # 如果FFmpeg已退出但没有被上一个判断和本循环第一个判断捕捉到,则当作异常退出
logger.error('ffmpeg未正常退出,请检查日志文件!')
record_status = False
break
def main():
global p, room_id, record_status, last_record_time, kill_times # noqa
while True:
record_status = False
while True:
logger.info('------------------------------')
logger.info(f'正在检测直播间:{room_id}')
try:
room_info = requests.get(f'https://api.live.bilibili.com/room/v1/Room/get_info?room_id={room_id}',
timeout=5)
except (requests.exceptions.ReadTimeout, requests.exceptions.Timeout, requests.exceptions.ConnectTimeout):
logger.error(f'无法连接至B站API,等待{check_time}s后重新开始检测')
time.sleep(check_time)
continue
live_status = loads(room_info.text)['data']['live_status']
if live_status == 1:
break
elif live_status == 0:
logger.info(f'没有开播,等待{check_time}s重新开始检测')
time.sleep(check_time)
if not os.path.exists(os.path.join('download')):
try:
os.mkdir(os.path.join('download'))
except: # noqa
logger.error(f'无法创建下载文件夹 ↓\n{traceback.format_exc()}')
sys.exit(1)
if os.path.isfile(os.path.join('download')):
logger.error('存在与下载文件夹同名的文件')
sys.exit(1)
logger.info('正在直播,准备开始录制')
m3u8_list = requests.get(
f'https://api.live.bilibili.com/xlive/web-room/v1/playUrl/playUrl?cid={room_id}&platform=h5&qn=10000')
m3u8_address = loads(m3u8_list.text)['data']['durl'][0]['url']
# 下面命令中的timeout单位为微秒,10000000us为10s(https://www.cnblogs.com/zhifa/p/12345376.html)
command = ['ffmpeg', '-rw_timeout', '10000000', '-timeout', '10000000', '-listen_timeout', '10000000',
'-headers',
'"Accept: */*? Accept-Encoding: gzip, deflate, br? Accept-Language: zh,zh-TW;q=0.9,en-US;q=0.8,en;'
f'q=0.7,zh-CN;q=0.6,ru;q=0.5? Origin: https://live.bilibili.com/{room_id}? '
'User-Agent: Mozilla/5.0 (Windows NT 10.0;Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36?"', '-i',
m3u8_address, '-c:v', 'copy', '-c:a', 'copy', '-bsf:a', 'aac_adtstoasc',
'-f', 'segment', '-segment_time', str(segment_time), '-segment_start_number', '1',
os.path.join('download', f'[{room_id}]_{get_time()}_part%03d.{file_extensions}'), '-y']
if debug:
logger.debug('FFmpeg命令如下 ↓')
command_str = ''
for _ in command:
command_str += _
logger.debug(command_str)
p = Popen(command, stdin=PIPE, stdout=PIPE, stderr=STDOUT, shell=False)
record_status = True
start_time = last_record_time = get_timestamp()
try:
t = threading.Thread(target=record)
t.start()
while True:
if not record_status:
break
if verbose or debug:
time.sleep(20)
logger.info(f'--==>>> 已录制 {round((get_timestamp() - start_time) / 60, 2)} 分钟 <<<==--')
else:
time.sleep(60)
logger.info(f'--==>>> 已录制 {int((get_timestamp() - start_time) / 60)} 分钟 <<<==--')
if not record_status:
break
except KeyboardInterrupt:
# p.send_signal(signal.CTRL_C_EVENT)
logger.info('停止录制,等待ffmpeg退出后本程序会自动退出')
logger.info('若长时间卡住,请再次按下ctrl+c (可能会损坏视频文件)')
logger.info('Bye!')
sys.exit(0)
kill_times = 0
logger.info('FFmpeg已退出,重新开始检测直播间')
# time.sleep(check_time)
if __name__ == '__main__':
logger.info('B站直播录播姬 By: Red_lnn')
logger.info('如要停止录制并退出,请按键盘 Ctrl+C')
logger.info('如要修改录制设置,请以纯文本方式打开.py文件')
logger.info('准备开始录制...')
time.sleep(0.3)
try:
main()
except KeyboardInterrupt:
logger.info('Bye!')
sys.exit(0)
config.py(配置文件)
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
"""
*------------以下为可配置项-------------*
"""
# room_id = 1151716 # 莴苣某人
# room_id = 1857249 # Red_lnn
room_id = 1151716 # 要录制的B站直播间的直播间ID
segment_time = 3600 # 录播分段时长(单位:秒)
check_time = 60 # 开播检测间隔(单位:秒)
file_extensions = 'flv' # 录制文件后缀名(文件格式)
verbose = True # 是否打印ffmpeg输出信息到控制台
debug = False # 是否显示并保存调试信息(优先级高于 verbose)
save_log = True # 是否保存日志信息为文件,同一天多次启动本脚本会共用同一个日志文件,每天凌晨分割一次日志文件
"""
*------------以上为可配置项-------------*
"""
来源:https://github.com/Redlnn/blive_record


猜你喜欢
- 当一个函数进行完成后需要重定向到一个带参数的urlURLpath('peopleapply/<int:jobid>/
- 本文实例讲述了PHP+redis实现添加处理投票的方法。分享给大家供大家参考,具体如下:<?php header("Cont
- 插件下载:blueideasearch.xpi首先第一步 说一下怎么样查看firefox插件的源码, 就我上边写的那个东西,把它下载下来.将
- 题目描述原题链接 :66. 加一给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。最高位数字存放在数组的首位, 数组
- 前言所需要安装的库有:pip install opencv-pythonpip install matplotlibPython接口帮助文档
- 伴随着时间的增长,公司的数据库会越来越多,查询速度也会越来越慢。打开数据库看到几十万条的数据,查询起来难免不废时间。要提升SQL的查询效能,
- 整体思路将要备份的目录列为一个列表,通过执行系统命令,进行压缩、备份。这样关键在于构造命令并使用 os.system( )来执行,一开始使用
- 1 减少HTTP请求数量 (Minimize HTTP Requests) tag:content80%的用户响应时间被花费在前端
- 前言最近在搞 Python 课程设计,想要搞一个好看的 UI,惊艳全班所有人。但打开 Qt Creator,Win7 风格的复古的按钮是在让
- 假设通过爬虫你获取到了北京2016年3,10月份每天白天的最高气温(分别位于列表a,b),那么此时如何寻找出气温随时间(天)变化的某种规律?
- 一、代码示例 window.open(url,'新窗口','width='+(window.screen.a
- 前言当后台返回的数据过多时,我们就要配置分页器,比如一页最多只能展示10条等等,drf中默认配置了3个分页面PageNumberPagina
- 有时你会发现你写的视图函数是十分类似的,只有一点点的不同。 比如说,你有两个视图,它们的内容是一致的,除了它们所用的模板不太一样:# url
- 1.最基本的作为一个本本分分的函数声明使用。 function func(){} 或 var func=function(){}; 2.作为
- 在将数据库从MSSQL迁移到MySQL的过程中,基于业务逻辑的要求,需要在MySQL的自增列插入0值。在MSSQL中是这样完成的: stri
- 网上讨论的文章已经很多了,这里举一个简单的例子来讨论一下 Composition API 的用法,具体问题才好具体讨论嘛。假如我们要做一个论
- 参数strSQL 要导出的SQL查询语句strFields 字段名称列表,如果为空字符,则使用SQL语句中的字段名用法示例:1:export
- SQLite数据库使用单个磁盘文件,并且不需要像Oracle、MSSQL、MySQL等数据库管理系统那样启动服务,使用非常灵活方便。但是SQ
- 本文实例为大家分享了python访问者模式代码,供大家参考,具体内容如下"""访问者模式""
- 本文实例讲述了PHP实现从上往下打印二叉树的方法。分享给大家供大家参考,具体如下:问题从上往下打印出二叉树的每个节点,同层节点从左至右打印。