Skip to content

Instantly share code, notes, and snippets.

@UnluckyNinja
Forked from sunny00123/liverecord.groovy
Last active August 24, 2016 05:51
Show Gist options
  • Save UnluckyNinja/fe63c946bc27d97f30e11e4516505d26 to your computer and use it in GitHub Desktop.
Save UnluckyNinja/fe63c946bc27d97f30e11e4516505d26 to your computer and use it in GitHub Desktop.
recording of bilibili live streams
#!/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