-
-
Save UnluckyNinja/fe63c946bc27d97f30e11e4516505d26 to your computer and use it in GitHub Desktop.
recording of bilibili live streams
This file contains hidden or 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 ARGS = [ | |
UID : "276904", // B站UID | |
ROOMID : "131985", // 直播间的房间编号,不是地址编号 | |
OUTPUTDIR : /F:\FLV\workground\ffmpeg\bin\output/, // 录制文件输出目录 | |
FFMPEG : /F:\FLV\workground\ffmpeg\bin\ffmpeg.exe/, // ffmpeg可执行程序位置 | |
CHECK_INTERVAL : 60, // 直播检测线程的调度间隔,单位:秒 | |
SPLIT_INTERVAL : 60 * 10, // 录制多长时间分割一次,防止ffmpeg录制出错时无法检测到,单位:秒 | |
] | |
def scheduledExecutorService = Executors.newSingleThreadScheduledExecutor() | |
scheduledExecutorService.scheduleWithFixedDelay(new WatchThread(ARGS), 0, 1, TimeUnit.SECONDS) | |
class WatchThread implements Runnable { | |
def UID | |
def ROOMID | |
def OUTPUTDIR | |
def FFMPEG | |
def CHECK_INTERVAL | |
def SPLIT_INTERVAL | |
WatchThread(){ | |
super() | |
Runtime.runtime.addShutdownHook { worker?.stop() } | |
} | |
@Lazy def worker = new RecordWorker(FFMPEG, ROOMID, OUTPUTDIR) | |
boolean isLiving = false | |
int livingCheck = 0 // 检测计时器 | |
int splitCheck = 0 // 分割计时器 | |
@Override | |
void run() { | |
if (shouldRecord()) { // 检测是否应该开始录制 | |
if (!worker.working) { // 当前没有ffmpeg进程 | |
worker.start() | |
} else { | |
checkAndSplit() // 当前正在录制,开始按时长分割,防止ffmpeg录制出错时无法检测到 | |
} | |
} | |
} | |
boolean shouldRecord(){ | |
if (livingCheck == CHECK_INTERVAL) { // overflow reset | |
livingCheck = 0 | |
} | |
if (livingCheck == 0) { // 60秒一检测API直播活动状态 | |
def isLvingURL = new URL("http://live.bilibili.com/bili/isliving/$UID?callback=isliving").text | |
isLiving = !isLvingURL.contains(/"data":""/) | |
} | |
if (!isLiving) { // 非直播状态下log直播情况 | |
if (!worker.working) { | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 还没有直播" | |
} else { | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 直播关闭了" | |
worker.stop() | |
} | |
} | |
livingCheck++ | |
return isLiving | |
} | |
def checkAndSplit(){ | |
splitCheck++ | |
if (splitCheck == SPLIT_INTERVAL) { | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 录播超出时长,分割中……" | |
splitCheck = 0 | |
worker.restart() | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 分割完毕" | |
} | |
} | |
} | |
class RecordWorker { | |
boolean working = false; | |
def workerThread | |
def FFMPEG | |
def ROOMID | |
def OUTPUTDIR | |
RecordWorker(FFMPEG, ROOMID, OUTPUTDIR){ | |
this.FFMPEG = FFMPEG | |
this.ROOMID = ROOMID | |
this.OUTPUTDIR = OUTPUTDIR | |
} | |
def start(){ | |
working = true | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 启动录制线程" | |
workerThread = new WorkerThread(FFMPEG, ROOMID, OUTPUTDIR) | |
workerThread.start() | |
} | |
def restart(){ | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 重启录制线程" | |
stop() | |
start() | |
} | |
def stop(){ | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 关闭录制线程" | |
if(!working){ | |
return false | |
} | |
workerThread.quitFFmpeg() | |
working = false | |
return true | |
} | |
class WorkerThread extends Thread{ | |
volatile Process process | |
volatile int retry = 0 | |
def FFMPEG | |
def ROOMID | |
def OUTPUTDIR | |
WorkerThread(FFMPEG, ROOMID, OUTPUTDIR){ | |
this.FFMPEG = FFMPEG | |
this.ROOMID = ROOMID | |
this.OUTPUTDIR = OUTPUTDIR | |
} | |
@Override | |
void run(){ | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 准备启动 FFMPEG" | |
def h5play = new URL("http://live.bilibili.com/api/playurl?player=1&cid=${ROOMID}&quality=0").text | |
def matcher = h5play =~ /<url><!\[CDATA\[(.+)\]\]><\/url>/ | |
if (matcher.find()) { | |
retry = 0 | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 正在直播中" | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 开始录制了" | |
def m3u8 = matcher.group 1 | |
String[] command = [FFMPEG, | |
"-i", "$m3u8", | |
"-c", "copy", | |
"-f", "flv", | |
"${OUTPUTDIR}${File.separator}${Calendar.getInstance().format("yyyy-MM-dd-HH-mm-ss")}.flv"] | |
process = command.execute() | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] FFMPEG 启动完毕,等待输出" | |
process.waitForProcessOutput System.out, System.err | |
} else { | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 无法获取直播流地址" | |
retry++ | |
if (retry == 10) { | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 无法获取直播流地址,重试已达上限" | |
System.exit 1 | |
} | |
} | |
} | |
void quitFFmpeg() { | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 正在关闭 FFMPEG" | |
if (process?.alive) { | |
process.out.withWriter{ writer -> | |
writer.write "q" | |
writer.flush() | |
} | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 已退出录制" | |
} else { | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 进程不存在" | |
} | |
println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] FFMPEG 已停止活动" | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment