init
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
// 部分数据和函数定义
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/orzogc/acfundanmu"
|
||||
)
|
||||
|
||||
type control int
|
||||
|
||||
// 控制信息
|
||||
const (
|
||||
startCycle control = iota
|
||||
stopCycle
|
||||
quit
|
||||
)
|
||||
|
||||
// 主播的管道信息
|
||||
type controlMsg struct {
|
||||
s streamer
|
||||
c control
|
||||
liveID string
|
||||
}
|
||||
|
||||
// 主播信息
|
||||
type streamerInfo struct {
|
||||
ch chan controlMsg // 控制信息
|
||||
modify bool
|
||||
}
|
||||
|
||||
// 一段录播视频:直播流意外中断后会重新开一段
|
||||
type recordSegment struct {
|
||||
file string // 视频文件路径
|
||||
startNs int64 // 这一段开始录制的时刻,纳秒 Unix
|
||||
}
|
||||
|
||||
// 直播信息
|
||||
type liveInfo struct {
|
||||
LiveID string // 直播 ID
|
||||
uid int // 主播的 uid
|
||||
isDanmu bool // 是否正在录制直播弹幕
|
||||
danmuCancel context.CancelFunc // 用来停止录制弹幕
|
||||
assFile string // 弹幕文件路径
|
||||
cfg acfundanmu.SubConfig
|
||||
isRecording bool // 是否正在录制直播视频
|
||||
recordCancel context.CancelFunc // 用来停止录制视频(中断当前分段)
|
||||
recordStop context.CancelFunc // 用来结束整个录制循环,避免停止后又被当成意外中断而重启
|
||||
ffmpegStdin io.WriteCloser // ffmpeg 的 stdin
|
||||
recordFile string // 视频文件路径
|
||||
firstRecordFile string // 第一个分段的视频文件路径(与弹幕文件同名配对,用于自动合并)
|
||||
recordStartNs int64 // 第一个分段开始录制的时刻,纳秒 Unix(用于计算弹幕与录播的时间偏移)
|
||||
recordSegments []recordSegment // 本场录播的所有分段(中断重连会产生新的分段),合并时全部参与
|
||||
danmuDone bool // 弹幕录制是否已经结束
|
||||
recordDone bool // 视频录制是否已经结束
|
||||
danmuStarted bool // 本场是否启动过弹幕录制
|
||||
recordStarted bool // 本场是否启动过视频录制
|
||||
danmuStopped bool // 弹幕是否被用户手动停止(手动停止不触发自动合并)
|
||||
recordStopped bool // 视频是否被用户手动停止
|
||||
mergeStarted bool // 自动合并是否已经启动
|
||||
doneAt int64 // 最近一次录制结束的时刻,纳秒 Unix(用于延迟清理这条记录)
|
||||
resumeUntil int64 // 断流后等待直播恢复的截止时刻,纳秒 Unix;0 表示没在等待
|
||||
}
|
||||
|
||||
// streamerInfo 的 map
|
||||
var sInfoMap struct {
|
||||
sync.Mutex
|
||||
info map[int]*streamerInfo
|
||||
}
|
||||
|
||||
// liveInfo 的 map
|
||||
var lInfoMap struct {
|
||||
sync.RWMutex
|
||||
info map[string]liveInfo
|
||||
}
|
||||
|
||||
// 设备 ID
|
||||
var deviceID string
|
||||
|
||||
var (
|
||||
exeDir string // 运行程序所在文件夹
|
||||
mainCh chan controlMsg // main() 的管道
|
||||
mainCtx context.Context // main() 的 ctx
|
||||
configDir *string // 设置文件所在文件夹
|
||||
recordDir *string // 弹幕文件保存的文件夹
|
||||
logger = log.New(os.Stdout, "", log.LstdFlags) // 可以同步输出的 logger
|
||||
itoa = strconv.Itoa // 将 int 转换为字符串
|
||||
)
|
||||
|
||||
// 检查错误
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 后台任务的统一入口:兜底 recover,避免单个任务的 panic 终止整个进程
|
||||
// name 用于日志里指出是哪个任务出错,f 里可以继续做自己的 recover
|
||||
func safeGo(name string, f func()) {
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
lPrintErrf("%s 发生错误并已恢复,其它录制任务不受影响:%v", name, err)
|
||||
}
|
||||
}()
|
||||
f()
|
||||
}()
|
||||
}
|
||||
|
||||
// 尝试运行,三次出错后结束运行
|
||||
func runThrice(f func() error) error {
|
||||
var err error
|
||||
for retry := 0; retry < 3; retry++ {
|
||||
if err = f(); err != nil {
|
||||
log.Printf("%v", err)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
return fmt.Errorf("运行三次都出现错误:%v", err)
|
||||
}
|
||||
|
||||
// 获取时间
|
||||
func getTime() string {
|
||||
t := time.Now()
|
||||
return fmt.Sprintf("%d-%02d-%02d %02d-%02d-%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
|
||||
}
|
||||
|
||||
// 打印带时间戳的 log 信息
|
||||
// lPrintln 定义在 webui.go 里,同时输出到控制台和 web UI 的日志
|
||||
|
||||
// 获取时间,按照 log 的时间格式
|
||||
func getLogTime() string {
|
||||
t := time.Now()
|
||||
return fmt.Sprintf("%d/%02d/%02d %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
|
||||
}
|
||||
|
||||
// 打印警告信息
|
||||
func lPrintWarn(msg ...any) {
|
||||
w := []any{"[WARN]"}
|
||||
msg = append(w, msg...)
|
||||
lPrintln(msg...)
|
||||
}
|
||||
|
||||
// 打印错误信息
|
||||
func lPrintErr(msg ...any) {
|
||||
e := []any{"[ERROR]"}
|
||||
msg = append(e, msg...)
|
||||
lPrintln(msg...)
|
||||
}
|
||||
|
||||
// 打印带时间戳的 log 信息(格式化字符串)
|
||||
func lPrintf(format string, a ...any) {
|
||||
lPrintln(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// 打印警告信息(格式化字符串)
|
||||
func lPrintWarnf(format string, a ...any) {
|
||||
lPrintWarn(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// 打印错误信息(格式化字符串)
|
||||
func lPrintErrf(format string, a ...any) {
|
||||
lPrintErr(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// 返回 ID(UID)形式的字符串
|
||||
func (s *streamer) longID() string {
|
||||
return fmt.Sprintf("%s(%d)", s.Name, s.UID)
|
||||
}
|
||||
|
||||
// 日志里用的主播名:只使用内存里已经有的数据,取不到就不发网络请求。
|
||||
// getName 缓存未命中时会重试 3 次 HTTP(最长 30 多秒),放在日志里会把界面卡住。
|
||||
func nameForLog(uid int) string {
|
||||
if s, ok := getStreamer(uid); ok && s.Name != "" {
|
||||
return s.Name
|
||||
}
|
||||
liveRooms.RLock()
|
||||
defer liveRooms.RUnlock()
|
||||
if room, ok := liveRooms.rooms[uid]; ok {
|
||||
return room.name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 返回 ID(UID)形式的字符串,用于日志,取不到名字时只打 uid
|
||||
func longID(uid int) string {
|
||||
name := nameForLog(uid)
|
||||
if name == "" {
|
||||
return itoa(uid)
|
||||
}
|
||||
return fmt.Sprintf("%s(%d)", name, uid)
|
||||
}
|
||||
|
||||
// 根据 uid 获取 liveInfo
|
||||
func getLiveInfoByUID(uid int) (infoList []liveInfo, ok bool) {
|
||||
lInfoMap.RLock()
|
||||
defer lInfoMap.RUnlock()
|
||||
for _, info := range lInfoMap.info {
|
||||
if info.uid == uid {
|
||||
infoList = append(infoList, info)
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
return infoList, ok
|
||||
}
|
||||
|
||||
// 根据 liveID 获取 liveInfo
|
||||
func getLiveInfo(liveID string) (liveInfo, bool) {
|
||||
lInfoMap.RLock()
|
||||
defer lInfoMap.RUnlock()
|
||||
if info, ok := lInfoMap.info[liveID]; ok {
|
||||
return info, true
|
||||
}
|
||||
return liveInfo{}, false
|
||||
}
|
||||
|
||||
// 将 info 放进 lInfoMap 里
|
||||
func setLiveInfo(info liveInfo) {
|
||||
lInfoMap.Lock()
|
||||
defer lInfoMap.Unlock()
|
||||
lInfoMap.info[info.LiveID] = info
|
||||
}
|
||||
|
||||
// 原地更新 liveInfo 的部分字段,不覆盖其他字段(弹幕和视频录制共用同一个 liveInfo)
|
||||
func updateLiveInfo(liveID string, f func(*liveInfo)) {
|
||||
lInfoMap.Lock()
|
||||
defer lInfoMap.Unlock()
|
||||
info, ok := lInfoMap.info[liveID]
|
||||
if !ok {
|
||||
info = liveInfo{LiveID: liveID}
|
||||
}
|
||||
f(&info)
|
||||
lInfoMap.info[liveID] = info
|
||||
}
|
||||
|
||||
// 根据 liveID 查询是否正在录制直播弹幕
|
||||
func isDanmu(liveID string) bool {
|
||||
if info, ok := getLiveInfo(liveID); ok {
|
||||
return info.isDanmu
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 根据 liveID 查询是否正在录制直播视频
|
||||
func isRecording(liveID string) bool {
|
||||
if info, ok := getLiveInfo(liveID); ok {
|
||||
return info.isRecording
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user