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
  • Select an option

  • Save UnluckyNinja/fe63c946bc27d97f30e11e4516505d26 to your computer and use it in GitHub Desktop.

Select an option

Save UnluckyNinja/fe63c946bc27d97f30e11e4516505d26 to your computer and use it in GitHub Desktop.

Revisions

  1. UnluckyNinja revised this gist Aug 24, 2016. 1 changed file with 67 additions and 52 deletions.
    119 changes: 67 additions & 52 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -24,7 +24,7 @@ class WatchThread implements Runnable {

    WatchThread(){
    super()
    Runtime.runtime.addShutdownHook { worker?.stopWorker() }
    Runtime.runtime.addShutdownHook { worker?.stop() }
    }

    @Lazy def worker = new RecordWorker(FFMPEG, ROOMID, OUTPUTDIR)
    @@ -36,9 +36,7 @@ class WatchThread implements Runnable {
    void run() {
    if (shouldRecord()) { // 检测是否应该开始录制
    if (!worker.working) { // 当前没有ffmpeg进程
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 正在启动录制线程"
    worker.start()
    worker.join()
    } else {
    checkAndSplit() // 当前正在录制,开始按时长分割,防止ffmpeg录制出错时无法检测到
    }
    @@ -58,7 +56,7 @@ class WatchThread implements Runnable {
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 还没有直播"
    } else {
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 直播关闭了"
    worker.stopWorker()
    worker.stop()
    }
    }
    livingCheck++
    @@ -77,12 +75,11 @@ class WatchThread implements Runnable {

    }

    class RecordWorker extends Thread{
    class RecordWorker {

    volatile Process process
    volatile int retry = 0
    boolean working = false;

    volatile boolean working = false;
    def workerThread

    def FFMPEG
    def ROOMID
    @@ -94,67 +91,85 @@ class RecordWorker extends Thread{
    this.OUTPUTDIR = OUTPUTDIR
    }

    @Override
    void run(){
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 准备启动 FFMPEG"
    def start(){
    working = true
    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
    }
    }
    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")}] 重启录制线程"
    stopWorker()
    this.start()
    this.join()
    stop()
    start()
    }

    def stopWorker(){
    def stop(){
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 关闭录制线程"
    if(!working){
    return false
    }
    quitFFmpeg()
    workerThread.quitFFmpeg()
    working = false
    return true
    }

    private synchronized setWorking(boolean flag){
    working = flag
    }
    class WorkerThread extends Thread{

    private 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()
    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")}] 已退出录制"
    } else {
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 进程不存在"
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] FFMPEG 已停止活动"
    }
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] FFMPEG 已停止活动"
    }
    }
  2. UnluckyNinja revised this gist Aug 20, 2016. 1 changed file with 129 additions and 76 deletions.
    205 changes: 129 additions & 76 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -2,106 +2,159 @@
    import java.util.concurrent.Executors
    import java.util.concurrent.TimeUnit

    def UID = "276904" // B站UID
    def ROOMID = "131985" // 直播间的房间编号,不是地址编号
    def OUTPUTDIR = /D:\ffmpeg\bin/ // 录制文件输出目录
    def FFMPEG = /D:\ffmpeg\bin\ffmpeg.exe/ // ffmpeg可执行程序位置
    def CHECKER = 60 // 直播检测线程的调度间隔,单位:秒
    def TRACKS = 60 * 10 // 录制多长时间分割一次,防止ffmpeg录制出错时无法检测到,单位:秒

    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 RecorderThread(UID, ROOMID, OUTPUTDIR, FFMPEG, CHECKER, TRACKS), 0, 1, TimeUnit.SECONDS)
    scheduledExecutorService.scheduleWithFixedDelay(new WatchThread(ARGS), 0, 1, TimeUnit.SECONDS)

    class RecorderThread implements Runnable {
    class WatchThread implements Runnable {

    def UID
    def ROOMID
    def OUTPUTDIR
    def FFMPEG
    def CHECKER
    def TRACKS
    def CHECK_INTERVAL
    def SPLIT_INTERVAL

    RecorderThread(UID, ROOMID, OUTPUTDIR, FFMPEG, CHECKER, TRACKS) {
    this.UID = UID
    this.ROOMID = ROOMID
    this.OUTPUTDIR = OUTPUTDIR
    this.FFMPEG = FFMPEG
    this.CHECKER = CHECKER
    this.TRACKS = TRACKS
    Runtime.runtime.addShutdownHook { quitFFmpeg() }
    WatchThread(){
    super()
    Runtime.runtime.addShutdownHook { worker?.stopWorker() }
    }

    volatile Process process
    volatile isliving = false
    volatile retry = 0
    volatile split = 0 // 分割计时器
    volatile check = 0 // 检测计时器
    @Lazy def worker = new RecordWorker(FFMPEG, ROOMID, OUTPUTDIR)
    boolean isLiving = false
    int livingCheck = 0 // 检测计时器
    int splitCheck = 0 // 分割计时器

    @Override
    void run() {
    if (check == 0) {
    def islivingurl = new URL("http://live.bilibili.com/bili/isliving/$UID?callback=isliving").text
    isliving = !islivingurl.contains(/"data":""/)
    if (shouldRecord()) { // 检测是否应该开始录制
    if (!worker.working) { // 当前没有ffmpeg进程
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 正在启动录制线程"
    worker.start()
    worker.join()
    } else {
    checkAndSplit() // 当前正在录制,开始按时长分割,防止ffmpeg录制出错时无法检测到
    }
    }
    check++
    if (check == CHECKER) {
    check = 0
    }

    boolean shouldRecord(){
    if (livingCheck == CHECK_INTERVAL) { // overflow reset
    livingCheck = 0
    }
    if (isliving) { // 检测到正在直播
    if (process == null) { // 当前没有ffmpeg进程
    Thread thread = new Thread({ // 创建一个线程运行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()
    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
    }
    }
    })
    thread.start() // 启动录制线程
    } else { // 当前正在录制,开始按时长分割,防止ffmpeg录制出错时无法检测到
    split++
    if (split == TRACKS) {
    split = 0
    quitFFmpeg()
    }
    }
    } else if (check == 1) {
    if (process == null) {
    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")}] 直播关闭了"
    quitFFmpeg()
    worker.stopWorker()
    }
    }
    livingCheck++
    return isLiving
    }

    void quitFFmpeg() {
    if (process != null) {
    if (process.alive) {
    def writer = new BufferedWriter(new OutputStreamWriter(process.out))
    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 extends Thread{

    volatile Process process
    volatile int retry = 0

    volatile boolean working = false;

    def FFMPEG
    def ROOMID
    def OUTPUTDIR

    RecordWorker(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"
    working = true
    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
    }
    }
    }

    def restart(){
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 重启录制线程"
    stopWorker()
    this.start()
    this.join()
    }

    def stopWorker(){
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 关闭录制线程"
    if(!working){
    return false
    }
    quitFFmpeg()
    working = false
    return true
    }

    private synchronized setWorking(boolean flag){
    working = flag
    }

    private 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")}] 进程不存在"
    }
    process = null
    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 已停止活动"
    }
    }
    }
  3. @sunny00123 sunny00123 revised this gist Aug 20, 2016. 1 changed file with 5 additions and 5 deletions.
    10 changes: 5 additions & 5 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -7,7 +7,7 @@ def ROOMID = "131985" // 直播间的房间编号,不是地址编号
    def OUTPUTDIR = /D:\ffmpeg\bin/ // 录制文件输出目录
    def FFMPEG = /D:\ffmpeg\bin\ffmpeg.exe/ // ffmpeg可执行程序位置
    def CHECKER = 60 // 直播检测线程的调度间隔,单位:秒
    def TRACKS = 60 * 15 // 录制多长时间分割一次,防止ffmpeg录制出错时无法检测到,单位:秒
    def TRACKS = 60 * 10 // 录制多长时间分割一次,防止ffmpeg录制出错时无法检测到,单位:秒

    def scheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
    scheduledExecutorService.scheduleWithFixedDelay(new RecorderThread(UID, ROOMID, OUTPUTDIR, FFMPEG, CHECKER, TRACKS), 0, 1, TimeUnit.SECONDS)
    @@ -50,8 +50,8 @@ class RecorderThread implements Runnable {
    if (isliving) { // 检测到正在直播
    if (process == null) { // 当前没有ffmpeg进程
    Thread thread = new Thread({ // 创建一个线程运行ffmpeg,防止阻塞监控线程
    def h5play = new URL("http://live.bilibili.com/api/h5playurl?roomid=$ROOMID").text
    def matcher = h5play =~ /"url":"(.+)"/
    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")}] 正在直播中"
    @@ -60,8 +60,8 @@ class RecorderThread implements Runnable {
    String[] command = [FFMPEG,
    "-i", "$m3u8",
    "-c", "copy",
    "-bsf:a", "aac_adtstoasc",
    "${OUTPUTDIR}${File.separator}${Calendar.getInstance().format("yyyy-MM-dd-HH-mm-ss")}.mp4"]
    "-f", "flv",
    "${OUTPUTDIR}${File.separator}${Calendar.getInstance().format("yyyy-MM-dd-HH-mm-ss")}.flv"]
    process = command.execute()
    process.waitForProcessOutput System.out, System.err
    } else {
  4. @sunny00123 sunny00123 revised this gist Aug 20, 2016. 1 changed file with 8 additions and 4 deletions.
    12 changes: 8 additions & 4 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -93,11 +93,15 @@ class RecorderThread implements Runnable {

    void quitFFmpeg() {
    if (process != null) {
    def writer = new BufferedWriter(new OutputStreamWriter(process.outputStream))
    writer.write "q"
    writer.flush()
    if (process.alive) {
    def writer = new BufferedWriter(new OutputStreamWriter(process.out))
    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")}] 进程不存在"
    }
    process = null
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 已退出录制"
    }
    }
    }
  5. @sunny00123 sunny00123 revised this gist Aug 20, 2016. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -58,7 +58,6 @@ class RecorderThread implements Runnable {
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 开始录制了"
    def m3u8 = matcher.group 1
    String[] command = [FFMPEG,
    "-re",
    "-i", "$m3u8",
    "-c", "copy",
    "-bsf:a", "aac_adtstoasc",
    @@ -93,7 +92,7 @@ class RecorderThread implements Runnable {
    }

    void quitFFmpeg() {
    if (process != null && process.outputStream != null) {
    if (process != null) {
    def writer = new BufferedWriter(new OutputStreamWriter(process.outputStream))
    writer.write "q"
    writer.flush()
  6. @sunny00123 sunny00123 revised this gist Aug 20, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -82,7 +82,7 @@ class RecorderThread implements Runnable {
    quitFFmpeg()
    }
    }
    } else {
    } else if (check == 1) {
    if (process == null) {
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 还没有直播"
    } else {
  7. @sunny00123 sunny00123 revised this gist Aug 20, 2016. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -10,7 +10,6 @@ def CHECKER = 60 // 直播检测线程的调度间隔,单位:秒
    def TRACKS = 60 * 15 // 录制多长时间分割一次,防止ffmpeg录制出错时无法检测到,单位:秒

    def scheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
    scheduledExecutorService.addShutdownHook { scheduledExecutorService.shutdown() }
    scheduledExecutorService.scheduleWithFixedDelay(new RecorderThread(UID, ROOMID, OUTPUTDIR, FFMPEG, CHECKER, TRACKS), 0, 1, TimeUnit.SECONDS)

    class RecorderThread implements Runnable {
  8. @sunny00123 sunny00123 revised this gist Aug 20, 2016. 1 changed file with 3 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    #!/usr/bin/env groovy
    import java.util.concurrent.Executors
    import java.util.concurrent.TimeUnit

    @@ -6,7 +7,7 @@ def ROOMID = "131985" // 直播间的房间编号,不是地址编号
    def OUTPUTDIR = /D:\ffmpeg\bin/ // 录制文件输出目录
    def FFMPEG = /D:\ffmpeg\bin\ffmpeg.exe/ // ffmpeg可执行程序位置
    def CHECKER = 60 // 直播检测线程的调度间隔,单位:秒
    def TRACKS = 60 * 30 // 录制多长时间分割一次,防止ffmpeg录制出错时无法检测到,单位:秒
    def TRACKS = 60 * 15 // 录制多长时间分割一次,防止ffmpeg录制出错时无法检测到,单位:秒

    def scheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
    scheduledExecutorService.addShutdownHook { scheduledExecutorService.shutdown() }
    @@ -93,7 +94,7 @@ class RecorderThread implements Runnable {
    }

    void quitFFmpeg() {
    if (process != null && process.alive) {
    if (process != null && process.outputStream != null) {
    def writer = new BufferedWriter(new OutputStreamWriter(process.outputStream))
    writer.write "q"
    writer.flush()
  9. @sunny00123 sunny00123 revised this gist Aug 20, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -5,7 +5,7 @@ def UID = "276904" // B站UID
    def ROOMID = "131985" // 直播间的房间编号,不是地址编号
    def OUTPUTDIR = /D:\ffmpeg\bin/ // 录制文件输出目录
    def FFMPEG = /D:\ffmpeg\bin\ffmpeg.exe/ // ffmpeg可执行程序位置
    def CHECKER = 30 // 直播检测线程的调度间隔,单位:秒
    def CHECKER = 60 // 直播检测线程的调度间隔,单位:秒
    def TRACKS = 60 * 30 // 录制多长时间分割一次,防止ffmpeg录制出错时无法检测到,单位:秒

    def scheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
  10. @sunny00123 sunny00123 revised this gist Aug 19, 2016. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -6,7 +6,7 @@ def ROOMID = "131985" // 直播间的房间编号,不是地址编号
    def OUTPUTDIR = /D:\ffmpeg\bin/ // 录制文件输出目录
    def FFMPEG = /D:\ffmpeg\bin\ffmpeg.exe/ // ffmpeg可执行程序位置
    def CHECKER = 30 // 直播检测线程的调度间隔,单位:秒
    def TRACKS = 60 * 30 // 录制多长时间分批一次,防止ffmpeg录制出错时无法检测到,单位:秒
    def TRACKS = 60 * 30 // 录制多长时间分割一次,防止ffmpeg录制出错时无法检测到,单位:秒

    def scheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
    scheduledExecutorService.addShutdownHook { scheduledExecutorService.shutdown() }
    @@ -75,7 +75,7 @@ class RecorderThread implements Runnable {
    }
    })
    thread.start() // 启动录制线程
    } else { // 当前正在录制,开始按时长分隔,防止ffmpeg录制出错时无法检测到
    } else { // 当前正在录制,开始按时长分割,防止ffmpeg录制出错时无法检测到
    split++
    if (split == TRACKS) {
    split = 0
  11. @sunny00123 sunny00123 revised this gist Aug 19, 2016. 1 changed file with 26 additions and 7 deletions.
    33 changes: 26 additions & 7 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -5,34 +5,48 @@ def UID = "276904" // B站UID
    def ROOMID = "131985" // 直播间的房间编号,不是地址编号
    def OUTPUTDIR = /D:\ffmpeg\bin/ // 录制文件输出目录
    def FFMPEG = /D:\ffmpeg\bin\ffmpeg.exe/ // ffmpeg可执行程序位置
    def DELAY = 30 // 直播检测线程的调度间隔,单位:秒
    def CHECKER = 30 // 直播检测线程的调度间隔,单位:秒
    def TRACKS = 60 * 30 // 录制多长时间分批一次,防止ffmpeg录制出错时无法检测到,单位:秒

    def scheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
    scheduledExecutorService.addShutdownHook { scheduledExecutorService.shutdown() }
    scheduledExecutorService.scheduleWithFixedDelay(new RecorderThread(UID, ROOMID, OUTPUTDIR, FFMPEG), 0, DELAY, TimeUnit.SECONDS)
    scheduledExecutorService.scheduleWithFixedDelay(new RecorderThread(UID, ROOMID, OUTPUTDIR, FFMPEG, CHECKER, TRACKS), 0, 1, TimeUnit.SECONDS)

    class RecorderThread implements Runnable {

    def UID
    def ROOMID
    def OUTPUTDIR
    def FFMPEG
    def CHECKER
    def TRACKS

    RecorderThread(UID, ROOMID, OUTPUTDIR, FFMPEG) {
    RecorderThread(UID, ROOMID, OUTPUTDIR, FFMPEG, CHECKER, TRACKS) {
    this.UID = UID
    this.ROOMID = ROOMID
    this.OUTPUTDIR = OUTPUTDIR
    this.FFMPEG = FFMPEG
    this.CHECKER = CHECKER
    this.TRACKS = TRACKS
    Runtime.runtime.addShutdownHook { quitFFmpeg() }
    }

    volatile Process process
    volatile retry = 0
    volatile isliving = false
    volatile retry = 0
    volatile split = 0 // 分割计时器
    volatile check = 0 // 检测计时器

    @Override
    void run() {
    def islivingurl = new URL("http://live.bilibili.com/bili/isliving/$UID?callback=isliving").text
    def isliving = !islivingurl.contains(/"data":""/)
    if (check == 0) {
    def islivingurl = new URL("http://live.bilibili.com/bili/isliving/$UID?callback=isliving").text
    isliving = !islivingurl.contains(/"data":""/)
    }
    check++
    if (check == CHECKER) {
    check = 0
    }
    if (isliving) { // 检测到正在直播
    if (process == null) { // 当前没有ffmpeg进程
    Thread thread = new Thread({ // 创建一个线程运行ffmpeg,防止阻塞监控线程
    @@ -48,7 +62,6 @@ class RecorderThread implements Runnable {
    "-i", "$m3u8",
    "-c", "copy",
    "-bsf:a", "aac_adtstoasc",
    // "-f", "flv",
    "${OUTPUTDIR}${File.separator}${Calendar.getInstance().format("yyyy-MM-dd-HH-mm-ss")}.mp4"]
    process = command.execute()
    process.waitForProcessOutput System.out, System.err
    @@ -62,6 +75,12 @@ class RecorderThread implements Runnable {
    }
    })
    thread.start() // 启动录制线程
    } else { // 当前正在录制,开始按时长分隔,防止ffmpeg录制出错时无法检测到
    split++
    if (split == TRACKS) {
    split = 0
    quitFFmpeg()
    }
    }
    } else {
    if (process == null) {
  12. @sunny00123 sunny00123 revised this gist Aug 19, 2016. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -44,12 +44,12 @@ class RecorderThread implements Runnable {
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 开始录制了"
    def m3u8 = matcher.group 1
    String[] command = [FFMPEG,
    "-re",
    "-i", "$m3u8",
    "-acodec", "copy",
    "-c", "copy",
    "-bsf:a", "aac_adtstoasc",
    "-vcodec", "copy",
    "-f", "flv",
    "${OUTPUTDIR}${File.separator}${Calendar.getInstance().format("yyyy-MM-dd-HH-mm-ss")}.flv"]
    // "-f", "flv",
    "${OUTPUTDIR}${File.separator}${Calendar.getInstance().format("yyyy-MM-dd-HH-mm-ss")}.mp4"]
    process = command.execute()
    process.waitForProcessOutput System.out, System.err
    } else {
  13. @sunny00123 sunny00123 revised this gist Aug 18, 2016. 1 changed file with 6 additions and 6 deletions.
    12 changes: 6 additions & 6 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -1,11 +1,11 @@
    import java.util.concurrent.Executors
    import java.util.concurrent.TimeUnit

    def UID = "276904" // B站用户UID
    def UID = "276904" // B站UID
    def ROOMID = "131985" // 直播间的房间编号,不是地址编号
    def OUTPUTDIR = /D:\ffmpeg\bin/ // 录制文件输出目录
    def FFMPEG = /D:\ffmpeg\bin\ffmpeg.exe/ // ffmpeg可执行程序位置
    def DELAY = 30 // 监控线程的间隔,单位:秒
    def DELAY = 30 // 直播检测线程的调度间隔,单位:秒

    def scheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
    scheduledExecutorService.addShutdownHook { scheduledExecutorService.shutdown() }
    @@ -33,9 +33,9 @@ class RecorderThread implements Runnable {
    void run() {
    def islivingurl = new URL("http://live.bilibili.com/bili/isliving/$UID?callback=isliving").text
    def isliving = !islivingurl.contains(/"data":""/)
    if (isliving) {
    if (process == null) {
    Thread thread = new Thread({
    if (isliving) { // 检测到正在直播
    if (process == null) { // 当前没有ffmpeg进程
    Thread thread = new Thread({ // 创建一个线程运行ffmpeg,防止阻塞监控线程
    def h5play = new URL("http://live.bilibili.com/api/h5playurl?roomid=$ROOMID").text
    def matcher = h5play =~ /"url":"(.+)"/
    if (matcher.find()) {
    @@ -61,7 +61,7 @@ class RecorderThread implements Runnable {
    }
    }
    })
    thread.start()
    thread.start() // 启动录制线程
    }
    } else {
    if (process == null) {
  14. @sunny00123 sunny00123 revised this gist Aug 18, 2016. No changes.
  15. @sunny00123 sunny00123 revised this gist Aug 18, 2016. 1 changed file with 71 additions and 57 deletions.
    128 changes: 71 additions & 57 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -3,69 +3,83 @@ import java.util.concurrent.TimeUnit

    def UID = "276904" // B站用户UID
    def ROOMID = "131985" // 直播间的房间编号,不是地址编号
    def OUTPUTDIR = /D:\Users\Sun\Desktop\ffmpeg\bin/ // 录制文件输出目录
    def FFMPEG = /D:\Users\Sun\Desktop\ffmpeg\bin\ffmpeg.exe/ // ffmpeg可执行程序位置
    def DELAY = 30 // SECONDS // 监控线程的间隔,当检测到直播时开启一个ffmpeg进程进行录制
    def OUTPUTDIR = /D:\ffmpeg\bin/ // 录制文件输出目录
    def FFMPEG = /D:\ffmpeg\bin\ffmpeg.exe/ // ffmpeg可执行程序位置
    def DELAY = 30 // 监控线程的间隔,单位:秒

    def scheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
    def counter = 0
    def isliving = false
    def isrecording = false
    Process process
    scheduledExecutorService.addShutdownHook { scheduledExecutorService.shutdown() }
    scheduledExecutorService.scheduleWithFixedDelay(new RecorderThread(UID, ROOMID, OUTPUTDIR, FFMPEG), 0, DELAY, TimeUnit.SECONDS)

    scheduledExecutorService.scheduleWithFixedDelay({
    def islivingurl = new URL("http://live.bilibili.com/bili/isliving/$UID").text
    isliving = !islivingurl.contains(/"data":""/)
    if (isliving) { // 开始直播了
    if (!isrecording) { // 如果没有录制,则开始录制
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 正在直播中"
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 开始录制了"
    isrecording = true
    Thread thread = new Thread({ // 开启一个守护线程去启动ffmpeg的进程防止阻塞监控线程
    def h5play = new URL("http://live.bilibili.com/api/h5playurl?roomid=$ROOMID").text
    def matcher = h5play =~ /"url":"(.+)"/
    if (matcher.find()) {
    def m3u8 = matcher.group(1)
    String[] command = [FFMPEG,
    "-i", "$m3u8",
    "-acodec", "copy",
    "-bsf:a", "aac_adtstoasc",
    "-vcodec", "copy",
    "-f", "mp4",
    "${Calendar.getInstance().format("yyyy-MM-dd-HH-mm-ss")}.mp4"]
    process = command.execute null, new File(OUTPUTDIR)
    process.addShutdownHook { // 监听ctrl+c的事件,退出时发送q到ffmpeg进程,避免出现 moov atom not found 错误
    quitFFmpeg process
    scheduledExecutorService.shutdown()
    }
    process.waitForProcessOutput System.out, System.err
    } else {
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 无法获取直播流地址"
    counter++
    if (counter == 10) { // 无法获取直播地址时,退出程序
    quitFFmpeg process
    scheduledExecutorService.shutdown()
    class RecorderThread implements Runnable {

    def UID
    def ROOMID
    def OUTPUTDIR
    def FFMPEG

    RecorderThread(UID, ROOMID, OUTPUTDIR, FFMPEG) {
    this.UID = UID
    this.ROOMID = ROOMID
    this.OUTPUTDIR = OUTPUTDIR
    this.FFMPEG = FFMPEG
    Runtime.runtime.addShutdownHook { quitFFmpeg() }
    }

    volatile Process process
    volatile retry = 0

    @Override
    void run() {
    def islivingurl = new URL("http://live.bilibili.com/bili/isliving/$UID?callback=isliving").text
    def isliving = !islivingurl.contains(/"data":""/)
    if (isliving) {
    if (process == null) {
    Thread thread = new Thread({
    def h5play = new URL("http://live.bilibili.com/api/h5playurl?roomid=$ROOMID").text
    def matcher = h5play =~ /"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",
    "-acodec", "copy",
    "-bsf:a", "aac_adtstoasc",
    "-vcodec", "copy",
    "-f", "flv",
    "${OUTPUTDIR}${File.separator}${Calendar.getInstance().format("yyyy-MM-dd-HH-mm-ss")}.flv"]
    process = command.execute()
    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
    }
    }
    }
    })
    thread.setDaemon(true)
    thread.start()
    }
    } else { // 未检测到直播
    if (process == null || !process.alive) { // 如果当前没有ffmpeg的进程,则没有直播
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 还没有直播"
    } else { // 如果当前有ffmpeg的进程,发送q到ffmpeg使其退出
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 直播关闭了"
    quitFFmpeg process
    isrecording = false
    })
    thread.start()
    }
    } else {
    if (process == null) {
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 还没有直播"
    } else {
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 直播关闭了"
    quitFFmpeg()
    }
    }
    }
    }, 0, DELAY, TimeUnit.SECONDS)

    static void quitFFmpeg(Process process) {
    if (process.alive) {
    def writer = new BufferedWriter(new OutputStreamWriter(process.outputStream))
    writer.write("q")
    writer.flush()
    void quitFFmpeg() {
    if (process != null && process.alive) {
    def writer = new BufferedWriter(new OutputStreamWriter(process.outputStream))
    writer.write "q"
    writer.flush()
    process = null
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 已退出录制"
    }
    }
    }
  16. @sunny00123 sunny00123 revised this gist Aug 14, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -52,7 +52,7 @@ scheduledExecutorService.scheduleWithFixedDelay({
    thread.start()
    }
    } else { // 未检测到直播
    if (!process.alive) { // 如果当前没有ffmpeg的进程,则没有直播
    if (process == null || !process.alive) { // 如果当前没有ffmpeg的进程,则没有直播
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 还没有直播"
    } else { // 如果当前有ffmpeg的进程,发送q到ffmpeg使其退出
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 直播关闭了"
  17. @sunny00123 sunny00123 created this gist Aug 14, 2016.
    71 changes: 71 additions & 0 deletions liverecord.groovy
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,71 @@
    import java.util.concurrent.Executors
    import java.util.concurrent.TimeUnit

    def UID = "276904" // B站用户UID
    def ROOMID = "131985" // 直播间的房间编号,不是地址编号
    def OUTPUTDIR = /D:\Users\Sun\Desktop\ffmpeg\bin/ // 录制文件输出目录
    def FFMPEG = /D:\Users\Sun\Desktop\ffmpeg\bin\ffmpeg.exe/ // ffmpeg可执行程序位置
    def DELAY = 30 // SECONDS // 监控线程的间隔,当检测到直播时开启一个ffmpeg进程进行录制

    def scheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
    def counter = 0
    def isliving = false
    def isrecording = false
    Process process

    scheduledExecutorService.scheduleWithFixedDelay({
    def islivingurl = new URL("http://live.bilibili.com/bili/isliving/$UID").text
    isliving = !islivingurl.contains(/"data":""/)
    if (isliving) { // 开始直播了
    if (!isrecording) { // 如果没有录制,则开始录制
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 正在直播中"
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 开始录制了"
    isrecording = true
    Thread thread = new Thread({ // 开启一个守护线程去启动ffmpeg的进程防止阻塞监控线程
    def h5play = new URL("http://live.bilibili.com/api/h5playurl?roomid=$ROOMID").text
    def matcher = h5play =~ /"url":"(.+)"/
    if (matcher.find()) {
    def m3u8 = matcher.group(1)
    String[] command = [FFMPEG,
    "-i", "$m3u8",
    "-acodec", "copy",
    "-bsf:a", "aac_adtstoasc",
    "-vcodec", "copy",
    "-f", "mp4",
    "${Calendar.getInstance().format("yyyy-MM-dd-HH-mm-ss")}.mp4"]
    process = command.execute null, new File(OUTPUTDIR)
    process.addShutdownHook { // 监听ctrl+c的事件,退出时发送q到ffmpeg进程,避免出现 moov atom not found 错误
    quitFFmpeg process
    scheduledExecutorService.shutdown()
    }
    process.waitForProcessOutput System.out, System.err
    } else {
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 无法获取直播流地址"
    counter++
    if (counter == 10) { // 无法获取直播地址时,退出程序
    quitFFmpeg process
    scheduledExecutorService.shutdown()
    }
    }
    })
    thread.setDaemon(true)
    thread.start()
    }
    } else { // 未检测到直播
    if (!process.alive) { // 如果当前没有ffmpeg的进程,则没有直播
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 还没有直播"
    } else { // 如果当前有ffmpeg的进程,发送q到ffmpeg使其退出
    println "[${Calendar.getInstance().format("yyyy-MM-dd HH:mm:ss")}] 直播关闭了"
    quitFFmpeg process
    isrecording = false
    }
    }
    }, 0, DELAY, TimeUnit.SECONDS)

    static void quitFFmpeg(Process process) {
    if (process.alive) {
    def writer = new BufferedWriter(new OutputStreamWriter(process.outputStream))
    writer.write("q")
    writer.flush()
    }
    }