-
Notifications
You must be signed in to change notification settings - Fork 21
/
record.go
329 lines (297 loc) · 8.5 KB
/
record.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
// 直播下载相关
package main
import (
"context"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sync"
"time"
"unicode/utf8"
)
// record用来传递下载信息
type record struct {
stdin io.WriteCloser // ffmpeg的stdin
cancel context.CancelFunc // 用来强行停止ffmpeg运行
ch chan control // 下载goroutine的管道
}
// 存放某些没在recordMap的下载
var danglingRec struct {
sync.Mutex // records的锁
records []record
}
// 查看并获取FFmpeg的位置
func getFFmpeg() (ffmpegFile string) {
ffmpegFile = "ffmpeg"
// linux和macOS下确认有没有安装FFmpeg
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
if _, err := exec.LookPath(ffmpegFile); err != nil {
lPrintErr("系统没有安装FFmpeg")
return ""
}
}
// windows下ffmpeg.exe需要和本程序exe放在同一文件夹下
if runtime.GOOS == "windows" {
ffmpegFile = filepath.Join(exeDir, "ffmpeg.exe")
if _, err := os.Stat(ffmpegFile); os.IsNotExist(err) {
lPrintErr("ffmpeg.exe需要和本程序放在同一文件夹下")
return ""
}
}
return ffmpegFile
}
// 转换文件名和限制文件名长度
func transFilename(filename string) string {
// 转换文件名不允许的特殊字符
var re *regexp.Regexp
if runtime.GOOS == "linux" {
re = regexp.MustCompile(`[/]`)
}
if runtime.GOOS == "windows" {
re = regexp.MustCompile(`[<>:"/\\|?*]`)
}
filename = re.ReplaceAllString(filename, "-")
// linux和macOS下限制文件名长度
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
if len(filename) > 250 {
filename = filename[(len(filename) - 250):]
}
}
outFilename := filepath.Join(exeDir, filename)
// windows下全路径文件名不能过长
if runtime.GOOS == "windows" {
if utf8.RuneCountInString(outFilename) > 255 {
lPrintErr("全路径文件名太长,取消下载")
desktopNotify("全路径文件名太长,取消下载")
return ""
}
}
return outFilename
}
// 设置自动下载指定主播的直播视频
func addRecord(uid int) bool {
isExist := false
streamers.Lock()
if s, ok := streamers.crt[uid]; ok {
isExist = true
if s.Record {
lPrintWarn("已经设置过自动下载" + s.Name + "的直播视频")
} else {
s.Record = true
s.Notify.NotifyRecord = true
sets(s)
lPrintln("成功设置自动下载" + s.Name + "的直播视频")
}
}
streamers.Unlock()
if !isExist {
name := getName(uid)
if name == "" {
lPrintWarn("不存在uid为" + itoa(uid) + "的用户")
return false
}
newStreamer := streamer{UID: uid, Name: name, Record: true, Notify: notify{NotifyRecord: true}}
streamers.Lock()
sets(newStreamer)
streamers.Unlock()
lPrintln("成功设置自动下载" + name + "的直播视频")
}
saveLiveConfig()
return true
}
// 取消自动下载指定主播的直播视频
func delRecord(uid int) bool {
streamers.Lock()
if s, ok := streamers.crt[uid]; ok {
if s.Notify.NotifyOn || s.Danmu {
s.Record = false
s.Notify.NotifyRecord = false
sets(s)
} else {
deleteStreamer(uid)
}
lPrintln("成功取消自动下载" + s.Name + "的直播视频")
} else {
lPrintWarn("没有设置过自动下载uid为" + itoa(uid) + "的主播的直播视频")
}
streamers.Unlock()
saveLiveConfig()
return true
}
// 临时下载指定主播的直播视频
func startRec(uid int, danmu bool) bool {
name := getName(uid)
if name == "" {
lPrintWarn("不存在uid为" + itoa(uid) + "的用户")
return false
}
s := streamer{UID: uid, Name: name, Notify: notify{NotifyRecord: true}}
msgMap.Lock()
if m, ok := msgMap.msg[s.UID]; ok && m.recording {
lPrintWarn("已经在下载" + s.longID() + "的直播视频,如要重启下载,请先运行 stoprecord " + s.itoa())
msgMap.Unlock()
return false
}
msgMap.Unlock()
if !s.isLiveOn() {
lPrintWarn(s.longID() + "不在直播,取消下载直播视频")
return false
}
ffmpegFile := getFFmpeg()
if ffmpegFile == "" {
desktopNotify("没有找到FFmpeg,停止下载直播视频")
return false
}
// 查看程序是否处于监听状态
if *isListen {
// goroutine是为了快速返回
go s.recordLive(ffmpegFile, danmu)
} else {
// 程序只在单独下载一个直播视频,不用goroutine,防止程序提前结束运行
s.recordLive(ffmpegFile, danmu)
}
return true
}
// 停止下载指定主播的直播视频
func stopRec(uid int) bool {
msgMap.Lock()
if m, ok := msgMap.msg[uid]; ok && m.recording {
s := streamer{UID: uid, Name: getName(uid)}
lPrintln("开始停止下载" + s.longID() + "的直播视频")
m.rec.ch <- stopRecord
io.WriteString(m.rec.stdin, "q")
// 等待20秒强关下载,goroutine是为了防止锁住时间过长
go func() {
time.Sleep(20 * time.Second)
m.rec.cancel()
}()
// 需要设置recording为false
m.recording = false
} else {
lPrintWarn("没有在下载uid为" + itoa(uid) + "的主播的直播视频")
}
msgMap.Unlock()
deleteMsg(uid)
return true
}
// 退出直播视频下载相关操作
func (s streamer) quitRec() {
msgMap.Lock()
if m, ok := msgMap.msg[s.UID]; ok {
m.recording = false
}
msgMap.Unlock()
deleteMsg(s.UID)
}
// 下载主播的直播视频
func (s streamer) recordLive(ffmpegFile string, danmu bool) {
defer func() {
if err := recover(); err != nil {
lPrintErr("Recovering from panic in recordLive(), the error is:", err)
lPrintErr("下载" + s.longID() + "的直播视频发生错误,如要重启下载,请运行 startrecord " + s.itoa())
desktopNotify("下载" + s.Name + "的直播视频发生错误")
s.quitRec()
}
}()
if ffmpegFile == "" {
desktopNotify("没有找到FFmpeg,停止下载直播视频")
s.quitRec()
return
}
// 获取直播源和对应的弹幕设置
liveURL := s.getLiveURL()
if liveURL == "" {
lPrintErr("无法获取" + s.longID() + "的直播源,退出下载直播视频,如要重启下载直播视频,请运行 startrecord " + s.itoa())
desktopNotify("无法获取" + s.Name + "的直播源,退出下载直播视频")
s.quitRec()
return
}
filename := getTime() + " " + s.Name + " " + s.getTitle()
recordFile := transFilename(filename)
if recordFile == "" {
s.quitRec()
return
}
// 想要输出其他视频格式可以修改config.json里的Output
recordFile = recordFile + "." + config.Output
lPrintln("开始下载" + s.longID() + "的直播视频")
lPrintln("本次下载的视频文件保存在" + recordFile)
if *isListen {
lPrintln("如果想提前结束下载" + s.longID() + "的直播视频,运行 stoprecord " + s.itoa())
}
if s.Notify.NotifyRecord {
if danmu {
desktopNotify("开始下载" + s.Name + "的直播视频和弹幕")
} else {
desktopNotify("开始下载" + s.Name + "的直播视频")
}
}
// 运行ffmpeg下载直播视频,不用mainCtx是为了能正常退出
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cmd := exec.CommandContext(ctx, ffmpegFile,
"-timeout", "10000000",
"-i", liveURL,
"-c", "copy", recordFile)
stdin, err := cmd.StdinPipe()
checkErr(err)
defer stdin.Close()
ch := make(chan control, 20)
rec := record{stdin: stdin, cancel: cancel, ch: ch}
msgMap.Lock()
if m, ok := msgMap.msg[s.UID]; ok {
m.recording = true
m.rec = rec
} else {
msgMap.msg[s.UID] = &sMsg{recording: true, rec: rec}
}
msgMap.Unlock()
if !*isListen {
// 程序单独下载一个直播视频时可以按q键退出(ffmpeg的特性)
cmd.Stdin = os.Stdin
lPrintln("按q键退出下载直播视频")
}
// 下载弹幕
if danmu {
go s.initDanmu(ctx, filename)
}
err = cmd.Run()
if err != nil {
lPrintErr("下载"+s.longID()+"的直播视频出现错误,尝试重启下载:", err)
}
if _, _, streamName, _ := s.getStreamURL(); streamName != "" {
select {
case msg := <-ch:
switch msg {
// 收到下播的信号
case liveOff:
// 收到停止下载的信号
case stopRecord:
default:
lPrintErr("未知的controlMsg:", msg)
}
default:
// 程序处于监听状态时重启下载,否则不重启
if *isListen {
// 由于某种原因导致下载意外结束
lPrintWarn("因意外结束下载" + s.longID() + "的直播视频,尝试重启下载")
// 延迟两秒,防止意外情况下刷屏
time.Sleep(2 * time.Second)
go s.recordLive(ffmpegFile, danmu)
}
}
} else {
s.quitRec()
}
lPrintln(s.longID() + "的直播视频下载已经结束")
if s.Notify.NotifyRecord {
if danmu {
desktopNotify(s.Name + "的直播视频和弹幕下载已经结束")
} else {
desktopNotify(s.Name + "的直播视频下载已经结束")
}
}
}