Last active
October 29, 2019 16:07
-
-
Save sunny00123/4e69283b930f0f3a36244237797be9d0 to your computer and use it in GitHub Desktop.
recording of bilibili live streams
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env groovy | |
import java.util.concurrent.Executors | |
import java.util.concurrent.TimeUnit | |
def OPTIONS = [ | |
UID : 276904, // B站UID | |
ROOMID : 131985, // 直播间的房间编号,不是地址编号 | |
OUTPUTDIR : "/home/live", // 录制文件输出目录,/D:\ffmpeg\bin/ | |
FFMPEG : "/usr/bin/ffmpeg", // ffmpeg可执行程序位置,/D:\ffmpeg\bin\ffmpeg.exe/ | |
CHECK_INTERVAL: 60, // 直播检测线程的间隔,单位:秒 | |
SPLIT_INTERVAL: 60 * 5 // 录制多长时间分割一次,防止ffmpeg录制出错时无法检测到,单位:秒 | |
] | |
def scheduledExecutorService = Executors.newSingleThreadScheduledExecutor() | |
scheduledExecutorService.scheduleWithFixedDelay(new Recorder(OPTIONS), 0, 1, TimeUnit.SECONDS) | |
class Recorder implements Runnable { | |
def UID | |
def ROOMID | |
def OUTPUTDIR | |
def FFMPEG | |
int CHECK_INTERVAL | |
int SPLIT_INTERVAL | |
Recorder() { | |
Runtime.runtime.addShutdownHook { | |
iscancle = true | |
quitFFmpeg() | |
println "${logtime()} 已退出运行" | |
} | |
} | |
volatile Process process | |
volatile boolean isliving = false | |
volatile boolean isrecord = false | |
volatile boolean iscancle = false | |
volatile int retry = 0 // 重试计时器 | |
volatile int check = 0 // 检测计时器 | |
volatile int split = 0 // 分割计时器 | |
def headers = ["User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", | |
"DNT" : "1"] | |
@Override | |
void run() { | |
try { | |
if (check >= CHECK_INTERVAL) { | |
check = 0 | |
} | |
if (check == 0) { | |
def islivingurl = "http://live.bilibili.com/bili/isliving/$UID?callback=isliving".toURL().getText(requestProperties: headers) | |
isliving = !islivingurl.contains(/"data":""/) | |
} | |
check++ | |
if (isliving) { // 检测到正在直播 | |
if (process == null && !isrecord) { // 当前没有ffmpeg进程 | |
isrecord = true | |
Thread thread = new Thread({ // 创建一个线程运行ffmpeg,防止阻塞检测线程 | |
def playurl = "http://live.bilibili.com/api/playurl?player=1&cid=$ROOMID&quality=0".toURL().getText(requestProperties: headers) | |
def matcher = playurl =~ /<url><!\[CDATA\[(.+)]]><\/url>/ | |
if (matcher.find()) { | |
retry = 0 | |
println "${logtime()} 正在直播中" | |
println "${logtime()} 开始录制了" | |
def url = matcher.group 1 | |
def date = Calendar.getInstance().format("yyyy-MM-dd-HH-mm-ss") | |
String[] command = [FFMPEG, | |
"-y", | |
"-i", "$url", | |
"-c", "copy", | |
"-f", "mp4", | |
"${OUTPUTDIR}${File.separator}${date}.mp4"] | |
process = command.execute() | |
def sc = new Scanner(process.errorStream) | |
def p = Pattern.compile("frame=.*") | |
def frame | |
while (null != (frame = sc.findWithinHorizon(p as Pattern, 0))) { | |
println frame | |
} | |
if (split != 0 && !iscancle) { | |
println "${logtime()} 直播流可能中断,重启录制" | |
split = 0 | |
quitFFmpeg() | |
} | |
} else { | |
println "${logtime()} 无法获取直播流地址" | |
retry++ | |
if (retry == 10) { | |
println "${logtime()} 无法获取直播流地址,重试已达上限" | |
System.exit 1 | |
} | |
} | |
}) | |
thread.start() // 启动录制线程 | |
} else { // 当前正在录制,开始按时长分割,防止ffmpeg录制出错时无法检测到 | |
split++ | |
if (split >= SPLIT_INTERVAL) { | |
split = 0 | |
println "${logtime()} 触发按时长分段" | |
quitFFmpeg() | |
} | |
} | |
} else if (check == 1) { | |
if (process == null) { | |
println "${logtime()} 还没有直播" | |
} else { | |
println "${logtime()} 直播关闭了" | |
quitFFmpeg() | |
} | |
} | |
} catch (Throwable t) { | |
println "${logtime()} ${t.getMessage()}" | |
quitFFmpeg() | |
} | |
} | |
void quitFFmpeg() { | |
if (process != null) { | |
try { | |
while (process != null && process.alive) { // 防止q不掉ffmpeg | |
process.out.withWriter { writer -> | |
writer.write "q" | |
writer.flush() | |
} | |
println "${logtime()} 正在退出录制" | |
TimeUnit.SECONDS.sleep(3) | |
} | |
process = null | |
isrecord = false | |
println "${logtime()} 已退出录制" | |
} catch (Throwable t) { | |
println "${logtime()} ${t.getMessage()}" | |
} | |
} else { | |
println "${logtime()} 进程不存在" | |
} | |
} | |
static String logtime() { | |
return "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}]" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
BililiveRecorder/BililiveRecorder#66 (comment)