Skip to content

Instantly share code, notes, and snippets.

@zhsso
Forked from sunny00123/liverecord.groovy
Created September 11, 2018 06:22
Show Gist options
  • Save zhsso/0feb3fce5c00328fdc572cf126d6a0a4 to your computer and use it in GitHub Desktop.
Save zhsso/0feb3fce5c00328fdc572cf126d6a0a4 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 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