├── .gitignore
├── Dockerfile
├── LICENSE
├── Readme.md
├── ReleaseNotes.md
├── build-386.ps1
├── build.ps1
├── changelog.txt
├── livedl-logger.go
├── readme-gen.pl
├── replacelocal.pl
├── src
├── amf
│ ├── amf.go
│ ├── amf0
│ │ └── amf0.go
│ ├── amf3
│ │ └── amf3.go
│ └── amf_t
│ │ └── amf_t.go
├── buildno
│ ├── buildno.go
│ └── funcs.go
├── cryptoconf
│ └── cryptoconf.go
├── defines
│ └── constant.go
├── files
│ └── files.go
├── flvs
│ └── flv.go
├── gorman
│ └── gorman.go
├── httpbase
│ └── httpbase.go
├── httpsub
│ └── httpsub.go
├── livedl.go
├── log4gui
│ └── log4gui.go
├── niconico
│ ├── jikken.gox
│ ├── nico.go
│ ├── nico_db.go
│ ├── nico_hls.go
│ ├── nico_mem_db.go
│ └── nico_rtmp.go
├── objs
│ └── objs.go
├── options
│ └── options.go
├── procs
│ ├── base
│ │ └── base.go
│ ├── ffmpeg
│ │ └── ffmpeg.go
│ ├── kill.go
│ ├── streamlink
│ │ └── streamlink.go
│ └── youtube_dl
│ │ └── youtube-dl.go
├── rtmps
│ ├── message.go
│ └── rtmp.go
├── twitcas
│ └── twicas.go
├── youtube
│ ├── comment.go
│ ├── youtube.go
│ └── youtube.gox
└── zip2mp4
│ └── zip2mp4.go
└── updatebuildno.go
/.gitignore:
--------------------------------------------------------------------------------
1 | /testrec/
2 |
3 | *.flv
4 | *.mp4
5 | *.ts
6 | *.mkv
7 | *.part
8 | *.mpg
9 | *.webm
10 |
11 |
12 | *~
13 |
14 | *.exe
15 | *.dll
16 | *.exe.config
17 |
18 | *.xml
19 | *.m3u8
20 |
21 | *.bin
22 | *.zip
23 | *.txt
24 | *.conf
25 |
26 | *.db
27 | *.pem
28 | *.ass
29 | *.sqlite
30 | *.sqlite3
31 | *-journal
32 | *.sqlite3-shm
33 | *.sqlite3-wal
34 |
35 | !changelog.txt
36 |
37 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.11-alpine as builder
2 |
3 | RUN apk add --no-cache \
4 | build-base \
5 | git && \
6 | go get github.com/gorilla/websocket && \
7 | go get golang.org/x/crypto/sha3 && \
8 | go get github.com/mattn/go-sqlite3 && \
9 | go get github.com/gin-gonic/gin
10 |
11 | COPY . /tmp/livedl
12 |
13 | RUN cd /tmp/livedl && \
14 | go build src/livedl.go
15 |
16 |
17 |
18 | FROM alpine:3.8
19 |
20 | RUN apk add --no-cache \
21 | ca-certificates \
22 | ffmpeg \
23 | openssl
24 |
25 | COPY --from=builder /tmp/livedl/livedl /usr/local/bin/
26 |
27 | WORKDIR /livedl
28 |
29 | VOLUME /livedl
30 |
31 | CMD livedl
32 |
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 himananiito
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | c# livedl
2 | 新配信(HTML5)に対応したニコ生録画ツール。ニコ生以外のサイトにも対応予定
3 |
4 | ## 更新
5 |
6 | [更新履歴](https://github.com/hanaonnao/livedl/blob/master/changelog.txt)
7 |
8 |
9 |
10 | ### 更新方法
11 |
12 | livedlのに移動、livedlを削除します
13 | ```
14 | rm -rf livedl
15 | ```
16 | ビルドを再実行し、Livedlビルドする。
17 |
18 |
19 | ## 使い方
20 | https://himananiito.hatenablog.jp/entry/livedl
21 | を参照
22 |
23 | ## Linux(Ubuntu)でのビルド方法
24 | ```
25 | cat /etc/os-release
26 | NAME="Ubuntu"
27 | VERSION="18.04.4 LTS (Bionic Beaver)"
28 | ```
29 |
30 | ### Go実行環境のインストール (無い場合)
31 | ```
32 | wget https://dl.google.com/go/go1.15.11.linux-amd64.tar.gz
33 | sudo rm -rf /usr/local/go
34 | sudo tar -C /usr/local -xzf go1.15.11.linux-amd64.tar.gz
35 | export PATH=$PATH:/usr/local/go/bin
36 | # 必要であれば、bashrcなどにPATHを追加する
37 | ```
38 |
39 | ### gitをインストール (無い場合)
40 | ```
41 | sudo apt-get install git
42 | ```
43 |
44 | ### gccなどのビルドツールをインストール (無い場合)
45 | ```
46 | sudo apt-get install build-essential
47 | ```
48 |
49 | ### 必要なgoのモジュールをインストール
50 | ```
51 | go get github.com/gorilla/websocket
52 | go get golang.org/x/crypto/sha3
53 | go get github.com/mattn/go-sqlite3
54 | go get github.com/gin-gonic/gin
55 | ```
56 |
57 | ### livedlのソースを取得
58 | ```
59 | git clone https://github.com/hanaonnao/livedl.git
60 |
61 | (元は git clone https://github.com/himananiito/livedl.git)
62 | ```
63 |
64 | ### livedlのコンパイル
65 |
66 | ディレクトリを移動
67 | ```
68 | cd livedl
69 | ```
70 |
71 | #### ~~(オプション)特定のバージョンを選択する場合~~
72 | ```
73 | ~$ git tag
74 | 20180513.6
75 | 20180514.7
76 | ...
77 | 20180729.21
78 | 20180807.22
79 | $ git checkout 20180729.21 (選んだバージョン)
80 | ```
81 |
82 | #### ~~(オプション)最新のコードをビルドする場合~~
83 | ```
84 | git checkout master
85 | ```
86 |
87 | ビルドする
88 | ```
89 | go build src/livedl.go
90 | ```
91 | もし、cannot find package "github.com/gin-gonic/gin" in any of:
92 |
93 | など出る場合は、
94 | `go get github.com/gin-gonic/gin` (適宜読み替える)したのち`go build src/livedl.go`を再実行する
95 |
96 | ```
97 | ./livedl -h
98 | livedl (20180807.22-linux)
99 | ```
100 |
101 | ## Windows(32bit及び64bit上での32bit向け)コンパイル方法
102 |
103 | ### gccのインストール
104 |
105 | gcc には必ず以下を使用すること。
106 |
107 | http://tdm-gcc.tdragon.net/download
108 |
109 | 環境変数で(例)`C:\TDM-GCC-64\bin`が他のgccより優先されるように設定すること。
110 |
111 | ### 必要なgoのモジュール
112 |
113 | linuxの説明に倣ってインストールする。
114 |
115 | ### コンパイル
116 |
117 | PowerSellで、`build-386.ps1` を実行する。または以下を実行する。
118 |
119 | ```
120 | set-item env:GOARCH -value 386
121 | set-item env:CGO_ENABLED -value 1
122 | go build -o livedl.x86.exe src/livedl.go
123 | ```
124 |
125 | ### 32bit環境で`x509: certificate signed by unknown authority`が出る
126 |
127 | 動けばいいのであればオプションで以下を指定する。
128 |
129 | `-http-skip-verify=on`
130 |
131 | ## Dockerでビルド
132 |
133 | ### livedlのソースを取得
134 | ```
135 | git clone https://github.com/himananiito/livedl.git
136 | cd livedl
137 | git checkout master # Or another version that supports docker (contains Dockerfile)
138 | ```
139 |
140 | ### イメージ作成
141 | ```
142 | docker build -t .
143 | ```
144 |
145 | ### イメージの使い方
146 |
147 | - 出力フォルダを/livedlにマウント
148 | - 通常のパラメーターに加えて`--no-chdir`を渡す
149 |
150 | ```
151 | docker run -it --rm -v ~/livedl:/livedl livedl --no-chdir ...
152 | ```
153 |
154 | 以上
155 |
--------------------------------------------------------------------------------
/ReleaseNotes.md:
--------------------------------------------------------------------------------
1 | ## 更新履歴/説明
2 |
3 | ### 20200903.39
4 | (https://egg.5ch.net/test/read.cgi/software/1595715643/57)
5 |
6 | ・セルフ追っかけ再生
7 | 例:http://127.0.0.1:12345/m3u8/2/1200/index.m3u8
8 | 現在のシーケンス番号から1200セグメント(リアルタイムの場合30分)戻ったところを再生
9 |
10 | ・追加オプション
11 | -nico-conv-seqno-start <num>
12 | MP4への変換を指定したセグメント番号から開始する
13 | -nico-conv-seqno-end <num>
14 | MP4への変換を指定したセグメント番号で終了する
15 | -nico-conv-force-concat
16 | MP4への変換で画質変更または抜けがあっても分割しないように設定
17 | -nico-conv-force-concat=on
18 | (+) 上記を有効に設定
19 | -nico-conv-force-concat=off
20 | (+) 上記を無効に設定(デフォルト)
21 |
22 | -d2h
23 | [実験的] 録画済みのdb(.sqlite3)を視聴するためのHLSサーバを立てる(-db-to-hls)
24 | 開始シーケンス番号は(変換ではないが) -nico-conv-seqno-start で指定
25 | 使用例:$ livedl lvXXXXXXXXX.sqlite3 -d2h -nico-hls-port 12345 -nico-conv-seqno-start 2780
26 |
--------------------------------------------------------------------------------
/build-386.ps1:
--------------------------------------------------------------------------------
1 |
2 | set-item env:GOARCH -value 386
3 | set-item env:CGO_ENABLED -value 1
4 |
5 | go build -o livedl.x86.exe src/livedl.go
6 |
--------------------------------------------------------------------------------
/build.ps1:
--------------------------------------------------------------------------------
1 | rm livedl.exe
2 | go run updatebuildno.go
3 | go build src/livedl.go
4 | .\build-386.ps1
5 | go build livedl-logger.go
6 |
7 | # hide local path
8 | perl replacelocal.pl
9 |
10 | # Generate Readme.txt
11 | perl readme-gen.pl
12 |
13 | # livedl test run(nico)
14 | $process = Start-Process -FilePath livedl.exe -ArgumentList '-nicotestrun -nicotesttimeout 7 -nicotestfmt "testrec/?UNAME?/?PID?-?UNAME?-?TITLE?"' -PassThru
15 | $process.WaitForExit(1000 * 61)
16 | $process.Kill()
17 |
18 | $process = Start-Process -FilePath livedl.x86.exe -ArgumentList '-nicotestrun -nicotesttimeout 7 -nicotestfmt "testrec/?UNAME?/?PID?-?UNAME?-?TITLE?"' -PassThru
19 | $process.WaitForExit(1000 * 30)
20 | $process.Kill()
21 |
22 | $dir = "livedl"
23 | $zip = "$dir.zip"
24 | if(Test-Path -PathType Leaf $zip) {
25 | rm $zip
26 | }
27 | if(Test-Path -PathType Container $dir) {
28 | rmdir -Recurse $dir
29 | }
30 | mkdir $dir
31 | cp livedl.exe $dir
32 | cp livedl.x86.exe $dir
33 | cp livedl-logger.exe $dir
34 | cp Readme.txt $dir
35 |
36 | cp livedl-gui.exe $dir
37 | cp livedl-gui.exe.config $dir
38 | cp Newtonsoft.Json.dll $dir
39 | cp Newtonsoft.Json.xml $dir
40 |
41 | Compress-Archive -Path $dir -DestinationPath $zip
42 |
43 | if(Test-Path -PathType Container $dir) {
44 | rmdir -Recurse $dir
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/changelog.txt:
--------------------------------------------------------------------------------
1 | 更新履歴/説明
2 |
3 | 20210405.40
4 |
5 | 更新12 (2021/01/31)
6 | ・livedl で-yt-no-streamlink=on -yt-no-youtube-dl=on が指定されたとき、YouTube Live のコメントを永久に取得し続けるパッチ
7 | https://egg.5ch.net/test/read.cgi/software/1595715643/567
8 | 止めたいときは Ctrl-C を入力して少し待てば「comment done」と表示されて終了します
9 | 放送終了後に放置した場合どういう挙動になるかは試してないです
10 |
11 | 更新11 (2021/01/30)
12 | ・livedl を YouTube Live の直近の仕様変更に対応
13 | https://egg.5ch.net/test/read.cgi/software/1595715643/559
14 |
15 |
16 | 更新10 (2021/01/27)
17 | ・金額のフォーマットの要望ないみたいだからこっちで勝手に決めさせてもらったよ
18 | https://egg.5ch.net/test/read.cgi/software/1595715643/543
19 | Youtubeの金額フォーマットを追加(コメントのamount属性)
20 |
21 | 更新9 (2021/01/25)
22 | ・livedl で YouTubeLive リプレイのコメントが取れるよう直したよ
23 | https://egg.5ch.net/test/read.cgi/software/1595715643/523
24 | YouTubeLiveのコメントAPI変更
25 |
26 | 更新8 (2021/01/01)
27 | ・livedl で一部コメントが保存されないのを修正する
28 | https://egg.5ch.net/test/read.cgi/software/1595715643/451
29 | https://egg.5ch.net/test/read.cgi/software/1595715643/457
30 | livedlでvpos属性またはdate_usec属性がないコメントが保存されませんの修正
31 |
32 | 更新7 (2021/01/01)
33 | ・livedl で waybackkey の取得方法を変更する
34 | https://egg.5ch.net/test/read.cgi/software/1595715643/424
35 |
36 | 更新6 (2020/12/13)
37 | ・livedl で YouTube Live を扱えるようにする
38 | patch は livedl.youtube-r1.patch のみ適用
39 | https://egg.5ch.net/test/read.cgi/software/1595715643/402
40 | https://egg.5ch.net/test/read.cgi/software/1595715643/406
41 |
42 | 更新5 (2020/11/15)
43 | ・livedl で HTTP のタイムアウト時間を変更できるようにする
44 | https://egg.5ch.net/test/read.cgi/software/1595715643/272
45 |
46 | 追加オプション
47 | -http-timeout <num>
48 | タイムアウト時間(秒)デフォルト: 5秒(最低値)
49 |
50 | 更新4 (2020/10/25)
51 | ・旧配信のタイムシフトを録画できるようにする
52 | https://egg.5ch.net/test/read.cgi/software/1595715643/228
53 |
54 | 更新3(2020/10/06)
55 | patch は livedl.comment-name-attribute-r1.patch.gz のみ適用
56 | https://egg.5ch.net/test/read.cgi/software/1595715643/194
57 | 更新2の修正
58 |
59 | 更新2(2020/10/05)
60 | ・XMLコメントのname属性(出演者が名前付きのコメントする時に使用)を保存するように修正
61 | https://egg.5ch.net/test/read.cgi/software/1595715643/174
62 |
63 | 更新1(2020/10/03)
64 | ・指定時間でタイムシフト録画を停止するためのパッチ(+α)
65 | https://egg.5ch.net/test/read.cgi/software/1595715643/163
66 |
67 | オプション
68 | -nico-ts-start <num>
69 | タイムシフトの録画を指定した再生時間(秒)から開始する
70 | -nico-ts-stop <num>
71 | タイムシフトの録画を指定した再生時間(秒)で停止する
72 | 上記2つは <分>:<秒> | <時>:<分>:<秒> の形式でも指定可能
73 |
74 | -nico-ts-start-min <num>
75 | タイムシフトの録画を指定した再生時間(分)から開始する
76 | -nico-ts-stop-min <num>
77 | タイムシフトの録画を指定した再生時間(分)で停止する
78 | 上記2つは <時>:<分> の形式でも指定可能
79 |
80 | 20200903.39
81 | (https://egg.5ch.net/test/read.cgi/software/1595715643/57)
82 |
83 | ・セルフ追っかけ再生
84 | 例:http://127.0.0.1:12345/m3u8/2/1200/index.m3u8
85 | 現在のシーケンス番号から1200セグメント(リアルタイムの場合30分)戻ったところを再生
86 |
87 | ・追加オプション
88 | -nico-conv-seqno-start <num>
89 | MP4への変換を指定したセグメント番号から開始する
90 | -nico-conv-seqno-end <num>
91 | MP4への変換を指定したセグメント番号で終了する
92 | -nico-conv-force-concat
93 | MP4への変換で画質変更または抜けがあっても分割しないように設定
94 | -nico-conv-force-concat=on
95 | (+) 上記を有効に設定
96 | -nico-conv-force-concat=off
97 | (+) 上記を無効に設定(デフォルト)
98 |
99 | -d2h
100 | [実験的] 録画済みのdb(.sqlite3)を視聴するためのHLSサーバを立てる(-db-to-hls)
101 | 開始シーケンス番号は(変換ではないが) -nico-conv-seqno-start で指定
102 | 使用例:$ livedl lvXXXXXXXXX.sqlite3 -d2h -nico-hls-port 12345 -nico-conv-seqno-start 2780
103 |
104 | 2020.07.29 ‐ ニコ生のコメント保存できない問題を修正
105 | https://egg.5ch.net/test/read.cgi/software/1595715643/17
106 |
107 | 2020.07.04 ‐ 「broadcastId not found」エラーへの対処
108 | https://egg.5ch.net/test/read.cgi/software/1570634489/744
109 |
110 |
111 | 2020.06.08 - APIの仕様変更に対応
112 | http://egg.5ch.net/test/read.cgi/software/1570634489/535
113 |
114 |
115 |
116 | 20181215.35
117 | ・-nico-ts-start-minオプションの追加
118 | ・win32bit版のビルドを追加
119 | ・-http-skip-verifyオプションを保存できるようにした
120 | ・ライセンスをMITにした
121 |
122 | 20181107.34
123 | ・[ニコ生] (暫定)TEMPORARILY_CROWDEDで録画終了するようにした
124 | ・ファイル名が半角ドットで終わる場合に全角ドットにした
125 | ・[YouTubeLive] コメントの改行をCRLFにした
126 | ・[ニコ生TS] タイムシフトの録画を指定した再生時間(秒)から開始するオプション追加(merged)
127 | ・[ニコ生TS] 32bitで終了しない問題を修正(merged)
128 |
129 | 20181008.33
130 | ・[Youtube] チャットが取得できない問題を修正
131 | ・[Youtube] Streamlinkでダウンロードできない場合にyoutube-dlを使うようにした
132 | ・[Youtube] コメントファイルを書き出せるようにした。
133 | ・#15 [ニコ生コメント] 出力をCRLFにした。/hbコマンドを出さないオプションを追加
134 |
135 | 20181003.32
136 | ・#14 ★緊急 [ニコ生] 新配信録画のプレイリスト取得にウェイトが入らない問題を修正
137 | ・#9 [ニコ生TS] プレイリストの最後で無限ループしてしまう問題を修正
138 | ・YoutubeLiveコメント対応中(未完了)
139 | ・[実験的] -yt-api-key オプションの追加(未使用)
140 |
141 | 20180925.31
142 | ・#8 [ツイキャス] 「c:」から始まるユーザ名が録画できない問題を修正
143 | ・#11 [ツイキャス] 実行直後またはリトライ中にエラーで終了する問題を修正
144 | ・#10 [ツイキャス] -tcas-retry-intervalが効かない問題を修正
145 | ・#12 [ニコ生] タイムシフトで先頭のセグメント(seqno=0)が取得できない問題を修正
146 |
--------------------------------------------------------------------------------
/livedl-logger.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "bufio"
6 | "regexp"
7 | "sync"
8 | "os"
9 | "os/exec"
10 | )
11 |
12 | func main() {
13 | args := os.Args[1:]
14 | var vid string
15 | for _, s := range args {
16 | if ma := regexp.MustCompile(`(lv\d{9,})`).FindStringSubmatch(s); len(ma) > 0 {
17 | vid = ma[1]
18 | }
19 | }
20 |
21 | args = append(args, "-nicoDebug")
22 | cmd := exec.Command("livedl", args...)
23 |
24 | if vid == "" {
25 | cmd.Stdout = os.Stdout
26 | cmd.Stderr = os.Stderr
27 | cmd.Run()
28 | } else {
29 | stdout, err := cmd.StdoutPipe()
30 | if err != nil {
31 | fmt.Println(err)
32 | return
33 | }
34 | stderr, err := cmd.StderrPipe()
35 | if err != nil {
36 | fmt.Println(err)
37 | return
38 | }
39 |
40 | name := fmt.Sprintf("log/%s.txt", vid)
41 | os.MkdirAll("log", os.ModePerm)
42 | f, err := os.Create(name)
43 | if err != nil {
44 | fmt.Println(err)
45 | return
46 | }
47 | defer f.Close()
48 |
49 | var mtx sync.Mutex
50 | append := func(s string) {
51 | mtx.Lock()
52 | defer mtx.Unlock()
53 | f.WriteString(s)
54 | }
55 |
56 | go func() {
57 | rdr := bufio.NewReader(stdout)
58 | for {
59 | s, err := rdr.ReadString('\n')
60 | if err != nil {
61 | return
62 | }
63 | fmt.Print(s)
64 | append(s)
65 | }
66 | defer stdout.Close()
67 | }()
68 | go func() {
69 | rdr := bufio.NewReader(stderr)
70 | for {
71 | s, err := rdr.ReadString('\n')
72 | if err != nil {
73 | return
74 | }
75 | append(s)
76 | }
77 | defer stderr.Close()
78 | }()
79 | cmd.Run()
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/readme-gen.pl:
--------------------------------------------------------------------------------
1 | use strict;
2 | use warnings;
3 | use v5.20;
4 |
5 | open my $f, "-|", "livedl", "-h" or die;
6 | undef $/;
7 | my $s = <$f>;
8 | close $f;
9 |
10 | $s =~ s{livedl\s*\((\d+\.\d+)[^\r\n]*}{livedl ($1)} or die;
11 | my $ver = $1;
12 |
13 | $s =~ s{chdir:[^\n]*\n}{};
14 |
15 | open my $g, "changelog.txt" or die;
16 | my $t = <$g>;
17 | close $g;
18 |
19 | $t =~ s{\$latest}{$ver} or die;
20 |
21 | open my $h, ">", "changelog.txt" or die;
22 | print $h $t;
23 | close $h;
24 |
25 | open my $o, ">", "Readme.txt" or die;
26 | say $o $s;
27 | say $o "";
28 | say $o $t;
29 | close $o;
30 |
--------------------------------------------------------------------------------
/replacelocal.pl:
--------------------------------------------------------------------------------
1 | # perl
2 | # livedl.exe内のローカルパスの文字列を隠す
3 | use strict;
4 | use v5.20;
5 |
6 | for my $file("livedl.exe", "livedl.x86.exe", "livedl-logger.exe") {
7 | open my $f, "<:raw", $file or die;
8 | undef $/;
9 | my $s = <$f>;
10 | close $f;
11 |
12 | say "$0: $file";
13 |
14 | my %h = ();
15 |
16 | while($s =~ m{(?<=\0)[^\0]{5,512}\.go(?=\0)|(?<=[[:cntrl:]])_/[A-Z]_/[^\0]{5,512}}g) {
17 | my $s = $&;
18 | if($s =~ m{\A(.*(?:/Users/.+?/go/src|/Go/src))(/.*)\z}s or
19 | $s =~ m{\A(.*(?=/livedl/src/))(/.*)\z}s) {
20 | my($all, $p, $f) = ($s, $1, $2);
21 |
22 | my $p2 = $p;
23 | $p2 =~ s{.}{*}gs;
24 | #$h{$all} = $p2 . $f;
25 |
26 | #say $p;
27 | $h{$p} = $p2;
28 | }
29 | }
30 |
31 | for my $k (sort{$a cmp $b} keys %h) {
32 | my $k2 = $k;
33 | $k2 =~ s{/}{\\}g;
34 |
35 | my $r = quotemeta $k;
36 | my $r2 = quotemeta $k2;
37 |
38 | say "$k => $h{$k}";
39 |
40 | $s =~ s{$r}{$h{$k}}g;
41 | $s =~ s{$r2}{$h{$k}}g;
42 | }
43 |
44 | open $f, ">:raw", $file or die;
45 | print $f $s;
46 | close $f;
47 |
48 | sleep 1;
49 | }
50 |
--------------------------------------------------------------------------------
/src/amf/amf.go:
--------------------------------------------------------------------------------
1 | package amf
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "./amf0"
7 | "./amf_t"
8 | )
9 |
10 | func SwitchToAmf3() amf_t.SwitchToAmf3 {
11 | return amf_t.SwitchToAmf3{}
12 | }
13 |
14 | func EncodeAmf0(data []interface{}, asEcmaArray bool) ([]byte, error) {
15 | return amf0.Encode(data, asEcmaArray)
16 | }
17 |
18 | func Amf0EcmaArray(data map[string]interface {}) (amf_t.AMF0EcmaArray) {
19 | return amf_t.AMF0EcmaArray{
20 | Data: data,
21 | }
22 | }
23 |
24 | // paddingHint: zero padded before AMF data
25 | func DecodeAmf0(data []byte, paddingHint... bool) (res []interface{}, err error) {
26 | rdr := bytes.NewReader(data)
27 | var seek1 bool
28 | for _, h := range paddingHint {
29 | if h {
30 | seek1 = true
31 | break
32 | }
33 | }
34 | if seek1 {
35 | rdr.Seek(1, io.SeekStart)
36 | }
37 | res, err = amf0.DecodeAll(rdr)
38 | if err != nil {
39 | if seek1 {
40 | // retry
41 | rdr.Seek(0, io.SeekStart)
42 | res, err = amf0.DecodeAll(rdr)
43 | }
44 | }
45 |
46 | return
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/src/amf/amf0/amf0.go:
--------------------------------------------------------------------------------
1 | package amf0
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "encoding/binary"
7 | "math"
8 | "fmt"
9 | "log"
10 | "../amf3"
11 | "../amf_t"
12 | )
13 |
14 | func encodeNumber(num float64, buff *bytes.Buffer) (err error) {
15 | if err = buff.WriteByte(0); err != nil {
16 | return
17 | }
18 |
19 | bits := math.Float64bits(num)
20 | bytes := make([]byte, 8)
21 | binary.BigEndian.PutUint64(bytes, bits)
22 | if _, err = buff.Write(bytes); err != nil {
23 | return
24 | }
25 | return
26 | }
27 | func encodeBoolean(b bool, buff *bytes.Buffer) (err error) {
28 | if err = buff.WriteByte(1); err != nil {
29 | return
30 | }
31 |
32 | var val byte
33 | if b {
34 | val = 1
35 | }
36 |
37 | if err = buff.WriteByte(val); err != nil {
38 | return
39 | }
40 |
41 | return
42 | }
43 | func encodeUtf8(s string, buff *bytes.Buffer) (err error) {
44 | bs := []byte(s)
45 | if len(bs) > 0xffff {
46 | err = fmt.Errorf("string too large")
47 | return
48 | }
49 |
50 | b0 := make([]byte, 2)
51 | binary.BigEndian.PutUint16(b0, uint16(len(bs)))
52 | if _, err = buff.Write(b0); err != nil {
53 | return
54 | }
55 | if _, err = buff.Write(bs); err != nil {
56 | return
57 | }
58 | return
59 | }
60 | func encodeString(s string, buff *bytes.Buffer) (err error) {
61 | if err = buff.WriteByte(2); err != nil {
62 | return
63 | }
64 | err = encodeUtf8(s, buff)
65 | return
66 | }
67 | func encodeObject(obj map[string]interface {}, buff *bytes.Buffer) (err error) {
68 | if err = buff.WriteByte(3); err != nil {
69 | return
70 | }
71 |
72 | for k, v := range obj {
73 | if err = encodeUtf8(k, buff); err != nil {
74 | return
75 | }
76 | if _, err = encode(v, false, buff); err != nil {
77 | return
78 | }
79 | }
80 | if _, err = buff.Write([]byte{0, 0, 9}); err != nil {
81 | return
82 | }
83 | return
84 | }
85 |
86 | func encodeNull(buff *bytes.Buffer) error {
87 | return buff.WriteByte(5)
88 | }
89 | func encodeSwitchToAmf3(buff *bytes.Buffer) error {
90 | return buff.WriteByte(0x11)
91 | }
92 | func encodeEcmaArray(data map[string]interface {}, buff *bytes.Buffer) (err error) {
93 | if err = buff.WriteByte(8); err != nil {
94 | return
95 | }
96 | buf4 := make([]byte, 4)
97 | binary.BigEndian.PutUint32(buf4, uint32(len(data)))
98 | if _, err = buff.Write(buf4); err != nil {
99 | return
100 | }
101 |
102 | for k, v := range data {
103 | if err = encodeUtf8(k, buff); err != nil {
104 | return
105 | }
106 | if _, err = encode(v, true, buff); err != nil {
107 | return
108 | }
109 | }
110 | if _, err = buff.Write([]byte{0, 0, 9}); err != nil {
111 | return
112 | }
113 |
114 | return
115 | }
116 | func encode(data interface{}, asEcmaArray bool, buff *bytes.Buffer) (toAmf3 bool, err error) {
117 | switch data.(type) {
118 | case string:
119 | err = encodeString(data.(string), buff)
120 | case float64:
121 | err = encodeNumber(data.(float64), buff)
122 | case int:
123 | err = encodeNumber(float64(data.(int)), buff)
124 | case bool:
125 | err = encodeBoolean(data.(bool), buff)
126 | case map[string]interface{}:
127 | if asEcmaArray {
128 | err = encodeEcmaArray(data.(map[string]interface{}), buff)
129 | } else {
130 | err = encodeObject(data.(map[string]interface{}), buff)
131 | }
132 | case []interface {}:
133 | m := make(map[string]interface{})
134 | for i, d := range data.([]interface {}) {
135 | k := fmt.Sprintf("%d", i)
136 | m[k] = d
137 | }
138 | err = encodeEcmaArray(m, buff)
139 | case nil:
140 | err = encodeNull(buff)
141 | case amf_t.SwitchToAmf3:
142 | toAmf3 = true
143 | err = encodeSwitchToAmf3(buff)
144 | default:
145 | log.Fatalf("amf0/encode %#v", data)
146 | }
147 | return
148 | }
149 |
150 | func Encode(data []interface{}, asEcmaArray bool) (b []byte, err error) {
151 | buff := bytes.NewBuffer(nil)
152 | for i, d := range data {
153 | var toAmf3 bool
154 | if toAmf3, err = encode(d, asEcmaArray, buff); err != nil {
155 | return
156 | }
157 | if toAmf3 {
158 | b2, e := amf3.Encode(data[i+1:])
159 | if e != nil {
160 | err = e
161 | return
162 | }
163 | b = append(b, buff.Bytes()...)
164 | b = append(b, b2...)
165 | return
166 | }
167 | }
168 | b = buff.Bytes()
169 | return
170 | }
171 |
172 | type objectEnd struct {}
173 |
174 | func decodeString(rdr *bytes.Reader) (str string, err error) {
175 | buf := make([]byte, 2)
176 | if _, err = io.ReadFull(rdr, buf); err != nil {
177 | return
178 | }
179 | len := (int(buf[0]) << 8) | int(buf[1])
180 | if len > 0 {
181 | buf := make([]byte, len)
182 | if _, err = io.ReadFull(rdr, buf); err != nil {
183 | return
184 | }
185 | str = string(buf)
186 | }
187 | return
188 | }
189 | func decodeNumber(rdr *bytes.Reader) (res float64, err error) {
190 | buf := make([]byte, 8)
191 | if _, err = io.ReadFull(rdr, buf); err != nil {
192 | return
193 | }
194 |
195 | u64 := binary.BigEndian.Uint64(buf)
196 | res = math.Float64frombits(u64)
197 | return
198 | }
199 | func decodeBoolean(rdr *bytes.Reader) (res bool, err error) {
200 | buf := make([]byte, 1)
201 | if _, err = io.ReadFull(rdr, buf); err != nil {
202 | return
203 | }
204 | if buf[0] == 0 {
205 | res = false
206 | } else {
207 | res = true
208 | }
209 | return
210 | }
211 | func decodeObject(rdr *bytes.Reader) (res map[string]interface{}, err error) {
212 | res = make(map[string]interface{})
213 | for {
214 | key, e := decodeString(rdr)
215 | if e != nil {
216 | err = e
217 | return
218 | }
219 |
220 | val, e := decodeOne(rdr)
221 | if e != nil {
222 | err = e
223 | return
224 | }
225 | if key == "" {
226 | switch val.(type) {
227 | case objectEnd:
228 | return
229 | default:
230 | log.Fatalf("decodeObject: parse error; Not object-end, %+s", val)
231 | }
232 | }
233 | res[key] = val
234 | }
235 | return
236 | }
237 | func decodeEcmaArray(rdr *bytes.Reader) (res map[string]interface{}, err error) {
238 | buf := make([]byte, 4)
239 | if _, err = io.ReadFull(rdr, buf); err != nil {
240 | return
241 | }
242 | //count := binary.BigEndian.Uint32(buf)
243 | //log.Printf("decodeEcmaArray: Count: %v", count)
244 | res, err = decodeObject(rdr)
245 |
246 | return
247 | }
248 | func decodeStrictArray(rdr *bytes.Reader) (res []interface{}, err error) {
249 | buf := make([]byte, 4)
250 | if _, err = io.ReadFull(rdr, buf); err != nil {
251 | return
252 | }
253 | count := binary.BigEndian.Uint32(buf)
254 | for i := uint32(0); i < count; i++ {
255 | re, e := decodeOne(rdr)
256 | if e != nil {
257 | err = e
258 | return
259 | }
260 | res = append(res, re)
261 | }
262 | return
263 | }
264 |
265 |
266 | func decodeOne(rdr *bytes.Reader) (res interface{}, err error) {
267 | buf := make([]byte, 1)
268 | if _, err = io.ReadFull(rdr, buf); err != nil {
269 | return
270 | }
271 | switch buf[0] {
272 | case 0: // Number
273 | res, err = decodeNumber(rdr)
274 | case 1: // Boolean
275 | res, err = decodeBoolean(rdr)
276 | case 2: // String
277 | res, err = decodeString(rdr)
278 | case 3:
279 | res, err = decodeObject(rdr)
280 | case 5: // Null
281 | res = nil
282 | case 6: // undefined
283 | res = nil
284 | case 8: // ECMA Array
285 | res, err = decodeEcmaArray(rdr)
286 |
287 | case 9: // Object End
288 | res = objectEnd{}
289 | case 10:
290 | res, err = decodeStrictArray(rdr)
291 | case 0x11: // Switch to AMF3
292 | dat, e := amf3.DecodeAll(rdr)
293 | if e != nil {
294 | err = e
295 | return
296 | }
297 | res = amf_t.AMF3{Data: dat}
298 | default:
299 | err = fmt.Errorf("Not implemented: type=%d", buf[0])
300 | }
301 | return
302 | }
303 |
304 |
305 |
306 | func DecodeAll(rdr *bytes.Reader) (res []interface{}, err error) {
307 | for rdr.Len() > 0 {
308 | re, e := decodeOne(rdr)
309 | if e != nil {
310 | err = e
311 | return
312 | }
313 | switch re.(type) {
314 | case amf_t.AMF3:
315 | res = append(res, re.(amf_t.AMF3).Data...)
316 | default:
317 | res = append(res, re)
318 | }
319 | }
320 | return
321 | }
322 |
--------------------------------------------------------------------------------
/src/amf/amf3/amf3.go:
--------------------------------------------------------------------------------
1 | package amf3
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "log"
7 | "fmt"
8 | )
9 |
10 | func decodeU29(rdr *bytes.Reader) (res int, err error) {
11 | for i := 0; i < 4; i++ {
12 | var num byte
13 | if num, err = rdr.ReadByte(); err != nil {
14 | return
15 | }
16 | var flg bool
17 | var val uint8
18 | if i == 3 {
19 | val = num
20 | } else {
21 | flg = (num & 0x80) == 0x80
22 | val = (num & 0x7f)
23 | }
24 |
25 | switch i {
26 | case 0:
27 | res = int(val)
28 | case 3:
29 | res = (res << 8) | int(val)
30 | default:
31 | res = (res << 7) | int(val)
32 | }
33 | if (! flg) {
34 | break
35 | }
36 | }
37 | return
38 | }
39 |
40 | // UTF-8-vr
41 | func decodeString(rdr *bytes.Reader) (str string, err error) {
42 | // UTF-8-vr = U29S-ref
43 | // UTF-8-vr = U29S-value *(UTF8-char)
44 | u29, err := decodeU29(rdr)
45 | if err != nil {
46 | return
47 | }
48 | flag := (u29 & 1) != 0
49 | len := u29 >> 1
50 |
51 | if (! flag) {
52 | // string reference table index
53 | log.Fatalf("[FIXME] not implemented: UTF-8-vr = U29S-ref")
54 | } else {
55 | buf := make([]byte, len)
56 | if _, err = io.ReadFull(rdr, buf); err != nil {
57 | return
58 | }
59 | str = string(buf)
60 | }
61 | return
62 | }
63 |
64 | func assocOrUtf8Empty(rdr *bytes.Reader) (key string, val interface{}, err error) {
65 | key, err = decodeString(rdr)
66 | if err != nil {
67 | return
68 | }
69 | if key == "" {
70 | //fmt.Printf("assocOrUtf8Empty: string is empty\n")
71 | return
72 | }
73 | val, err = decodeOne(rdr)
74 | if err != nil {
75 | return
76 | }
77 | //log.Fatalf("assocOrUtf8Empty: key=%v, val=%v", key, val)
78 | return
79 | }
80 |
81 | func decodeOne(rdr *bytes.Reader) (res interface{}, err error) {
82 | format, err := rdr.ReadByte()
83 | if err != nil {
84 | return
85 | }
86 | switch format {
87 | case 6: // string-marker
88 | res, err = decodeString(rdr)
89 | if err != nil {
90 | return
91 | }
92 | case 9: // array-marker
93 | // array-marker U29O-ref
94 | // # array-marker U29A-value (UTF-8-empty | * (assoc-value) UTF-8-empty) * (value-type)
95 | // array-marker U29A-value * (assoc-value) UTF-8-empty * (value-type)
96 | // array-marker U29A-value UTF-8-empty * (value-type)
97 | u29, _ := decodeU29(rdr)
98 | flag := u29 & 1 != 0
99 | count := u29 >> 1
100 | if (! flag) {
101 | log.Fatalf("[FIXME] not implemented: array-type = array-marker U29O-ref")
102 | }
103 | if count == 0 { // [FIXME] condition OK?
104 | // associative, terminated by empty string
105 | assoc := make(map[string]interface{})
106 | for {
107 | k, v, e := assocOrUtf8Empty(rdr)
108 | if e != nil {
109 | //fmt.Printf("## amf3 associative: %+v\n", e)
110 | err = e
111 | return
112 | }
113 | if k == "" {
114 | break
115 | }
116 | assoc[k] = v
117 | //log.Printf("AMF3 array: %v = %v", k, v)
118 | }
119 | res = assoc
120 | }
121 | //log.Fatalf("AMF3 array: len: %d", count)
122 | default:
123 | log.Printf("%v\n", res)
124 | log.Fatalf("Not implemented: %d", format)
125 | }
126 | return
127 | }
128 |
129 | func DecodeAll(rdr *bytes.Reader) (res []interface{}, err error) {
130 | for rdr.Len() > 0 {
131 | re, e := decodeOne(rdr)
132 | if e != nil {
133 | err = e
134 | return
135 | }
136 | res = append(res, re)
137 | }
138 | return
139 | }
140 |
141 |
142 | func encodeU29(num int, buff *bytes.Buffer) (err error) {
143 | if (0 <= num && num <= 0x7f) {
144 | if err = buff.WriteByte( byte(num & 0x7f) ); err != nil {
145 | return
146 | }
147 | } else if (0x80 <= num && num <= 0x3fff) {
148 | if err = buff.WriteByte( byte(0x80 | ((num >> 7) & 0x7f)) ); err != nil {
149 | return
150 | }
151 | if err = buff.WriteByte( byte(num & 0x7f) ); err != nil {
152 | return
153 | }
154 | } else if (0x4000 <= num && num <= 0x1fffff) {
155 | if err = buff.WriteByte( byte(0x80 | ((num >> 14) & 0x7f)) ); err != nil {
156 | return
157 | }
158 | if err = buff.WriteByte( byte(0x80 | ((num >> 7) & 0x7f)) ); err != nil {
159 | return
160 | }
161 | if err = buff.WriteByte( byte(num & 0x7f) ); err != nil {
162 | return
163 | }
164 | } else if (0x200000 <= num && num <= 0x3fffffff) {
165 | if err = buff.WriteByte( byte(0x80 | ((num >> 22) & 0x7f)) ); err != nil {
166 | return
167 | }
168 | if err = buff.WriteByte( byte(0x80 | ((num >> 15) & 0x7f)) ); err != nil {
169 | return
170 | }
171 | if err = buff.WriteByte( byte(0x80 | ((num >> 7) & 0x7f)) ); err != nil {
172 | return
173 | }
174 | if err = buff.WriteByte( byte(num & 0xff) ); err != nil {
175 | return
176 | }
177 | } else {
178 | err = fmt.Errorf("u29 overflow")
179 | }
180 | return
181 | }
182 |
183 | func encodeU28Flag(num int, flag bool, buff *bytes.Buffer) (err error) {
184 | if flag {
185 | err = encodeU29(((num << 1) | 1), buff)
186 | } else {
187 | err = encodeU29((num << 1), buff)
188 | }
189 | return
190 | }
191 |
192 |
193 | func encodeArray(data []interface {}, buff *bytes.Buffer) (err error) {
194 | // array-marker
195 | if err = buff.WriteByte(9); err != nil {
196 | return
197 | }
198 | // U29A-value; count of the dense portin of the Array
199 | if err = encodeU28Flag(len(data), true, buff); err != nil {
200 | return
201 | }
202 | // UTF-8-empty
203 | if err = buff.WriteByte(1); err != nil {
204 | return
205 | }
206 | for _, v := range data {
207 | if err = encode(v, buff); err != nil {
208 | return
209 | }
210 | }
211 | return
212 | }
213 |
214 | func encodeStringArray(data []string, buff *bytes.Buffer) error {
215 | var list []interface{}
216 | for _, v := range data {
217 | list = append(list, v)
218 | }
219 | return encodeArray(list, buff)
220 | }
221 | func encodeString(data string, buff *bytes.Buffer) (err error) {
222 | if err = buff.WriteByte(6); err != nil {
223 | return
224 | }
225 | bstr := []byte(data)
226 | // U29S-value
227 | if err = encodeU28Flag(len(bstr), true, buff); err != nil {
228 | return
229 | }
230 | if _, err = buff.Write(bstr); err != nil {
231 | return
232 | }
233 | return
234 | }
235 |
236 | func encode(data interface{}, buff *bytes.Buffer) (err error) {
237 | switch data.(type) {
238 | case string:
239 | err = encodeString(data.(string), buff)
240 | case []string:
241 | err = encodeStringArray(data.([]string), buff)
242 | default:
243 | log.Fatalf("amf0/encode %#v", data)
244 | }
245 | return
246 | }
247 | func Encode(data []interface{}) (b []byte, err error) {
248 | buff := bytes.NewBuffer(nil)
249 | for _, data := range data {
250 | if err = encode(data, buff); err != nil {
251 | return
252 | }
253 | }
254 | b = buff.Bytes()
255 | return
256 | }
--------------------------------------------------------------------------------
/src/amf/amf_t/amf_t.go:
--------------------------------------------------------------------------------
1 | package amf_t
2 |
3 | type AMF3 struct {
4 | Data []interface{}
5 | }
6 |
7 | type SwitchToAmf3 struct {
8 |
9 | }
10 |
11 | type AMF0EcmaArray struct {
12 | Data map[string]interface {}
13 | }
14 |
--------------------------------------------------------------------------------
/src/buildno/buildno.go:
--------------------------------------------------------------------------------
1 |
2 | package buildno
3 |
4 | var BuildDate = "20210405"
5 | var BuildNo = "40"
6 |
--------------------------------------------------------------------------------
/src/buildno/funcs.go:
--------------------------------------------------------------------------------
1 | package buildno
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | )
7 |
8 | func GetBuildNo() string {
9 | return fmt.Sprintf(
10 | "%v.%v-%s-%s",
11 | BuildDate,
12 | BuildNo,
13 | runtime.GOOS,
14 | runtime.GOARCH,
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/cryptoconf/cryptoconf.go:
--------------------------------------------------------------------------------
1 | package cryptoconf
2 |
3 | import (
4 | "golang.org/x/crypto/sha3"
5 | "crypto/aes"
6 | "crypto/cipher"
7 | "crypto/rand"
8 | "io"
9 | "io/ioutil"
10 | "os"
11 | "encoding/json"
12 | "fmt"
13 | "log"
14 | )
15 |
16 | func Set(dataSet map[string]string, fileName, pass string) (err error) {
17 | var data map[string]interface{}
18 | if _, test := os.Stat(fileName); test == nil {
19 | data, err = Load(fileName, pass)
20 | if err != nil {
21 | return
22 | }
23 | } else {
24 | data = map[string]interface{}{}
25 | }
26 | for key, val := range dataSet {
27 | data[key] = val
28 | }
29 |
30 | digest := sha3.Sum256([]byte(pass))
31 | block, err := aes.NewCipher(digest[:])
32 | if err != nil {
33 | log.Fatalln(err)
34 | }
35 | aesgcm, err := cipher.NewGCM(block)
36 | if err != nil {
37 | log.Fatalln(err.Error())
38 | }
39 |
40 | nonceSize := aesgcm.NonceSize()
41 | // Never use more than 2^32 random nonces with a given key because of the risk of a repeat.
42 | nonce := make([]byte, nonceSize)
43 | if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
44 | log.Fatalln(err.Error())
45 | }
46 |
47 | plaintext, err := json.Marshal(data)
48 | if err != nil {
49 | return
50 | }
51 | ciphertext := aesgcm.Seal(nonce, nonce, plaintext, nil)
52 | //fmt.Printf("%#v\n", ciphertext)
53 |
54 | file, err := os.Create(fileName)
55 | if err != nil {
56 | return
57 | }
58 | defer file.Close()
59 | if _, err = file.Write(ciphertext); err != nil {
60 | return
61 | }
62 |
63 | return
64 | }
65 |
66 | func Load(file, pass string) (data map[string]interface{}, err error) {
67 | b, err := ioutil.ReadFile(file)
68 | if err != nil {
69 | err = nil
70 | return
71 | }
72 |
73 | digest := sha3.Sum256([]byte(pass))
74 | block, err := aes.NewCipher(digest[:])
75 | if err != nil {
76 | log.Fatalln(err)
77 | }
78 | aesgcm, err := cipher.NewGCM(block)
79 | if err != nil {
80 | log.Fatalln(err.Error())
81 | }
82 |
83 | nonceSize := aesgcm.NonceSize()
84 |
85 | nonce, ciphertext := b[:nonceSize], b[nonceSize:]
86 |
87 | plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
88 | if err != nil {
89 | err = fmt.Errorf("Password wrong for config: %s", file)
90 | return
91 | }
92 |
93 | ////fmt.Printf("%s\n", plaintext)
94 | data = map[string]interface{}{}
95 | err = json.Unmarshal(plaintext, &data)
96 |
97 | return
98 | }
--------------------------------------------------------------------------------
/src/defines/constant.go:
--------------------------------------------------------------------------------
1 |
2 | package defines
3 |
4 | var Twitter = "@himananiito"
5 | var Email = "himananiito@yahoo.co.jp"
6 |
--------------------------------------------------------------------------------
/src/files/files.go:
--------------------------------------------------------------------------------
1 | package files
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 | "regexp"
9 | )
10 |
11 | func RemoveExtention(fileName string) string {
12 | e := filepath.Ext(fileName)
13 | base := strings.TrimSuffix(fileName, e)
14 | return base
15 | }
16 | func ChangeExtention(fileName, ext string) string {
17 | e := filepath.Ext(fileName)
18 | base := strings.TrimSuffix(fileName, e)
19 | return base + "." + ext
20 | }
21 |
22 | func MkdirByFileName(fileName string) (err error) {
23 | dir := filepath.Dir(fileName)
24 | err = os.MkdirAll(dir, os.ModePerm)
25 | if err != nil {
26 | fmt.Println(err)
27 | return
28 | }
29 | return
30 | }
31 |
32 | func GetFileNameNext(name string) (fileName string, err error) {
33 | fileName = name
34 | _, test := os.Stat(fileName)
35 | if test == nil {
36 | // file Exists
37 | ext := filepath.Ext(fileName)
38 | base := strings.TrimSuffix(fileName, ext)
39 |
40 | var i int
41 | for i = 2; i < 10000000 ; i++ {
42 | fileName = fmt.Sprintf("%s-%d%s", base, i, ext)
43 | _, test := os.Stat(fileName)
44 | if test != nil {
45 | return
46 | }
47 | }
48 | err = fmt.Errorf("too many files: %s", name)
49 | }
50 | return
51 | }
52 |
53 | func ReplaceForbidden(name string) (fileName string) {
54 | fileName = name
55 | fileName = regexp.MustCompile(`\\`).ReplaceAllString(fileName, "¥")
56 | fileName = regexp.MustCompile(`/`).ReplaceAllString(fileName, "∕")
57 | fileName = regexp.MustCompile(`:`).ReplaceAllString(fileName, ":")
58 | fileName = regexp.MustCompile(`\*`).ReplaceAllString(fileName, "*")
59 | fileName = regexp.MustCompile(`\?`).ReplaceAllString(fileName, "?")
60 | fileName = regexp.MustCompile(`"`).ReplaceAllString(fileName, `゛`)
61 | fileName = regexp.MustCompile(`<`).ReplaceAllString(fileName, "<")
62 | fileName = regexp.MustCompile(`>`).ReplaceAllString(fileName, ">")
63 | fileName = regexp.MustCompile(`\|`).ReplaceAllString(fileName, "|")
64 |
65 | fileName = regexp.MustCompile(`)`).ReplaceAllString(fileName, ")")
66 | fileName = regexp.MustCompile(`(`).ReplaceAllString(fileName, "(")
67 |
68 | fileName = regexp.MustCompile(`\p{Zs}+`).ReplaceAllString(fileName, " ")
69 | fileName = regexp.MustCompile(`\A\p{Zs}+|\p{Zs}+\z`).ReplaceAllString(fileName, "")
70 |
71 | // 末尾が.であるようなファイルは作れない
72 | fileName = regexp.MustCompile(`\.\p{Zs}*\z`).ReplaceAllString(fileName, ".")
73 |
74 | return
75 | }
--------------------------------------------------------------------------------
/src/flvs/flv.go:
--------------------------------------------------------------------------------
1 | package flvs
2 |
3 | import (
4 | "os"
5 | "fmt"
6 | "io"
7 | "encoding/binary"
8 | "bytes"
9 | "bufio"
10 | )
11 |
12 | type Flv struct {
13 | filename string
14 | file *os.File
15 | writer *bufio.Writer
16 | startAt int
17 | audioTimestamp int
18 | videoTimestamp int
19 | }
20 | func (flv *Flv) Flush() {
21 | if flv.writer != nil {
22 | flv.writer.Flush()
23 | }
24 | }
25 | func (flv *Flv) Close() {
26 | flv.Flush()
27 | if flv.file != nil {
28 | flv.file.Close()
29 | }
30 | }
31 | func Open(name string) (flv *Flv, err error) {
32 | file, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0777)
33 | if err != nil {
34 | return
35 | }
36 |
37 | flv = &Flv {
38 | filename: name,
39 | file: file,
40 | audioTimestamp: -1,
41 | videoTimestamp: -1,
42 | }
43 |
44 | stat, err := file.Stat()
45 | if err != nil {
46 |
47 | }
48 |
49 | // FLV header
50 | sz := stat.Size()
51 | if sz == 0 {
52 | if err = flv.writeHeader(); err != nil {
53 | flv.Close()
54 | return
55 | }
56 | }
57 |
58 | if err = flv.testHeader(); err != nil {
59 | flv.Close()
60 | return
61 | }
62 |
63 |
64 | flv.lastPacketTimestamp()
65 |
66 | if _, err = flv.file.Seek(0, 2); err != nil {
67 | return
68 | }
69 | ts := flv.GetLastTimestamp()
70 | if ts != 0 {
71 | fmt.Printf("[info] Seek point: %d\n", ts)
72 | }
73 |
74 |
75 | flv.writer = bufio.NewWriterSize(file, 256*1024)
76 |
77 |
78 | return
79 | }
80 | func (flv *Flv) AudioExists() bool {
81 | return flv.audioTimestamp >= 0
82 | }
83 | func (flv *Flv) VideoExists() bool {
84 | return flv.videoTimestamp >= 0
85 | }
86 | func (flv *Flv) testHeader() (err error) {
87 | if _, err = flv.file.Seek(0, 0); err != nil {
88 | return
89 | }
90 |
91 | b := make([]byte, 9)
92 | _, err = io.ReadFull(flv.file, b); if err != nil {
93 | return
94 | }
95 | if "FLV" != string(b[0:3]) {
96 | err = fmt.Errorf("magic number is not FLV")
97 | return
98 | }
99 | offset := binary.BigEndian.Uint32(b[5:9])
100 | flv.startAt = int(offset)
101 |
102 | return
103 | }
104 |
105 | func intToBE24(num int) (data []byte) {
106 | tmp := make([]byte, 4)
107 | binary.BigEndian.PutUint32(tmp, uint32(num))
108 | data = append(data, tmp[1:]...)
109 | return
110 | }
111 | func intToBE32(num int) (data []byte) {
112 | tmp := make([]byte, 4)
113 | binary.BigEndian.PutUint32(tmp, uint32(num))
114 | data = append(data, tmp[:]...)
115 | return
116 | }
117 |
118 | func (flv *Flv) writePacket(tag byte, rdr *bytes.Buffer, ts int) (err error) {
119 | buff := bytes.NewBuffer(nil)
120 |
121 | dataSize := intToBE24(rdr.Len())
122 | tagSize := intToBE32(11 + rdr.Len())
123 |
124 | // TagType
125 | if err = buff.WriteByte(tag); err != nil {
126 | return
127 | }
128 | // DataSize
129 | if _, err = buff.Write(dataSize); err != nil {
130 | return
131 | }
132 |
133 | // Timestamp
134 | tsBytes := intToBE32(ts)
135 | if _, err = buff.Write(tsBytes[1:4]); err != nil {
136 | return
137 | }
138 | // (TimestampExtended)
139 | if err = buff.WriteByte(tsBytes[0]); err != nil {
140 | return
141 | }
142 | // StreamID
143 | if _, err = buff.Write([]byte{0, 0, 0}); err != nil {
144 | return
145 | }
146 |
147 | // header
148 | if _, err = io.Copy(flv.writer, buff); err != nil {
149 | return
150 | }
151 | // data
152 | if _, err = io.Copy(flv.writer, rdr); err != nil {
153 | return
154 | }
155 |
156 | // PreviousTagSize
157 | if _, err = flv.writer.Write(tagSize); err != nil {
158 | return
159 | }
160 |
161 | return
162 | }
163 |
164 | func (flv *Flv) WriteAudio(rdr *bytes.Buffer, ts int) (err error) {
165 | if ts > flv.audioTimestamp {
166 | flv.audioTimestamp = ts
167 | err = flv.writePacket(8, rdr, ts)
168 | }
169 | return
170 | }
171 | func (flv *Flv) WriteVideo(rdr *bytes.Buffer, ts int) (err error) {
172 | if ts > flv.videoTimestamp {
173 | flv.videoTimestamp = ts
174 | err = flv.writePacket(9, rdr, ts)
175 | }
176 | return
177 | }
178 | func (flv *Flv) WriteMetaData(rdr *bytes.Buffer, ts int) (err error) {
179 | err = flv.writePacket(18, rdr, ts)
180 | return
181 | }
182 |
183 | func (flv *Flv) GetLastTimestamp() int {
184 | var min int
185 | if flv.audioTimestamp > flv.videoTimestamp {
186 | min = flv.videoTimestamp
187 | } else {
188 | min = flv.audioTimestamp
189 | }
190 | if min < 0 {
191 | return 0
192 | }
193 | return min
194 | }
195 |
196 | func (flv *Flv) lastPacketTimestamp() (err error) {
197 | defer flv.file.Seek(0, 2)
198 |
199 | if _, err = flv.file.Seek(-4, 2); err != nil {
200 | fmt.Printf("flv.lastPacketTimestamp: %#v\n", err)
201 | return
202 | }
203 |
204 | b0 := make([]byte, 4)
205 | b1 := make([]byte, 11)
206 |
207 | var audioFound bool
208 | var videoFound bool
209 | for !(audioFound && videoFound) {
210 | _, err = io.ReadFull(flv.file, b0); if err != nil {
211 | return
212 | }
213 | size := binary.BigEndian.Uint32(b0)
214 | //fmt.Printf("size: %d\n", size)
215 | if size == 0 {
216 | break
217 | }
218 |
219 | if _, err = flv.file.Seek(-(int64(size) + 4), 1); err != nil {
220 | return
221 | }
222 |
223 | _, err = io.ReadFull(flv.file, b1); if err != nil {
224 | return
225 | }
226 | ts :=
227 | (int(b1[7]) << 24) |
228 | (int(b1[4]) << 16) |
229 | (int(b1[5]) << 8) |
230 | (int(b1[6]) )
231 | //fmt.Printf("ts: %d\n", ts)
232 |
233 | if b1[0] == 8 {
234 | flv.audioTimestamp = ts
235 | audioFound = true
236 | } else if b1[0] == 9 {
237 | flv.videoTimestamp = ts
238 | videoFound = true
239 | }
240 |
241 | if _, err = flv.file.Seek(-(11 + 4), 1); err != nil {
242 | return
243 | }
244 |
245 | }
246 |
247 | return
248 | }
249 |
250 | func (flv *Flv) writeHeader() (err error) {
251 | _, err = flv.file.Write([]byte{
252 | 'F', 'L', 'V',
253 | 1, // FLV version 1
254 | 5, // Audio+Video tags are present
255 | 0, 0, 0, 9, // DataOffset = 9
256 | 0, 0, 0, 0, // PreviousTagSize0
257 | })
258 | return
259 | }
260 |
--------------------------------------------------------------------------------
/src/gorman/gorman.go:
--------------------------------------------------------------------------------
1 | package gorman
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type GoroutineManager struct {
8 | channels map[chan struct{}] struct{}
9 | mtxChan sync.Mutex
10 |
11 | mtxWg sync.Mutex
12 | wg sync.WaitGroup
13 |
14 | codeChecker func(code int)
15 | }
16 |
17 | func NewManager() *GoroutineManager {
18 | return &GoroutineManager{
19 | channels: map[chan struct{}] struct{}{},
20 | }
21 | }
22 | func WithChecker(f func(int)) *GoroutineManager {
23 | return &GoroutineManager{
24 | channels: map[chan struct{}] struct{}{},
25 | codeChecker: f,
26 | }
27 | }
28 | func (gm *GoroutineManager) addChan(c chan struct{}) {
29 | gm.mtxChan.Lock()
30 | defer gm.mtxChan.Unlock()
31 | gm.channels[c] = struct{}{}
32 | }
33 | func (gm *GoroutineManager) delChan(c chan struct{}) {
34 | gm.mtxChan.Lock()
35 | defer gm.mtxChan.Unlock()
36 | delete(gm.channels, c)
37 | }
38 | func (gm *GoroutineManager) Cancel() {
39 | gm.mtxChan.Lock()
40 | defer gm.mtxChan.Unlock()
41 | for c, _ := range gm.channels {
42 | close(c)
43 | delete(gm.channels, c)
44 | }
45 | }
46 | func (gm *GoroutineManager) Count() int {
47 | gm.mtxChan.Lock()
48 | defer gm.mtxChan.Unlock()
49 | return len(gm.channels)
50 | }
51 | func (gm *GoroutineManager) Go(f func(<-chan struct{}) int) {
52 | gm.wg.Add(1)
53 | stopChan := make(chan struct{}, 1)
54 | gm.addChan(stopChan)
55 |
56 | go func(){
57 | defer gm.wg.Done()
58 | code := f(stopChan)
59 | gm.delChan(stopChan)
60 | if gm.codeChecker != nil {
61 | gm.codeChecker(code)
62 | }
63 | }()
64 | }
65 | func (gm *GoroutineManager) RegisterCodeChecker(f func(int)) {
66 | gm.codeChecker = f
67 | }
68 | func (gm *GoroutineManager) Wait() {
69 | gm.wg.Wait()
70 | }
71 |
--------------------------------------------------------------------------------
/src/httpbase/httpbase.go:
--------------------------------------------------------------------------------
1 | package httpbase
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | "os"
7 | "fmt"
8 | "io"
9 | "io/ioutil"
10 | "strings"
11 | "net/http"
12 | "net/url"
13 | "time"
14 | "errors"
15 | "encoding/json"
16 | "encoding/pem"
17 | "bytes"
18 |
19 | "../buildno"
20 | "../defines"
21 | )
22 |
23 | func GetUserAgent() string {
24 | return fmt.Sprintf(
25 | "livedl/%s (contact: twitter=%s, email=%s)",
26 | buildno.GetBuildNo(),
27 | defines.Twitter,
28 | defines.Email,
29 | )
30 | }
31 |
32 | var Client = &http.Client{
33 | Timeout: time.Duration(5) * time.Second,
34 | CheckRedirect: func(req *http.Request, via []*http.Request) (err error) {
35 | if req != nil && via != nil && len(via) > 0 {
36 | if len(via) >= 10 {
37 | return errors.New("stopped after 10 redirects")
38 | }
39 | req.Header = via[0].Header
40 | }
41 | return nil
42 | },
43 | }
44 |
45 | func checkTransport() bool {
46 | if Client.Transport == nil {
47 | Client.Transport = &http.Transport{}
48 | }
49 | switch Client.Transport.(type) {
50 | case *http.Transport:
51 | return true
52 | }
53 | return false
54 | }
55 | func checkTLSClientConfig() bool {
56 | if !checkTransport() {
57 | return false
58 | }
59 |
60 | if Client.Transport.(*http.Transport).TLSClientConfig == nil {
61 | Client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{}
62 | }
63 |
64 | return true
65 | }
66 | func SetRootCA(file string) (err error) {
67 | if ! checkTLSClientConfig() {
68 | err = fmt.Errorf("SetRootCA: check failed")
69 | return
70 | }
71 |
72 | dat, err := ioutil.ReadFile(file)
73 | if err != nil {
74 | return
75 | }
76 |
77 | // try decode pem
78 | var nDecode int
79 | for len(dat) > 0 {
80 | block, d := pem.Decode(dat)
81 | if block == nil {
82 | break
83 | }
84 | dat = d
85 | nDecode++
86 | if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
87 | continue
88 | }
89 | addCert(block.Bytes)
90 | }
91 | if nDecode < 1 {
92 | addCert(dat)
93 | }
94 |
95 | return
96 | }
97 | func addCert(dat []byte) (err error) {
98 | certs, err := x509.ParseCertificates(dat)
99 | if err != nil {
100 | return
101 | }
102 | if certs == nil {
103 | err = fmt.Errorf("ParseCertificates failed")
104 | return
105 | }
106 |
107 | if len(certs) > 0 {
108 | if Client.Transport.(*http.Transport).TLSClientConfig.RootCAs == nil {
109 | Client.Transport.(*http.Transport).TLSClientConfig.RootCAs = x509.NewCertPool()
110 | }
111 | }
112 |
113 | for _, cert := range certs {
114 | Client.Transport.(*http.Transport).TLSClientConfig.RootCAs.AddCert(cert)
115 | }
116 | return
117 | }
118 |
119 | func SetSkipVerify(skip bool) (err error) {
120 | if checkTLSClientConfig() {
121 | Client.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = skip
122 | } else {
123 | err = fmt.Errorf("SetSkipVerify(%#v): check failed", skip)
124 | }
125 | return
126 | }
127 | func SetProxy(rawurl string) (err error) {
128 | if ! checkTransport() {
129 | return fmt.Errorf("SetProxy(%#v): check failed", rawurl)
130 | }
131 |
132 | u, err := url.Parse(rawurl)
133 | if err != nil {
134 | return
135 | }
136 | Client.Transport.(*http.Transport).Proxy = http.ProxyURL(u)
137 | return
138 | }
139 |
140 | func httpBase(method, uri string, header map[string]string, body io.Reader) (resp *http.Response, err, neterr error) {
141 | req, err := http.NewRequest(method, uri, body)
142 | if err != nil {
143 | return
144 | }
145 |
146 | req.Header.Set("User-Agent", GetUserAgent())
147 |
148 | for k, v := range header {
149 | req.Header.Set(k, v)
150 | }
151 |
152 | resp, neterr = Client.Do(req)
153 | if neterr != nil {
154 | if strings.Contains(neterr.Error(), "x509: certificate signed by unknown") {
155 | fmt.Println(neterr)
156 | os.Exit(10)
157 | }
158 | return
159 | }
160 | return
161 | }
162 | func Get(uri string, header map[string]string) (*http.Response, error, error) {
163 | return httpBase("GET", uri, header, nil)
164 | }
165 | func PostForm(uri string, header map[string]string, val url.Values) (*http.Response, error, error) {
166 | if header == nil {
167 | header = make(map[string]string)
168 | }
169 | header["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
170 | return httpBase("POST", uri, header, strings.NewReader(val.Encode()))
171 | }
172 | func reqJson(method, uri string, header map[string]string, data interface{}) (
173 | *http.Response, error, error) {
174 | encoded, err := json.Marshal(data)
175 | if err != nil {
176 | return nil, err, nil
177 | }
178 |
179 | if header == nil {
180 | header = make(map[string]string)
181 | }
182 | header["Content-Type"] = "application/json"
183 |
184 | return httpBase(method, uri, header, bytes.NewReader(encoded))
185 | }
186 | func PostJson(uri string, header map[string]string, data interface{}) (*http.Response, error, error) {
187 | return reqJson("POST", uri, header, data)
188 | }
189 | func PutJson(uri string, header map[string]string, data interface{}) (*http.Response, error, error) {
190 | return reqJson("PUT", uri, header, data)
191 | }
192 | func PostData(uri string, header map[string]string, data io.Reader) (*http.Response, error, error) {
193 | if header == nil {
194 | header = make(map[string]string)
195 | }
196 | return httpBase("POST", uri, header, data)
197 | }
198 | func GetBytes(uri string, header map[string]string) (code int, buff []byte, err, neterr error) {
199 | resp, err, neterr := Get(uri, header)
200 | if err != nil {
201 | return
202 | }
203 | if neterr != nil {
204 | return
205 | }
206 | defer resp.Body.Close()
207 |
208 | buff, neterr = ioutil.ReadAll(resp.Body)
209 | if neterr != nil {
210 | return
211 | }
212 |
213 | code = resp.StatusCode
214 |
215 | return
216 | }
--------------------------------------------------------------------------------
/src/httpsub/httpsub.go:
--------------------------------------------------------------------------------
1 |
2 | package httpsub
3 |
4 | import (
5 | "net/http"
6 | "os"
7 | "sync"
8 | "log"
9 | "io"
10 | "fmt"
11 | "bytes"
12 | )
13 |
14 | type SubDownloader struct {
15 | method string
16 | uri string
17 | data []byte
18 | Header map[string]string
19 | RangeSize int64
20 | BuffSize int64
21 | fileName string
22 | file *os.File
23 | numConcurrent int
24 | chRunning chan bool
25 | mtx sync.Mutex
26 | wg sync.WaitGroup
27 | chLength chan int64
28 | }
29 | func (sub *SubDownloader) Concurrent(c int) {
30 | sub.numConcurrent = c
31 | }
32 | func Get(uri, fileName string) (sub *SubDownloader) {
33 | sub = &SubDownloader{
34 | method: "GET",
35 | uri: uri,
36 | fileName: fileName,
37 | }
38 | return
39 | }
40 | func (sub *SubDownloader) Close() {
41 | sub.mtx.Lock()
42 | defer sub.mtx.Unlock()
43 | if sub.file != nil {
44 | sub.file.Close()
45 | sub.file = nil
46 | }
47 | }
48 | func (sub *SubDownloader) open() {
49 | f, err := os.Create(sub.fileName)
50 | if err != nil {
51 | log.Fatal(err)
52 | }
53 | sub.file = f
54 | }
55 | func (sub *SubDownloader) write(pos int64, rdr io.Reader) (err error) {
56 | sub.mtx.Lock()
57 | defer sub.mtx.Unlock()
58 | //fmt.Printf("write %d\n", pos)
59 | if sub.file == nil {
60 | sub.open()
61 | }
62 | if _, err = sub.file.Seek(pos, 0); err != nil {
63 | log.Fatalln(err)
64 | }
65 | if _, err = io.Copy(sub.file, rdr); err != nil {
66 | log.Fatalln(err)
67 | }
68 | return
69 | }
70 | func (sub *SubDownloader) subrange(pos int64) {
71 | //fmt.Printf("start subrange pos(%d), size(%d) \n", pos, sub.RangeSize)
72 | sub.wg.Add(1)
73 | sub.chRunning <- true
74 | go func() {
75 | defer func() {
76 | <-sub.chRunning
77 | sub.wg.Done()
78 | }()
79 | data := bytes.NewBuffer(sub.data)
80 | req, _ := http.NewRequest(sub.method, sub.uri, data)
81 | req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", pos, pos + sub.RangeSize - 1))
82 |
83 | client := new(http.Client)
84 | resp, err := client.Do(req)
85 | if err != nil {
86 | return
87 | }
88 | defer resp.Body.Close()
89 |
90 | switch resp.StatusCode {
91 | case 206:
92 | default:
93 | log.Fatalf("StatusCode is %v\n", resp.StatusCode)
94 | }
95 | sub.chLength <- resp.ContentLength
96 |
97 | buff := new(bytes.Buffer)
98 | wbytes := int64(0)
99 | for {
100 | n, _ := io.CopyN(buff, resp.Body, sub.BuffSize)
101 | //fmt.Printf("buff size is %d\n", buff.Len())
102 | if n > 0 {
103 | sub.write(pos + wbytes, buff)
104 | wbytes += n
105 | } else {
106 | return
107 | }
108 | }
109 | }()
110 | }
111 | func (sub *SubDownloader) Wait() {
112 | sub.chRunning = make(chan bool, sub.numConcurrent)
113 | sub.chLength = make(chan int64, 10)
114 |
115 | if sub.RangeSize <= 0 {
116 | sub.RangeSize = 10*1000*1000
117 | }
118 |
119 | if sub.BuffSize <= 0 {
120 | sub.BuffSize = 3*1000*1000
121 | }
122 |
123 | pos := int64(0)
124 | for {
125 | sub.subrange(pos)
126 | length := <-sub.chLength
127 | fmt.Printf("Downloading %v: %v-%v\n", sub.fileName, pos, pos + length - 1)
128 | if length == sub.RangeSize {
129 | pos += length
130 | } else {
131 | break
132 | }
133 | }
134 | sub.wg.Wait()
135 | sub.Close()
136 | }
137 |
--------------------------------------------------------------------------------
/src/livedl.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "regexp"
8 | "./options"
9 | "./twitcas"
10 | "./niconico"
11 | "./youtube"
12 | "./zip2mp4"
13 | "time"
14 | "strings"
15 | "./httpbase"
16 | )
17 |
18 | func main() {
19 | var baseDir string
20 | if regexp.MustCompile(`\AC:\\.*\\Temp\\go-build[^\\]*\\[^\\]+\\exe\\[^\\]*\.exe\z`).MatchString(os.Args[0]) {
21 | // go runで起動時
22 | pwd, e := os.Getwd()
23 | if e != nil {
24 | fmt.Println(e)
25 | return
26 | }
27 | baseDir = pwd
28 | } else {
29 | //pa, e := filepath.Abs(os.Args[0])
30 | pa, e := os.Executable()
31 | if e != nil {
32 | fmt.Println(e)
33 | return
34 | }
35 |
36 | // symlinkを追跡する
37 | for {
38 | sl, e := os.Readlink(pa)
39 | if e != nil {
40 | break
41 | }
42 | pa = sl
43 | }
44 | baseDir = filepath.Dir(pa)
45 | }
46 |
47 | opt := options.ParseArgs()
48 |
49 | // chdir if not disabled
50 | if !opt.NoChdir {
51 | fmt.Printf("chdir: %s\n", baseDir)
52 | if e := os.Chdir(baseDir); e != nil {
53 | fmt.Println(e)
54 | return
55 | }
56 | }
57 |
58 | // http
59 | if opt.HttpRootCA != "" {
60 | if err := httpbase.SetRootCA(opt.HttpRootCA); err != nil {
61 | fmt.Println(err)
62 | return
63 | }
64 | }
65 | if opt.HttpSkipVerify {
66 | if err := httpbase.SetSkipVerify(true); err != nil {
67 | fmt.Println(err)
68 | return
69 | }
70 | }
71 | if opt.HttpProxy != "" {
72 | if err := httpbase.SetProxy(opt.HttpProxy); err != nil {
73 | fmt.Println(err)
74 | return
75 | }
76 | }
77 |
78 | switch opt.Command {
79 | default:
80 | fmt.Printf("Unknown command: %v\n", opt.Command)
81 | os.Exit(1)
82 |
83 | case "TWITCAS":
84 | var doneTime int64
85 | for {
86 | done, dbLocked := twitcas.TwitcasRecord(opt.TcasId, "")
87 | if dbLocked {
88 | break
89 | }
90 | if (! opt.TcasRetry) {
91 | break
92 | }
93 |
94 | if opt.TcasRetryTimeoutMinute < 0 {
95 |
96 | } else if done {
97 | doneTime = time.Now().Unix()
98 |
99 | } else {
100 | if doneTime == 0 {
101 | doneTime = time.Now().Unix()
102 | } else {
103 | delta := time.Now().Unix() - doneTime
104 | var minutes int
105 | if opt.TcasRetryTimeoutMinute == 0 {
106 | minutes = options.DefaultTcasRetryTimeoutMinute
107 | } else {
108 | minutes = opt.TcasRetryTimeoutMinute
109 | }
110 |
111 | if minutes > 0 {
112 | if delta > int64(minutes * 60) {
113 | break
114 | }
115 | }
116 | }
117 | }
118 |
119 | var interval int
120 | if opt.TcasRetryInterval <= 0 {
121 | interval = options.DefaultTcasRetryInterval
122 | } else {
123 | interval = opt.TcasRetryInterval
124 | }
125 | select {
126 | case <-time.After(time.Duration(interval) * time.Second):
127 | }
128 | }
129 |
130 | case "YOUTUBE":
131 | err := youtube.Record(opt.YoutubeId, opt.YtNoStreamlink, opt.YtNoYoutubeDl)
132 | if err != nil {
133 | fmt.Println(err)
134 | }
135 |
136 | case "NICOLIVE":
137 | hlsPlaylistEnd, dbname, err := niconico.Record(opt);
138 | if err != nil {
139 | fmt.Println(err)
140 | os.Exit(1)
141 | }
142 | if hlsPlaylistEnd && opt.NicoAutoConvert {
143 | done, nMp4s, skipped, err := zip2mp4.ConvertDB(dbname, opt.ConvExt, opt.NicoSkipHb, opt.NicoConvForceConcat, opt.NicoConvSeqnoStart, opt.NicoConvSeqnoEnd)
144 | if err != nil {
145 | fmt.Println(err)
146 | os.Exit(1)
147 | }
148 | if done {
149 | if nMp4s == 1 && (! skipped) {
150 | if 1 <= opt.NicoAutoDeleteDBMode {
151 | os.Remove(dbname)
152 | }
153 | } else if 1 < nMp4s || (nMp4s == 1 && skipped) {
154 | if 2 <= opt.NicoAutoDeleteDBMode {
155 | os.Remove(dbname)
156 | }
157 | }
158 | }
159 | }
160 | case "NICOLIVE_TEST":
161 | if err := niconico.TestRun(opt); err != nil {
162 | fmt.Println(err)
163 | os.Exit(1)
164 | }
165 |
166 | case "ZIP2MP4":
167 | if err := zip2mp4.Convert(opt.ZipFile); err != nil {
168 | fmt.Println(err)
169 | os.Exit(1)
170 | }
171 |
172 | case "DB2MP4":
173 | if strings.HasSuffix(opt.DBFile, ".yt.sqlite3") {
174 | zip2mp4.YtComment(opt.DBFile)
175 |
176 | } else if opt.ExtractChunks {
177 | if _, err := zip2mp4.ExtractChunks(opt.DBFile, opt.NicoSkipHb); err != nil {
178 | fmt.Println(err)
179 | os.Exit(1)
180 | }
181 |
182 | } else {
183 | if _, _, _, err := zip2mp4.ConvertDB(opt.DBFile, opt.ConvExt, opt.NicoSkipHb, opt.NicoConvForceConcat, opt.NicoConvSeqnoStart, opt.NicoConvSeqnoEnd); err != nil {
184 | fmt.Println(err)
185 | os.Exit(1)
186 | }
187 | }
188 |
189 | case "DB2HLS":
190 | if opt.NicoHlsPort == 0 {
191 | fmt.Println("HLS port not specified")
192 | os.Exit(1)
193 | }
194 | if err := zip2mp4.ReplayDB(opt.DBFile, opt.NicoHlsPort, opt.NicoConvSeqnoStart); err != nil {
195 | fmt.Println(err)
196 | os.Exit(1)
197 | }
198 | }
199 |
200 |
201 | return
202 | }
203 |
--------------------------------------------------------------------------------
/src/log4gui/log4gui.go:
--------------------------------------------------------------------------------
1 | package log4gui
2 |
3 | import (
4 | "fmt"
5 | "encoding/json"
6 | )
7 |
8 | func print(k, v string) {
9 | bs, e := json.Marshal(map[string]string{
10 | k: v,
11 | })
12 | if(e != nil) {
13 | fmt.Println(e)
14 | return
15 | }
16 | fmt.Println("$" + string(bs) + "$")
17 | }
18 | func Info(s string) {
19 | print("Info", s)
20 | }
21 | func Error(s string) {
22 | print("Error", s)
23 | }
--------------------------------------------------------------------------------
/src/niconico/jikken.gox:
--------------------------------------------------------------------------------
1 |
2 |
3 | package niconico
4 |
5 | import (
6 | "fmt"
7 | "os"
8 | "time"
9 | "os/signal"
10 | "syscall"
11 | "net/http"
12 | "io/ioutil"
13 | "log"
14 | "encoding/json"
15 | "bytes"
16 | "../options"
17 | "../obj"
18 | "../files"
19 | )
20 |
21 |
22 | func getActionTrackId() (actionTrackId string, err error) {
23 | uri := "https://public.api.nicovideo.jp/v1/action-track-ids.json"
24 | req, _ := http.NewRequest("POST", uri, nil)
25 |
26 | req.Header.Set("Content-Type", "application/json")
27 |
28 | client := new(http.Client)
29 | resp, e := client.Do(req)
30 | if e != nil {
31 | err = e
32 | return
33 | }
34 | defer resp.Body.Close()
35 | bs, err := ioutil.ReadAll(resp.Body)
36 | if err != nil {
37 | log.Println(err)
38 | }
39 |
40 | var props interface{}
41 | if err = json.Unmarshal(bs, &props); err != nil {
42 | return
43 | }
44 |
45 | //obj.PrintAsJson(props)
46 |
47 | data, ok := obj.FindString(props, "data")
48 | if (! ok) {
49 | err = fmt.Errorf("actionTrackId not found")
50 | }
51 | actionTrackId = data
52 |
53 | return
54 | }
55 |
56 | func jikkenWatching(opt options.Option, actionTrackId string, isArchive bool) (props interface{}, err error) {
57 |
58 | str, _ := json.Marshal(OBJ{
59 | "actionTrackId": actionTrackId,
60 | "isBroadcaster": false,
61 | "isLowLatencyStream": true,
62 | "streamCapacity": "superhigh",
63 | "streamProtocol": "https",
64 | "streamQuality": "auto", // high, auto
65 | })
66 | if err != nil {
67 | log.Println(err)
68 | return
69 | }
70 |
71 | data := bytes.NewReader(str)
72 |
73 | var uri string
74 | if isArchive {
75 | uri = fmt.Sprintf("https://api.cas.nicovideo.jp/v1/services/live/programs/%s/watching-archive", opt.NicoLiveId)
76 | } else {
77 | uri = fmt.Sprintf("https://api.cas.nicovideo.jp/v1/services/live/programs/%s/watching", opt.NicoLiveId)
78 | }
79 | req, _ := http.NewRequest("POST", uri, data)
80 |
81 | //if opt.NicoSession != "" {
82 | req.Header.Set("Cookie", "user_session=" + opt.NicoSession)
83 | //}
84 | req.Header.Set("Accept", "application/json")
85 | req.Header.Set("Content-Type", "application/json")
86 | req.Header.Set("Origin", "https://cas.nicovideo.jp")
87 | req.Header.Set("X-Connection-Environment", "ethernet")
88 | req.Header.Set("X-Frontend-Id", "91")
89 |
90 | client := new(http.Client)
91 | resp, e := client.Do(req)
92 | if e != nil {
93 | err = e
94 | return
95 | }
96 | defer resp.Body.Close()
97 | bs, err := ioutil.ReadAll(resp.Body)
98 | if err != nil {
99 | log.Println(err)
100 | }
101 |
102 | if err = json.Unmarshal([]byte(bs), &props); err != nil {
103 | return
104 | }
105 |
106 | //obj.PrintAsJson(props)
107 |
108 | return
109 | }
110 |
111 |
112 | func jikkenPut(opt options.Option, actionTrackId string) (forbidden, notOnAir bool, err error) {
113 | str, _ := json.Marshal(OBJ{
114 | "actionTrackId": actionTrackId,
115 | "isBroadcaster": false,
116 | })
117 | if err != nil {
118 | log.Println(err)
119 | }
120 | fmt.Printf("\n%s\n\n", str)
121 |
122 | data := bytes.NewReader(str)
123 |
124 | uri := fmt.Sprintf("https://api.cas.nicovideo.jp/v1/services/live/programs/%s/watching", opt.NicoLiveId)
125 | req, _ := http.NewRequest("PUT", uri, data)
126 |
127 | //if opt.NicoSession != "" {
128 | req.Header.Set("Cookie", "user_session=" + opt.NicoSession)
129 | //}
130 | req.Header.Set("Accept", "application/json")
131 | req.Header.Set("Content-Type", "application/json")
132 | req.Header.Set("Origin", "https://cas.nicovideo.jp")
133 | req.Header.Set("X-Frontend-Id", "91")
134 |
135 | client := new(http.Client)
136 | resp, e := client.Do(req)
137 | if e != nil {
138 | err = e
139 | return
140 | }
141 | defer resp.Body.Close()
142 | bs, err := ioutil.ReadAll(resp.Body)
143 | if err != nil {
144 | log.Println(err)
145 | }
146 |
147 | var props interface{}
148 | if err = json.Unmarshal([]byte(bs), &props); err != nil {
149 | return
150 | }
151 |
152 | //obj.PrintAsJson(props)
153 |
154 | if code, ok := obj.FindString(props, "meta", "errorCode"); ok {
155 | switch code {
156 | case "FORBIDDEN":
157 | forbidden = true
158 | return
159 | case "PROGRAM_NOT_ONAIR":
160 | notOnAir = true
161 | return
162 | }
163 | }
164 |
165 | return
166 | }
167 |
168 |
169 | func jikkenHousou(nicoliveProgramId, title, userId, nickname, communityId string, opt options.Option, isArchive bool) (err error) {
170 |
171 | chInterrupt := make(chan os.Signal, 10)
172 | signal.Notify(chInterrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
173 |
174 | actionTrackId, err := getActionTrackId()
175 | if err != nil {
176 | log.Println(err)
177 | }
178 |
179 | media := &NicoMedia{}
180 |
181 | defer func() {
182 | if media.zipWriter != nil {
183 | media.zipWriter.Close()
184 | }
185 | }()
186 |
187 | title = files.ReplaceForbidden(title)
188 | nickname = files.ReplaceForbidden(nickname)
189 | media.fileName = fmt.Sprintf("%s-%s-%s.zip", nicoliveProgramId, nickname, title)
190 |
191 |
192 | var nLast int
193 | L_main: for {
194 | select {
195 | case <-chInterrupt:
196 | break L_main
197 | default:
198 | }
199 | props, e := jikkenWatching(opt, actionTrackId, isArchive)
200 | if e != nil {
201 | err = e
202 | log.Println(err)
203 | return
204 | }
205 |
206 | if uri, ok := obj.FindString(props, "data", "streamServer", "url"); ok {
207 | //fmt.Println(uri)
208 |
209 | is403, e := media.SetPlaylist(uri)
210 | if is403 {
211 | break L_main
212 | }
213 | if e != nil {
214 | err = e
215 | log.Println(e)
216 | return
217 | }
218 | }
219 |
220 | L_loc: for i := 0; true; i++ {
221 | select {
222 | case <-chInterrupt:
223 | break L_main
224 | default:
225 | }
226 |
227 | is403, e := media.GetMedias()
228 | if is403 {
229 | n := media.getNumChunk()
230 | if n != nLast {
231 | nLast = n
232 | break L_loc
233 | } else {
234 | break L_main
235 | }
236 | }
237 | if e != nil {
238 | log.Println(e)
239 | return
240 | }
241 | if i > 60 {
242 | forbidden, notOnAir, e := jikkenPut(opt, actionTrackId)
243 | if e != nil {
244 | err = e
245 | log.Println(e)
246 | return
247 | }
248 | if notOnAir {
249 | break L_main
250 | }
251 | if forbidden {
252 | break L_loc
253 | }
254 | i = 0
255 | }
256 | select {
257 | case <-chInterrupt:
258 | break L_main
259 | case <-time.After(1 * time.Second):
260 | }
261 | }
262 | }
263 | if media.zipWriter != nil {
264 | media.zipWriter.Close()
265 | }
266 |
267 | signal.Stop(chInterrupt)
268 |
269 | return
270 | }
271 |
--------------------------------------------------------------------------------
/src/niconico/nico.go:
--------------------------------------------------------------------------------
1 |
2 | package niconico
3 |
4 | import (
5 | "fmt"
6 | "net/http"
7 | "net/url"
8 | "strings"
9 | "../options"
10 | "io/ioutil"
11 | "regexp"
12 | "strconv"
13 | "os"
14 | "net"
15 | "encoding/xml"
16 | "bufio"
17 | "os/signal"
18 | "syscall"
19 | "runtime"
20 | _ "net/http/pprof"
21 | "../httpbase"
22 | )
23 |
24 | func NicoLogin(opt options.Option) (err error) {
25 | id, pass, _, _ := options.LoadNicoAccount(opt.NicoLoginAlias)
26 |
27 | if id == "" || pass == "" {
28 | err = fmt.Errorf("Login ID/Password not set. Use -nico-login \",\"")
29 | return
30 | }
31 |
32 | resp, err, neterr := httpbase.PostForm(
33 | "https://account.nicovideo.jp/api/v1/login",
34 | nil,
35 | url.Values{"mail_tel": {id}, "password": {pass}, "site": {"nicoaccountsdk"}},
36 | )
37 | if err != nil {
38 | return
39 | }
40 | if neterr != nil {
41 | err = neterr
42 | return
43 | }
44 | defer resp.Body.Close()
45 |
46 | body, err := ioutil.ReadAll(resp.Body)
47 | if err != nil {
48 | return
49 | }
50 |
51 | if ma := regexp.MustCompile(`(.+?)`).FindSubmatch(body); len(ma) > 0 {
52 | options.SetNicoSession(opt.NicoLoginAlias, string(ma[1]))
53 |
54 | fmt.Println("login success")
55 | } else {
56 | err = fmt.Errorf("login failed: session_key not found")
57 | return
58 | }
59 | return
60 | }
61 |
62 | func Record(opt options.Option) (hlsPlaylistEnd bool, dbName string, err error) {
63 |
64 | for i := 0; i < 2; i++ {
65 | // load session info
66 | if opt.NicoSession == "" || i > 0 {
67 | _, _, opt.NicoSession, _ = options.LoadNicoAccount(opt.NicoLoginAlias)
68 | }
69 |
70 | if (! opt.NicoRtmpOnly) {
71 | var done bool
72 | var notLogin bool
73 | var reserved bool
74 | done, hlsPlaylistEnd, notLogin, reserved, dbName, err = NicoRecHls(opt)
75 | if done {
76 | return
77 | }
78 | if err != nil {
79 | return
80 | }
81 | if notLogin {
82 | fmt.Println("not_login")
83 | if err = NicoLogin(opt); err != nil {
84 | return
85 | }
86 | continue
87 | }
88 | if reserved {
89 | continue
90 | }
91 | }
92 |
93 | if (! opt.NicoHlsOnly) {
94 | notLogin, e := NicoRecRtmp(opt)
95 | if e != nil {
96 | err = e
97 | return
98 | }
99 | if notLogin {
100 | fmt.Println("not_login")
101 | if err = NicoLogin(opt); err != nil {
102 | return
103 | }
104 | continue
105 | }
106 | }
107 |
108 | break
109 | }
110 |
111 | return
112 | }
113 |
114 | func TestRun(opt options.Option) (err error) {
115 |
116 | go func() {
117 | fmt.Println(http.ListenAndServe("localhost:6060", nil))
118 | }()
119 |
120 |
121 | if false {
122 | ch := make(chan os.Signal, 10)
123 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
124 | go func() {
125 | <-ch
126 | os.Exit(0)
127 | }()
128 | }
129 |
130 | opt.NicoRtmpIndex = map[int]bool{
131 | 0: true,
132 | }
133 |
134 | var nextId func() string
135 |
136 | if opt.NicoLiveId == "" {
137 | // niconama alert
138 |
139 | if opt.NicoTestTimeout <= 0 {
140 | opt.NicoTestTimeout = 12
141 | }
142 |
143 |
144 | resp, e, nete := httpbase.Get("http://live.nicovideo.jp/api/getalertinfo", nil)
145 | if e != nil {
146 | err = e
147 | return
148 | }
149 | if nete != nil {
150 | err = nete
151 | return
152 | }
153 | defer resp.Body.Close()
154 |
155 | switch resp.StatusCode {
156 | case 200:
157 | default:
158 | err = fmt.Errorf("StatusCode is %v", resp.StatusCode)
159 | return
160 | }
161 |
162 | type Alert struct {
163 | User string `xml:"user_id"`
164 | UserHash string `xml:"user_hash"`
165 | Addr string `xml:"ms>addr"`
166 | Port string `xml:"ms>port"`
167 | Thread string `xml:"ms>thread"`
168 | }
169 | status := &Alert{}
170 | dat, _ := ioutil.ReadAll(resp.Body)
171 | resp.Body.Close()
172 |
173 | err = xml.Unmarshal(dat, status)
174 | if err != nil {
175 | fmt.Println(string(dat))
176 | fmt.Printf("error: %v", err)
177 | return
178 | }
179 |
180 | raddr, e := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%s", status.Addr, status.Port))
181 | if e != nil {
182 | fmt.Printf("%v\n", e)
183 | return
184 | }
185 |
186 | conn, e := net.DialTCP("tcp", nil, raddr)
187 | if e != nil {
188 | err = e
189 | return
190 | }
191 | defer conn.Close()
192 |
193 | msg := fmt.Sprintf(`%c`, status.Thread, 0)
194 | if _, err = conn.Write([]byte(msg)); err != nil {
195 | fmt.Println(err)
196 | return
197 | }
198 |
199 | rdr := bufio.NewReader(conn)
200 |
201 | chLatest := make(chan string, 1000)
202 | go func(){
203 | for {
204 | s, e := rdr.ReadString(0)
205 | if e != nil {
206 | fmt.Println(e)
207 | err = e
208 | return
209 | }
210 | //fmt.Println(s)
211 | if ma := regexp.MustCompile(`>(\d+),\S+,\S+<`).FindStringSubmatch(s); len(ma) > 0 {
212 | L0:for {
213 | select {
214 | case <-chLatest:
215 | default:
216 | break L0
217 | }
218 | }
219 | chLatest <- ma[1]
220 | }
221 | }
222 | }()
223 |
224 | nextId = func() (string) {
225 | L1:for {
226 | select {
227 | case <-chLatest:
228 | default:
229 | break L1
230 | }
231 | }
232 | return <-chLatest
233 | }
234 |
235 | } else {
236 | // start from NicoLiveId
237 | var id int64
238 | if ma := regexp.MustCompile(`\Alv(\d+)\z`).FindStringSubmatch(opt.NicoLiveId); len(ma) > 0 {
239 | if id, err = strconv.ParseInt(ma[1], 10, 64); err != nil {
240 | fmt.Println(err)
241 | return
242 | }
243 | } else {
244 | fmt.Println("TestRun: NicoLiveId not specified")
245 | return
246 | }
247 |
248 | nextId = func() (s string) {
249 | s = fmt.Sprintf("%d", id)
250 | id++
251 | return
252 | }
253 | }
254 |
255 | if opt.NicoTestTimeout <= 0 {
256 | opt.NicoTestTimeout = 3
257 | }
258 |
259 | //chErr := make(chan error)
260 | var NFCount int
261 | var endCount int
262 | for {
263 | opt.NicoLiveId = fmt.Sprintf("lv%s", nextId())
264 |
265 | fmt.Fprintf(os.Stderr, "start test: %s\n", opt.NicoLiveId)
266 | fmt.Fprintf(os.Stderr, "# NumGoroutine: %d\n", runtime.NumGoroutine())
267 |
268 | var msg string
269 | _, _, err = Record(opt)
270 | if err != nil {
271 | if ma := regexp.MustCompile(`\AError\s+code:\s*(\S+)`).FindStringSubmatch(err.Error()); len(ma) > 0 {
272 | msg = ma[1]
273 | switch ma[1] {
274 | case "notfound", "closed", "comingsoon", "timeshift_ticket_exhaust":
275 | case "deletedbyuser", "deletedbyvisor", "violated":
276 | case "usertimeshift", "tsarchive", "require_community_member",
277 | "noauth", "full", "premium_only", "selected-country":
278 | default:
279 | fmt.Fprintf(os.Stderr, "unknown: %s\n", ma[1])
280 | return
281 | }
282 |
283 | } else if strings.Contains(err.Error(), "closed network") {
284 | msg = "OK"
285 | } else {
286 | fmt.Fprintln(os.Stderr, err)
287 | return
288 | }
289 | } else {
290 | msg = "OK"
291 | }
292 |
293 | fmt.Fprintf(os.Stderr, "%s: %s\n---------\n", opt.NicoLiveId, msg)
294 |
295 | endCount++
296 | if endCount > 100 {
297 | break
298 | }
299 |
300 | if msg == "notfound" {
301 | NFCount++
302 | } else {
303 | NFCount = 0
304 | }
305 | if NFCount >= 10 {
306 | return
307 | }
308 | }
309 | return
310 | }
311 |
--------------------------------------------------------------------------------
/src/niconico/nico_db.go:
--------------------------------------------------------------------------------
1 | package niconico
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | "os"
7 | "log"
8 | "strings"
9 | "database/sql"
10 |
11 | "path/filepath"
12 | "../files"
13 | )
14 |
15 | var SelMedia = `SELECT
16 | seqno, bandwidth, size, data FROM media
17 | WHERE IFNULL(notfound, 0) == 0 AND data IS NOT NULL
18 | ORDER BY seqno`
19 |
20 | var SelComment = `SELECT
21 | vpos,
22 | date,
23 | date_usec,
24 | IFNULL(no, -1) AS no,
25 | IFNULL(anonymity, 0) AS anonymity,
26 | user_id,
27 | content,
28 | IFNULL(mail, "") AS mail,
29 | %s
30 | IFNULL(premium, 0) AS premium,
31 | IFNULL(score, 0) AS score,
32 | thread,
33 | IFNULL(origin, "") AS origin,
34 | IFNULL(locale, "") AS locale
35 | FROM comment
36 | ORDER BY date2`
37 |
38 | func SelMediaF(seqnoStart, seqnoEnd int64) (ret string) {
39 | ret = `SELECT
40 | seqno, bandwidth, size, data FROM media
41 | WHERE IFNULL(notfound, 0) == 0 AND data IS NOT NULL`
42 |
43 | if seqnoStart > 0 {
44 | ret += ` AND seqno >= ` + fmt.Sprint(seqnoStart)
45 | }
46 |
47 | if seqnoEnd > 0 {
48 | ret += ` AND seqno <= ` + fmt.Sprint(seqnoEnd)
49 | }
50 |
51 | ret += ` ORDER BY seqno`
52 |
53 | return
54 | }
55 |
56 | func (hls *NicoHls) dbOpen() (err error) {
57 | db, err := sql.Open("sqlite3", hls.dbName)
58 | if err != nil {
59 | return
60 | }
61 |
62 | hls.db = db
63 |
64 | _, err = hls.db.Exec(`
65 | PRAGMA synchronous = OFF;
66 | PRAGMA journal_mode = WAL;
67 | `)
68 | if err != nil {
69 | return
70 | }
71 |
72 | err = hls.dbCreate()
73 | if err != nil {
74 | hls.db.Close()
75 | }
76 | return
77 | }
78 |
79 | func (hls *NicoHls) dbCreate() (err error) {
80 | hls.dbMtx.Lock()
81 | defer hls.dbMtx.Unlock()
82 |
83 | // table media
84 |
85 | _, err = hls.db.Exec(`
86 | CREATE TABLE IF NOT EXISTS media (
87 | seqno INTEGER PRIMARY KEY NOT NULL UNIQUE,
88 | current INTEGER,
89 | position REAL,
90 | notfound INTEGER,
91 | bandwidth INTEGER,
92 | size INTEGER,
93 | data BLOB
94 | )
95 | `)
96 | if err != nil {
97 | return
98 | }
99 |
100 | _, err = hls.db.Exec(`
101 | CREATE UNIQUE INDEX IF NOT EXISTS media0 ON media(seqno);
102 | CREATE INDEX IF NOT EXISTS media1 ON media(position);
103 | ---- for debug ----
104 | CREATE INDEX IF NOT EXISTS media100 ON media(size);
105 | CREATE INDEX IF NOT EXISTS media101 ON media(notfound);
106 | `)
107 | if err != nil {
108 | return
109 | }
110 |
111 | // table comment
112 |
113 | _, err = hls.db.Exec(`
114 | CREATE TABLE IF NOT EXISTS comment (
115 | vpos INTEGER NOT NULL,
116 | date INTEGER NOT NULL,
117 | date_usec INTEGER NOT NULL,
118 | date2 INTEGER NOT NULL,
119 | no INTEGER,
120 | anonymity INTEGER,
121 | user_id TEXT NOT NULL,
122 | content TEXT NOT NULL,
123 | mail TEXT,
124 | name TEXT,
125 | premium INTEGER,
126 | score INTEGER,
127 | thread TEXT,
128 | origin TEXT,
129 | locale TEXT,
130 | hash TEXT UNIQUE NOT NULL
131 | )`)
132 | if err != nil {
133 | return
134 | }
135 |
136 | _, err = hls.db.Exec(`
137 | CREATE UNIQUE INDEX IF NOT EXISTS comment0 ON comment(hash);
138 | ---- for debug ----
139 | CREATE INDEX IF NOT EXISTS comment100 ON comment(date2);
140 | CREATE INDEX IF NOT EXISTS comment101 ON comment(no);
141 | `)
142 | if err != nil {
143 | return
144 | }
145 |
146 |
147 | // kvs media
148 |
149 | _, err = hls.db.Exec(`
150 | CREATE TABLE IF NOT EXISTS kvs (
151 | k TEXT PRIMARY KEY NOT NULL UNIQUE,
152 | v BLOB
153 | )
154 | `)
155 | if err != nil {
156 | return
157 | }
158 | _, err = hls.db.Exec(`
159 | CREATE UNIQUE INDEX IF NOT EXISTS kvs0 ON kvs(k);
160 | `)
161 | if err != nil {
162 | return
163 | }
164 |
165 | //hls.__dbBegin()
166 |
167 | return
168 | }
169 |
170 | // timeshift
171 | func (hls *NicoHls) dbSetPosition() {
172 | hls.dbExec(`UPDATE media SET position = ? WHERE seqno=?`,
173 | hls.playlist.position,
174 | hls.playlist.seqNo,
175 | )
176 | }
177 |
178 | // timeshift
179 | func (hls *NicoHls) dbGetLastPosition() (res float64) {
180 | hls.dbMtx.Lock()
181 | defer hls.dbMtx.Unlock()
182 |
183 | hls.db.QueryRow("SELECT position FROM media ORDER BY POSITION DESC LIMIT 1").Scan(&res)
184 | return
185 | }
186 |
187 | //func (hls *NicoHls) __dbBegin() {
188 | // return
189 | ///////////////////////////////////////////
190 | //hls.db.Exec(`BEGIN TRANSACTION`)
191 | //}
192 | //func (hls *NicoHls) __dbCommit(t time.Time) {
193 | // return
194 | ///////////////////////////////////////////
195 |
196 | //// Never hls.dbMtx.Lock()
197 | //var start int64
198 | //hls.db.Exec(`COMMIT; BEGIN TRANSACTION`)
199 | //if t.UnixNano() - hls.lastCommit.UnixNano() > 500000000 {
200 | // log.Printf("Commit: %s\n", hls.dbName)
201 | //}
202 | //hls.lastCommit = t
203 | //}
204 | func (hls *NicoHls) dbCommit() {
205 | // hls.dbMtx.Lock()
206 | // defer hls.dbMtx.Unlock()
207 |
208 | // hls.__dbCommit(time.Now())
209 | }
210 | func (hls *NicoHls) dbExec(query string, args ...interface{}) {
211 | hls.dbMtx.Lock()
212 | defer hls.dbMtx.Unlock()
213 |
214 | if hls.nicoDebug {
215 | start := time.Now().UnixNano()
216 | defer func() {
217 | t := (time.Now().UnixNano() - start) / (1000 * 1000)
218 | if t > 100 {
219 | fmt.Fprintf(os.Stderr, "%s:[WARN]dbExec: %d(ms):%s\n", debug_Now(), t, query)
220 | }
221 | }()
222 | }
223 |
224 | if _, err := hls.db.Exec(query, args...); err != nil {
225 | fmt.Printf("dbExec %#v\n", err)
226 | //hls.db.Exec("COMMIT")
227 | hls.db.Close()
228 | os.Exit(1)
229 | }
230 | }
231 |
232 | func (hls *NicoHls) dbKVSet(k string, v interface{}) {
233 | query := `INSERT OR REPLACE INTO kvs (k,v) VALUES (?,?)`
234 | hls.startDBGoroutine(func(sig <-chan struct{}) int {
235 | hls.dbExec(query, k, v)
236 | return OK
237 | })
238 | }
239 |
240 | func (hls *NicoHls) dbInsertReplaceOrIgnore(table string, data map[string]interface{}, replace bool) {
241 | var keys []string
242 | var qs []string
243 | var args []interface{}
244 |
245 | for k, v := range data {
246 | keys = append(keys, k)
247 | qs = append(qs, "?")
248 | args = append(args, v)
249 | }
250 |
251 | var replaceOrIgnore string
252 | if replace {
253 | replaceOrIgnore = "REPLACE"
254 | } else {
255 | replaceOrIgnore = "IGNORE"
256 | }
257 |
258 | query := fmt.Sprintf(
259 | `INSERT OR %s INTO %s (%s) VALUES (%s)`,
260 | replaceOrIgnore,
261 | table,
262 | strings.Join(keys, ","),
263 | strings.Join(qs, ","),
264 | )
265 |
266 | hls.startDBGoroutine(func(sig <-chan struct{}) int {
267 | hls.dbExec(query, args...)
268 | return OK
269 | })
270 | }
271 |
272 | func (hls *NicoHls) dbInsert(table string, data map[string]interface{}) {
273 | hls.dbInsertReplaceOrIgnore(table, data, false)
274 | }
275 | func (hls *NicoHls) dbReplace(table string, data map[string]interface{}) {
276 | hls.dbInsertReplaceOrIgnore(table, data, true)
277 | }
278 |
279 | // timeshift
280 | func (hls *NicoHls) dbGetFromWhen() (res_from int, when float64) {
281 | hls.dbMtx.Lock()
282 | defer hls.dbMtx.Unlock()
283 | var date2 int64
284 | var no int
285 |
286 | hls.db.QueryRow("SELECT date2, no FROM comment ORDER BY date2 ASC LIMIT 1").Scan(&date2, &no)
287 | res_from = no
288 | if res_from <= 0 {
289 | res_from = 1
290 | }
291 |
292 | if date2 == 0 {
293 | var endTime float64
294 | hls.db.QueryRow(`SELECT v FROM kvs WHERE k = "endTime"`).Scan(&endTime)
295 |
296 | when = endTime + 3600
297 | } else {
298 | when = float64(date2) / (1000 * 1000)
299 | }
300 |
301 | return
302 | }
303 |
304 | func WriteComment(db *sql.DB, fileName string, skipHb bool) {
305 |
306 | var fSelComment = func(revision int) string {
307 | var selAppend string
308 | if revision >= 1 {
309 | selAppend += `IFNULL(name, "") AS name,`
310 | }
311 | return fmt.Sprintf(SelComment, selAppend)
312 | }
313 |
314 | var commentRevision int
315 | var nameCount int64
316 | db.QueryRow(`SELECT COUNT(name) FROM pragma_table_info('comment') WHERE name = 'name'`).Scan(&nameCount)
317 | if nameCount > 0 {
318 | commentRevision = 1
319 | }
320 |
321 | rows, err := db.Query(fSelComment(commentRevision))
322 | if err != nil {
323 | log.Println(err)
324 | return
325 | }
326 | defer rows.Close()
327 |
328 | fileName = files.ChangeExtention(fileName, "xml")
329 |
330 | dir := filepath.Dir(fileName)
331 | base := filepath.Base(fileName)
332 | base, err = files.GetFileNameNext(base)
333 | if err != nil {
334 | fmt.Println(err)
335 | os.Exit(1)
336 | }
337 | fileName = filepath.Join(dir, base)
338 | f, err := os.Create(fileName)
339 | if err != nil {
340 | log.Fatalln(err)
341 | }
342 | defer f.Close()
343 | fmt.Fprintf(f, "%s\r\n", ``)
344 | fmt.Fprintf(f, "%s\r\n", ``)
345 |
346 | for rows.Next() {
347 | var vpos int64
348 | var date int64
349 | var date_usec int64
350 | var no int64
351 | var anonymity int64
352 | var user_id string
353 | var content string
354 | var mail string
355 | var name string
356 | var premium int64
357 | var score int64
358 | var thread string
359 | var origin string
360 | var locale string
361 | var dest0 = []interface{} {
362 | &vpos ,
363 | &date ,
364 | &date_usec ,
365 | &no ,
366 | &anonymity ,
367 | &user_id ,
368 | &content ,
369 | &mail ,
370 | }
371 | var dest1 = []interface{} {
372 | &premium ,
373 | &score ,
374 | &thread ,
375 | &origin ,
376 | &locale ,
377 | }
378 | if commentRevision >= 1 {
379 | dest0 = append(dest0, &name)
380 | }
381 | var dest = append(dest0, dest1...)
382 | err = rows.Scan(dest...)
383 | if err != nil {
384 | log.Println(err)
385 | return
386 | }
387 |
388 | // skip /hb
389 | if (premium > 1) && skipHb && strings.HasPrefix(content, "/hb ") {
390 | continue
391 | }
392 |
393 | if (vpos < 0) {
394 | continue
395 | }
396 |
397 | line := fmt.Sprintf(
398 | `= 0 {
407 | line += fmt.Sprintf(` no="%d"`, no)
408 | }
409 | if anonymity != 0 {
410 | line += fmt.Sprintf(` anonymity="%d"`, anonymity)
411 | }
412 | if mail != "" {
413 | mail = strings.Replace(mail, `"`, """, -1)
414 | mail = strings.Replace(mail, "&", "&", -1)
415 | mail = strings.Replace(mail, "<", "<", -1)
416 | line += fmt.Sprintf(` mail="%s"`, mail)
417 | }
418 | if name != "" {
419 | name = strings.Replace(name, `"`, """, -1)
420 | name = strings.Replace(name, "&", "&", -1)
421 | name = strings.Replace(name, "<", "<", -1)
422 | line += fmt.Sprintf(` name="%s"`, name)
423 | }
424 | if origin != "" {
425 | origin = strings.Replace(origin, `"`, """, -1)
426 | origin = strings.Replace(origin, "&", "&", -1)
427 | origin = strings.Replace(origin, "<", "<", -1)
428 | line += fmt.Sprintf(` origin="%s"`, origin)
429 | }
430 | if premium != 0 {
431 | line += fmt.Sprintf(` premium="%d"`, premium)
432 | }
433 | if score != 0 {
434 | line += fmt.Sprintf(` score="%d"`, score)
435 | }
436 | if locale != "" {
437 | locale = strings.Replace(locale, `"`, """, -1)
438 | locale = strings.Replace(locale, "&", "&", -1)
439 | locale = strings.Replace(locale, "<", "<", -1)
440 | line += fmt.Sprintf(` locale="%s"`, locale)
441 | }
442 | line += ">"
443 | content = strings.Replace(content, "&", "&", -1)
444 | content = strings.Replace(content, "<", "<", -1)
445 | line += content
446 | line += ""
447 | fmt.Fprintf(f, "%s\r\n", line)
448 | }
449 | fmt.Fprintf(f, "%s\r\n", ``)
450 | }
451 |
452 | // ts
453 | func (hls *NicoHls) dbGetLastMedia(i int) (res []byte) {
454 | hls.dbMtx.Lock()
455 | defer hls.dbMtx.Unlock()
456 | hls.db.QueryRow("SELECT data FROM media WHERE seqno = ?", i).Scan(&res)
457 | return
458 | }
459 | func (hls *NicoHls) dbGetLastSeqNo() (res int64) {
460 | hls.dbMtx.Lock()
461 | defer hls.dbMtx.Unlock()
462 | hls.db.QueryRow("SELECT seqno FROM media ORDER BY seqno DESC LIMIT 1").Scan(&res)
463 | return
464 | }
465 |
--------------------------------------------------------------------------------
/src/niconico/nico_mem_db.go:
--------------------------------------------------------------------------------
1 | package niconico
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | "os"
7 | "database/sql"
8 | )
9 |
10 | func (hls *NicoHls) memdbOpen() (err error) {
11 | db, err := sql.Open("sqlite3", "file::memory:?mode=memory&cache=shared")
12 | if err != nil {
13 | return
14 | }
15 |
16 | hls.memdb = db
17 |
18 | err = hls.memdbCreate()
19 | if err != nil {
20 | hls.memdb.Close()
21 | }
22 |
23 | if hls.db != nil {
24 | rows, e := hls.db.Query(`SELECT * FROM
25 | (SELECT seqno, IFNULL(notfound, 0), IFNULL(size, 0) FROM media ORDER BY seqno DESC LIMIT 10) ORDER BY seqno`)
26 | if e != nil {
27 | err = e
28 | return
29 | }
30 | defer rows.Close()
31 |
32 | var found404 bool
33 | for rows.Next() {
34 | var seqno int
35 | var notfound bool
36 | var size int
37 | err = rows.Scan(&seqno, ¬found, &size)
38 | if err != nil {
39 | return
40 | }
41 | if notfound || size == 0 {
42 | hls.memdbSet404(seqno)
43 | found404 = true
44 | } else {
45 | hls.memdbSet200(seqno)
46 | }
47 | if (! found404) {
48 | hls.memdbSetStopBack(seqno)
49 | if hls.nicoDebug {
50 | fmt.Fprintf(os.Stderr, "memdbSetStopBack(%d)\n", seqno)
51 | }
52 | }
53 | }
54 | }
55 |
56 | return
57 | }
58 |
59 | func (hls *NicoHls) memdbCreate() (err error) {
60 | hls.memdbMtx.Lock()
61 | defer hls.memdbMtx.Unlock()
62 |
63 | _, err = hls.memdb.Exec(`
64 | CREATE TABLE IF NOT EXISTS media (
65 | seqno INTEGER PRIMARY KEY NOT NULL UNIQUE,
66 | is200 INTEGER,
67 | is404 INTEGER,
68 | stopback INTEGER
69 | )
70 | `)
71 | if err != nil {
72 | return
73 | }
74 |
75 | _, err = hls.memdb.Exec(`
76 | CREATE UNIQUE INDEX IF NOT EXISTS media0 ON media(seqno);
77 | `)
78 | if err != nil {
79 | return
80 | }
81 |
82 | return
83 | }
84 | func (hls *NicoHls) memdbSetStopBack(seqno int) {
85 | if hls.nicoDebug {
86 | start := time.Now().UnixNano()
87 | defer func() {
88 | t := (time.Now().UnixNano() - start) / (1000 * 1000)
89 | if t > 100 {
90 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbSetStopBack: %d(ms)\n", debug_Now(), t)
91 | }
92 | }()
93 | }
94 |
95 | hls.memdbMtx.Lock()
96 | defer hls.memdbMtx.Unlock()
97 |
98 | _, err := hls.memdb.Exec(`
99 | INSERT OR IGNORE INTO media (seqno, stopback) VALUES (?, 1);
100 | UPDATE media SET stopback = 1 WHERE seqno=?;
101 | `, seqno, seqno)
102 | if err != nil {
103 | fmt.Println(err)
104 | }
105 | }
106 | func (hls *NicoHls) memdbGetStopBack(seqno int) (res bool) {
107 | if hls.nicoDebug {
108 | start := time.Now().UnixNano()
109 | defer func() {
110 | t := (time.Now().UnixNano() - start) / (1000 * 1000)
111 | if t > 100 {
112 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbGetStopBack: %d(ms)\n", debug_Now(), t)
113 | }
114 | }()
115 | }
116 |
117 | hls.memdbMtx.Lock()
118 | defer hls.memdbMtx.Unlock()
119 |
120 | hls.memdb.QueryRow("SELECT IFNULL(stopback, 0) FROM media WHERE seqno=?", seqno).Scan(&res)
121 | return
122 | }
123 | func (hls *NicoHls) memdbSet200(seqno int) {
124 | if hls.nicoDebug {
125 | start := time.Now().UnixNano()
126 | defer func() {
127 | t := (time.Now().UnixNano() - start) / (1000 * 1000)
128 | if t > 100 {
129 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbSet200: %d(ms)\n", debug_Now(), t)
130 | }
131 | }()
132 | }
133 |
134 | hls.memdbMtx.Lock()
135 | defer hls.memdbMtx.Unlock()
136 |
137 | hls.memdb.Exec(`INSERT OR REPLACE INTO media (seqno, is200) VALUES (?, 1)`, seqno)
138 | }
139 | func (hls *NicoHls) memdbSet404(seqno int) {
140 | if hls.nicoDebug {
141 | start := time.Now().UnixNano()
142 | defer func() {
143 | t := (time.Now().UnixNano() - start) / (1000 * 1000)
144 | if t > 100 {
145 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbSet404: %d(ms)\n", debug_Now(), t)
146 | }
147 | }()
148 | }
149 |
150 | hls.memdbMtx.Lock()
151 | defer hls.memdbMtx.Unlock()
152 |
153 | hls.memdb.Exec(`INSERT OR REPLACE INTO media (seqno, is404) VALUES (?, 1)`, seqno)
154 | }
155 | func (hls *NicoHls) memdbCheck200(seqno int) (res bool) {
156 | if hls.nicoDebug {
157 | start := time.Now().UnixNano()
158 | defer func() {
159 | t := (time.Now().UnixNano() - start) / (1000 * 1000)
160 | if t > 100 {
161 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbCheck200: %d(ms)\n", debug_Now(), t)
162 | }
163 | }()
164 | }
165 |
166 | hls.memdbMtx.Lock()
167 | defer hls.memdbMtx.Unlock()
168 |
169 | hls.memdb.QueryRow("SELECT IFNULL(is200, 0) FROM media WHERE seqno=?", seqno).Scan(&res)
170 | return
171 | }
172 | func (hls *NicoHls) memdbDelete(seqno int) {
173 | if hls.nicoDebug {
174 | start := time.Now().UnixNano()
175 | defer func() {
176 | t := (time.Now().UnixNano() - start) / (1000 * 1000)
177 | if t > 100 {
178 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbDelete: %d(ms)\n", debug_Now(), t)
179 | }
180 | }()
181 | }
182 |
183 | hls.memdbMtx.Lock()
184 | defer hls.memdbMtx.Unlock()
185 |
186 | min := seqno - 100
187 | hls.memdb.Exec(`DELETE FROM media WHERE seqno < ?`, min)
188 | }
189 | func (hls *NicoHls) memdbCount() (res int) {
190 | if hls.nicoDebug {
191 | start := time.Now().UnixNano()
192 | defer func() {
193 | t := (time.Now().UnixNano() - start) / (1000 * 1000)
194 | if t > 100 {
195 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbCount: %d(ms)\n", debug_Now(), t)
196 | }
197 | }()
198 | }
199 |
200 | hls.memdbMtx.Lock()
201 | defer hls.memdbMtx.Unlock()
202 |
203 | hls.memdb.QueryRow("SELECT COUNT(seqno) FROM media").Scan(&res)
204 | return
205 | }
--------------------------------------------------------------------------------
/src/niconico/nico_rtmp.go:
--------------------------------------------------------------------------------
1 | package niconico
2 |
3 | import (
4 | "fmt"
5 | "encoding/xml"
6 | "io/ioutil"
7 | "regexp"
8 | "strings"
9 | "net/url"
10 | "sync"
11 | "log"
12 | "time"
13 | "../rtmps"
14 | "../amf"
15 | "../options"
16 | "../files"
17 | "../httpbase"
18 | )
19 |
20 | type Content struct {
21 | Id string `xml:"id,attr"`
22 | Text string `xml:",chardata"`
23 | }
24 | type Tickets struct {
25 | Name string `xml:"name,attr"`
26 | Text string `xml:",chardata"`
27 | }
28 | type Status struct {
29 | Title string `xml:"stream>title"`
30 | CommunityId string `xml:"stream>default_community"`
31 | Id string `xml:"stream>id"`
32 | Provider string `xml:"stream>provider_type"`
33 | IsArchive bool `xml:"stream>archive"`
34 | IsArchivePlayerServer bool `xml:"stream>is_archiveplayserver"`
35 | Ques []string `xml:"stream>quesheet>que"`
36 | Contents []Content `xml:"stream>contents_list>contents"`
37 | IsPremium bool `xml:"user>is_premium"`
38 | Url string `xml:"rtmp>url"`
39 | Ticket string `xml:"rtmp>ticket"`
40 | Tickets []Tickets `xml:"tickets>stream"`
41 | ErrorCode string `xml:"error>code"`
42 | streams []Stream
43 | chStream chan struct{}
44 | wg *sync.WaitGroup
45 | }
46 | type StatusE struct {
47 | Url string `xml:"rtmp>url"`
48 | Ticket string `xml:"rtmp>ticket"`
49 | ErrorCode string `xml:"error>code"`
50 | }
51 | type Stream struct {
52 | originUrl string
53 | streamName string
54 | originTicket string
55 | }
56 | func (status *Status) quesheet() {
57 | stream := make(map[string][]Stream)
58 | playType := make(map[string]string)
59 |
60 | // timeshift; tag
61 | re_pub := regexp.MustCompile(`\A/publish\s+(\S+)\s+(?:(\S+?),)?(\S+?)(?:\?(\S+))?\z`)
62 | re_play := regexp.MustCompile(`\A/play\s+(\S+)\s+(\S+)\z`)
63 |
64 | for _, q := range status.Ques {
65 | // /publish lv* /content/*/lv*_*_1_*.f4v
66 | if ma := re_pub.FindStringSubmatch(q); len(ma) >= 5 {
67 | stream[ma[1]] = append(stream[ma[1]], Stream{
68 | originUrl: ma[2],
69 | streamName: ma[3],
70 | originTicket: ma[4],
71 | })
72 |
73 | // /play ...
74 | } else if ma := re_play.FindStringSubmatch(q); len(ma) > 0 {
75 | // /play case:sp:rtmp:lv*_s_lv*,mobile:rtmp:lv*_s_lv*_sub1,premium:rtmp:lv*_s_lv*_sub1,default:rtmp:lv*_s_lv* main
76 | if strings.HasPrefix(ma[1], "case:") {
77 | s0 := ma[1]
78 | s0 = strings.TrimPrefix(s0, "case:")
79 | cases := strings.Split(s0, ",")
80 | // sp:rtmp:lv*_s_lv*
81 | re := regexp.MustCompile(`\A(\S+?):rtmp:(\S+?)\z`)
82 | for _, c := range cases {
83 | if ma := re.FindStringSubmatch(c); len(ma) > 0 {
84 | playType[ma[1]] = ma[2]
85 | }
86 | }
87 |
88 | // /play rtmp:lv* main
89 | } else {
90 | re := regexp.MustCompile(`\Artmp:(\S+?)\z`)
91 | if ma := re.FindStringSubmatch(ma[1]); len(ma) > 0 {
92 | playType["default"] = ma[1]
93 | }
94 | }
95 | }
96 | }
97 |
98 | pt, ok := playType["premium"]
99 | if ok && status.IsPremium {
100 | s, ok := stream[ pt ]
101 | if ok {
102 | status.streams = s
103 | }
104 | } else {
105 | pt, ok := playType["default"]
106 | if ok {
107 | s, ok := stream[ pt ]
108 | if ok {
109 | status.streams = s
110 | }
111 | }
112 | }
113 | }
114 | func (status *Status) initStreams() {
115 |
116 | if len(status.streams) > 0 {
117 | return
118 | }
119 |
120 | //if status.isOfficialLive() {
121 | status.contentsOfficialLive()
122 | //} else if status.isLive() {
123 | status.contentsNonOfficialLive()
124 | //} else {
125 | status.quesheet()
126 | //}
127 |
128 | return
129 | }
130 | func (status *Status) getFileName(index int) (name string) {
131 | if len(status.streams) == 1 {
132 | //name = fmt.Sprintf("%s.flv", status.Id)
133 | name = fmt.Sprintf("%s-%s-%s.flv", status.Id, status.CommunityId, status.Title)
134 | } else if len(status.streams) > 1 {
135 | //name = fmt.Sprintf("%s-%d.flv", status.Id, 1 + index)
136 | name = fmt.Sprintf("%s-%s-%s#%d.flv", status.Id, status.CommunityId, status.Title, 1 + index)
137 | } else {
138 | log.Fatalf("No stream")
139 | }
140 | name = files.ReplaceForbidden(name)
141 | return
142 | }
143 | func (status *Status) contentsNonOfficialLive() {
144 | re := regexp.MustCompile(`\A(?:rtmp:)?(rtmp\w*://\S+?)(?:,(\S+?)(?:\?(\S+))?)?\z`)
145 |
146 | // Live (not timeshift); tag
147 | for _, c := range status.Contents {
148 | if ma := re.FindStringSubmatch(c.Text); len(ma) > 0 {
149 | status.streams = append(status.streams, Stream{
150 | originUrl: ma[1],
151 | streamName: ma[2],
152 | originTicket: ma[3],
153 | })
154 | }
155 | }
156 |
157 | }
158 | func (status *Status) contentsOfficialLive() {
159 |
160 | tickets := make(map[string] string)
161 | for _, t := range status.Tickets {
162 | tickets[t.Name] = t.Text
163 | }
164 |
165 | for _, c := range status.Contents {
166 | if strings.HasPrefix(c.Text, "case:") {
167 | c.Text = strings.TrimPrefix(c.Text, "case:")
168 |
169 | for _, c := range strings.Split(c.Text, ",") {
170 | c, e := url.PathUnescape(c)
171 | if e != nil {
172 | fmt.Printf("%v\n", e)
173 | }
174 |
175 | re := regexp.MustCompile(`\A(\S+?):(?:limelight:|akamai:)?(\S+),(\S+)\z`)
176 | if ma := re.FindStringSubmatch(c); len(ma) > 0 {
177 | fmt.Printf("\n%#v\n", ma)
178 | switch ma[1] {
179 | default:
180 | fmt.Printf("unknown contents case %#v\n", ma[1])
181 | case "mobile":
182 | case "middle":
183 | case "default":
184 | status.Url = ma[2]
185 | t, ok := tickets[ma[3]]
186 | if (! ok) {
187 | fmt.Printf("not found %s\n", ma[3])
188 | }
189 | fmt.Printf("%s\n", t)
190 | status.streams = append(status.streams, Stream{
191 | streamName: ma[3],
192 | originTicket: t,
193 | })
194 | }
195 | }
196 | }
197 | }
198 | }
199 | }
200 |
201 | func (status *Status) relayStreamName(i, offset int) (s string) {
202 | s = regexp.MustCompile(`[^/\\]+\z`).FindString(status.streams[i].streamName)
203 | if offset >= 0 {
204 | s += fmt.Sprintf("_%d", offset)
205 | }
206 | return
207 | }
208 |
209 | func (status *Status) streamName(i, offset int) (name string, err error) {
210 | if status.isOfficialLive() {
211 | if i >= len(status.streams) {
212 | err = fmt.Errorf("(status *Status) streamName(i int): Out of index: %d\n", i)
213 | return
214 | }
215 |
216 | name = status.streams[i].streamName
217 | if status.streams[i].originTicket != "" {
218 | name += "?" + status.streams[i].originTicket
219 | }
220 | return
221 |
222 | } else if status.isOfficialTs() {
223 | name = status.streams[i].streamName
224 | name = regexp.MustCompile(`(?i:\.flv)$`).ReplaceAllString(name, "")
225 | if regexp.MustCompile(`(?i:\.(?:f4v|mp4))$`).MatchString(name) {
226 | name = "mp4:" + name
227 | } else if regexp.MustCompile(`(?i:\.raw)$`).MatchString(name) {
228 | name = "raw:" + name
229 | }
230 |
231 | } else {
232 | name = status.relayStreamName(i, offset)
233 | }
234 |
235 | return
236 | }
237 | func (status *Status) tcUrl() (url string, err error) {
238 | if status.Url != "" {
239 | url = status.Url
240 | return
241 | } else {
242 | status.contentsOfficialLive()
243 | }
244 |
245 | if status.Url != "" {
246 | url = status.Url
247 | return
248 | }
249 |
250 | err = fmt.Errorf("tcUrl not found")
251 | return
252 | }
253 | func (status *Status) isTs() bool {
254 | return status.IsArchive
255 | }
256 | func (status *Status) isLive() bool {
257 | return (! status.IsArchive)
258 | }
259 | func (status *Status) isOfficialLive() bool {
260 | return (status.Provider == "official") && (! status.IsArchive)
261 | }
262 | func (status *Status) isOfficialTs() bool {
263 | if status.IsArchive {
264 | switch status.Provider {
265 | case "official": return true
266 | case "channel": return status.IsArchivePlayerServer
267 | }
268 | }
269 | return false
270 | }
271 |
272 | func (st Stream) relayStreamName(offset int) (s string) {
273 | s = regexp.MustCompile(`[^/\\]+\z`).FindString(st.streamName)
274 | if offset >= 0 {
275 | s += fmt.Sprintf("_%d", offset)
276 | }
277 | return
278 | }
279 | func (st Stream) noticeStreamName(offset int) (s string) {
280 | s = st.streamName
281 | s = regexp.MustCompile(`(?i:\.flv)$`).ReplaceAllString(s, "")
282 | if regexp.MustCompile(`(?i:\.(?:f4v|mp4))$`).MatchString(s) {
283 | s = "mp4:" + s
284 | } else if regexp.MustCompile(`(?i:\.raw)$`).MatchString(s) {
285 | s = "raw:" + s
286 | }
287 |
288 | if st.originTicket != "" {
289 | s += "?" + st.originTicket
290 | }
291 |
292 | return
293 | }
294 |
295 | func (status *Status) recStream(index int, opt options.Option) (err error) {
296 | defer func(){
297 | <-status.chStream
298 | status.wg.Done()
299 | }()
300 |
301 | stream := status.streams[index]
302 |
303 | tcUrl, err := status.tcUrl()
304 | if err != nil {
305 | return
306 | }
307 |
308 | rtmp, err := rtmps.NewRtmp(
309 | // tcUrl
310 | tcUrl,
311 | // swfUrl
312 | "http://live.nicovideo.jp/nicoliveplayer.swf?180116154229",
313 | // pageUrl
314 | "http://live.nicovideo.jp/watch/" + status.Id,
315 | // option
316 | status.Ticket,
317 | )
318 | if err != nil {
319 | return
320 | }
321 | defer rtmp.Close()
322 |
323 |
324 | fileName, err := files.GetFileNameNext(status.getFileName(index))
325 | if err != nil {
326 | return
327 | }
328 | rtmp.SetFlvName(fileName)
329 |
330 |
331 | tryRecord := func() (incomplete bool, err error) {
332 |
333 | if err = rtmp.Connect(); err != nil {
334 | return
335 | }
336 |
337 | // default: 2500000
338 | //if err = rtmp.SetPeerBandwidth(100*1000*1000, 0); err != nil {
339 | if err = rtmp.SetPeerBandwidth(2500000, 0); err != nil {
340 | fmt.Printf("SetPeerBandwidth: %v\n", err)
341 | return
342 | }
343 |
344 | if err = rtmp.WindowAckSize(2500000); err != nil {
345 | fmt.Printf("WindowAckSize: %v\n", err)
346 | return
347 | }
348 |
349 | if err = rtmp.CreateStream(); err != nil {
350 | fmt.Printf("CreateStream %v\n", err)
351 | return
352 | }
353 |
354 | if err = rtmp.SetBufferLength(0, 2000); err != nil {
355 | fmt.Printf("SetBufferLength: %v\n", err)
356 | return
357 | }
358 |
359 | var offset int
360 | if status.IsArchive {
361 | offset = 0
362 | } else {
363 | offset = -2
364 | }
365 |
366 | if status.isOfficialTs() {
367 | for i := 0; true; i++ {
368 | if i > 30 {
369 | err = fmt.Errorf("sendFileRequest: No response")
370 | return
371 | }
372 | data, e := rtmp.Command(
373 | "sendFileRequest", []interface{} {
374 | nil,
375 | amf.SwitchToAmf3(),
376 | []string{
377 | stream.streamName,
378 | },
379 | })
380 | if e != nil {
381 | err = e
382 | return
383 | }
384 |
385 | var resCnt int
386 | switch data.(type) {
387 | case map[string]interface{}:
388 | resCnt = len(data.(map[string]interface{}))
389 | case map[int]interface{}:
390 | resCnt = len(data.(map[int]interface{}))
391 | case []interface{}:
392 | resCnt = len(data.([]interface{}))
393 | case []string:
394 | resCnt = len(data.([]string))
395 | }
396 | if resCnt > 0 {
397 | break
398 | }
399 | time.Sleep(10 * time.Second)
400 | }
401 |
402 | } else if (! status.isOfficialLive()) {
403 | // /publishの第二引数
404 | // streamName(param1:String)
405 | // 「,」で区切る
406 | // ._originUrl, streamName(playStreamName)
407 | // streamName に、「?」がついてるなら originTickt となる
408 | // streamName の.flvは削除する
409 | // streamNameが/\.(f4v|mp4)$/iなら、頭にmp4:をつける
410 | // /\.raw$/iなら、raw:をつける。
411 | // relayStreamName: streamNameの頭からスラッシュまでを削除したもの
412 |
413 | _, err = rtmp.Command(
414 | "nlPlayNotice", []interface{} {
415 | nil,
416 | // _connection.request.originUrl
417 | stream.originUrl,
418 |
419 | // this._connection.request.playStreamRequest
420 | // originticket あるなら
421 | // playStreamName ? this._originTicket
422 | // 無いなら playStreamName
423 | stream.noticeStreamName(offset),
424 |
425 | // var _loc1_:String = this._relayStreamName;
426 | // if(this._offset != -2)
427 | // {
428 | // _loc1_ = _loc1_ + ("_" + this.offset);
429 | // }
430 | // user nama: String 'lvxxxxxxxxx'
431 | // user kako: lvxxxxxxxxx_xxxxxxxxxxxx_1_xxxxxx.f4v_0
432 | stream.relayStreamName(offset),
433 |
434 | // seek offset
435 | // user nama: -2, user kako: 0
436 | offset,
437 | })
438 | if err != nil {
439 | fmt.Printf("nlPlayNotice %v\n", err)
440 | return
441 | }
442 | }
443 |
444 | if err = rtmp.SetBufferLength(1, 3600 * 1000); err != nil {
445 | fmt.Printf("SetBufferLength: %v\n", err)
446 | return
447 | }
448 |
449 | // No return
450 | rtmp.SetFixAggrTimestamp(true)
451 |
452 | // user kako: lv*********_************_*_******.f4v_0
453 | // official or channel ts: mp4:/content/********/lv*********_************_*_******.f4v
454 | //if err = rtmp.Play(status.origin.playStreamName(status.isTsOfficial(), offset)); err != nil {
455 | streamName, err := status.streamName(index, offset)
456 | if err != nil {
457 | return
458 | }
459 |
460 | if status.isOfficialTs() {
461 | ts := rtmp.GetTimestamp()
462 | if ts > 1000 {
463 | err = rtmp.PlayTime(streamName, ts - 1000)
464 | } else {
465 | err = rtmp.PlayTime(streamName, -5000)
466 | }
467 |
468 | } else if status.isTs() {
469 | rtmp.SetFlush(true)
470 | err = rtmp.PlayTime(streamName, -5000)
471 |
472 | } else {
473 | err = rtmp.Play(streamName)
474 | }
475 | if err != nil {
476 | fmt.Printf("Play: %v\n", err)
477 | return
478 | }
479 |
480 | // Non-recordedなタイムシフトでseekしても、timestampが変わるだけで
481 | // 最初からの再生となってしまうのでやらないこと
482 |
483 | // 公式のタイムシフトでSeekしてもタイムスタンプがおかしい
484 |
485 | if opt.NicoTestTimeout > 0 {
486 | // test mode
487 | _, incomplete, err = rtmp.WaitTest(opt.NicoTestTimeout)
488 | } else {
489 | // normal mode
490 | _, incomplete, err = rtmp.Wait()
491 | }
492 | return
493 | } // end func
494 |
495 | //ticketTime := time.Now().Unix()
496 | //rtmp.SetNoSeek(false)
497 | for i := 0; i < 10; i++ {
498 | incomplete, e := tryRecord()
499 | if e != nil {
500 | err = e
501 | fmt.Printf("%v\n", e)
502 | return
503 | } else if incomplete && status.isOfficialTs() {
504 | fmt.Println("incomplete")
505 | time.Sleep(3 * time.Second)
506 |
507 | // update ticket
508 | if true {
509 | //if time.Now().Unix() > ticketTime + 60 {
510 | //ticketTime = time.Now().Unix()
511 | if ticket, e := getTicket(opt); e != nil {
512 | err = e
513 | return
514 | } else {
515 | rtmp.SetConnectOpt(ticket)
516 | }
517 | //}
518 | }
519 |
520 | continue
521 | }
522 | break
523 | }
524 |
525 | fmt.Printf("done\n")
526 | return
527 | }
528 |
529 | func (status *Status) recAllStreams(opt options.Option) (err error) {
530 |
531 | status.initStreams()
532 |
533 | var MaxConn int
534 | if opt.NicoRtmpMaxConn == 0 {
535 | if status.isOfficialTs() {
536 | MaxConn = 1
537 | } else {
538 | MaxConn = 4
539 | }
540 | } else if opt.NicoRtmpMaxConn < 0 {
541 | MaxConn = 1
542 | } else {
543 | MaxConn = opt.NicoRtmpMaxConn
544 | }
545 |
546 | status.wg = &sync.WaitGroup{}
547 | status.chStream = make(chan struct{}, MaxConn)
548 |
549 | ticketTime := time.Now().Unix()
550 |
551 | for index, _ := range status.streams {
552 | if opt.NicoRtmpIndex != nil {
553 | if tes, ok := opt.NicoRtmpIndex[index]; !ok || !tes {
554 | continue
555 | }
556 | }
557 |
558 | // blocks here
559 | status.chStream <- struct{}{}
560 | status.wg.Add(1)
561 |
562 | go status.recStream(index, opt)
563 |
564 | now := time.Now().Unix()
565 | if now > ticketTime + 60 {
566 | ticketTime = now
567 | if ticket, e := getTicket(opt); e != nil {
568 | err = e
569 | return
570 | } else {
571 | status.Ticket = ticket
572 | }
573 | }
574 | }
575 |
576 | status.wg.Wait()
577 |
578 | return
579 | }
580 |
581 | func getTicket(opt options.Option) (ticket string, err error) {
582 | status, notLogin, err := getStatus(opt)
583 | if err != nil {
584 | return
585 | }
586 | if status.Ticket != "" {
587 | ticket = status.Ticket
588 | } else {
589 | if notLogin {
590 | err = fmt.Errorf("notLogin")
591 | } else {
592 | err = fmt.Errorf("Ticket not found")
593 | }
594 | }
595 | return
596 | }
597 | func getStatus(opt options.Option) (status *Status, notLogin bool, err error) {
598 | var uri, uriE string
599 |
600 | // experimental
601 | if opt.NicoStatusHTTPS {
602 | uri = fmt.Sprintf("https://ow.live.nicovideo.jp/api/getplayerstatus?v=%s", opt.NicoLiveId)
603 | uriE = fmt.Sprintf("https://ow.live.nicovideo.jp/api/getedgestatus?v=%s", opt.NicoLiveId)
604 | } else {
605 | uri = fmt.Sprintf("http://watch.live.nicovideo.jp/api/getplayerstatus?v=%s", opt.NicoLiveId)
606 | uriE = fmt.Sprintf("http://watch.live.nicovideo.jp/api/getedgestatus?v=%s", opt.NicoLiveId)
607 | }
608 |
609 | header := make(map[string]string, 4)
610 | if opt.NicoSession != "" {
611 | header["Cookie"] = "user_session=" + opt.NicoSession
612 | }
613 |
614 | // experimental
615 | //if opt.NicoStatusHTTPS {
616 | // req.Header.Set("User-Agent", "Niconico/1.0 (Unix; U; iPhone OS 10.3.3; ja-jp; nicoiphone; iPhone5,2) Version/6.65")
617 | //}
618 |
619 | resp, err, neterr := httpbase.Get(uri, header)
620 | if err != nil {
621 | return
622 | }
623 | if neterr != nil {
624 | err = neterr
625 | return
626 | }
627 | defer resp.Body.Close()
628 |
629 | dat, _ := ioutil.ReadAll(resp.Body)
630 | status = &Status{}
631 | err = xml.Unmarshal(dat, status)
632 | if err != nil {
633 | //fmt.Println(string(dat))
634 | fmt.Printf("error: %v", err)
635 | return
636 | }
637 |
638 | switch status.ErrorCode {
639 | case "":
640 | case "notlogin":
641 | notLogin = true
642 | default:
643 | err = fmt.Errorf("Error code: %s\n", status.ErrorCode)
644 | return
645 | }
646 |
647 | respE, err, neterr := httpbase.Get(uriE, header)
648 | if err != nil {
649 | return
650 | }
651 | if neterr != nil {
652 | err = neterr
653 | return
654 | }
655 | defer respE.Body.Close()
656 |
657 | datE, _ := ioutil.ReadAll(respE.Body)
658 | statusE := &StatusE{}
659 | err = xml.Unmarshal(datE, statusE)
660 | if err != nil {
661 | //fmt.Println(string(dat))
662 | fmt.Printf("error: %v", err)
663 | return
664 | }
665 |
666 | switch statusE.ErrorCode {
667 | case "":
668 | case "notlogin":
669 | notLogin = true
670 | default:
671 | err = fmt.Errorf("Error code: %s\n", statusE.ErrorCode)
672 | return
673 | }
674 |
675 | if statusE.Url != "" {
676 | status.Url = statusE.Url
677 | }
678 | if statusE.Ticket != "" {
679 | status.Ticket = statusE.Ticket
680 | }
681 |
682 | return
683 | }
684 |
685 | func NicoRecRtmp(opt options.Option) (notLogin bool, err error) {
686 | status, notLogin, err := getStatus(opt)
687 | if err != nil {
688 | return
689 | }
690 | if notLogin {
691 | return
692 | }
693 |
694 | status.recAllStreams(opt)
695 | return
696 | }
697 |
--------------------------------------------------------------------------------
/src/objs/objs.go:
--------------------------------------------------------------------------------
1 |
2 | package objs
3 |
4 | import (
5 | "fmt"
6 | "encoding/json"
7 | )
8 |
9 | func PrintAsJson(data interface{}) {
10 | json, err := json.MarshalIndent(data, "", " ")
11 | if err != nil {
12 | return
13 | }
14 | fmt.Println(string(json))
15 | }
16 | func Find(intf interface{}, keylist... string) (res interface{}, ok bool) {
17 | res = intf
18 | if len(keylist) == 0 {
19 | ok = true
20 | return
21 | }
22 | for i, k := range keylist {
23 | var test bool
24 | //var obj map[string]interface{}
25 | switch res.(type) {
26 | case map[string]interface{}:
27 | res, test = res.(map[string]interface{})[k]
28 | if (! test) {
29 | ok = false
30 | return
31 | }
32 | case []interface{}:
33 | for _, o := range res.([]interface{}) {
34 | _res, _ok := Find(o, keylist[i:]...)
35 | if _ok {
36 | res = _res
37 | ok = _ok
38 | return
39 | }
40 | }
41 | }
42 | }
43 | ok = true
44 | return
45 | }
46 | func FindFloat64(intf interface{}, keylist... string) (res float64, ok bool) {
47 | val, ok := Find(intf, keylist...)
48 | if !ok {
49 | return
50 | }
51 | res, ok = val.(float64)
52 | return
53 | }
54 | func FindString(intf interface{}, keylist... string) (res string, ok bool) {
55 | val, ok := Find(intf, keylist...)
56 | if !ok {
57 | return
58 | }
59 | res, ok = val.(string)
60 | return
61 | }
62 | func FindBool(intf interface{}, keylist... string) (res bool, ok bool) {
63 | val, ok := Find(intf, keylist...)
64 | if !ok {
65 | return
66 | }
67 | res, ok = val.(bool)
68 | return
69 | }
70 | func FindArray(intf interface{}, keylist... string) (res []interface{}, ok bool) {
71 | val, ok := Find(intf, keylist...)
72 | if !ok {
73 | return
74 | }
75 | res, ok = val.([]interface{})
76 | return
77 | }
78 |
79 |
--------------------------------------------------------------------------------
/src/procs/base/base.go:
--------------------------------------------------------------------------------
1 | package base
2 |
3 | import (
4 | "io"
5 | "os"
6 | "os/exec"
7 | )
8 |
9 | func Open(cmdList *[]string, stdinEn, stdoutEn, stdErrEn, consoleEn bool, args []string) (cmd *exec.Cmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser, err error) {
10 |
11 | for i, cmdName := range *cmdList {
12 | cmd = exec.Command(cmdName, args...)
13 |
14 | if stdinEn {
15 | stdin, err = cmd.StdinPipe()
16 | if err != nil {
17 | return
18 | }
19 | }
20 |
21 | if stdoutEn {
22 | stdout, err = cmd.StdoutPipe()
23 | if err != nil {
24 | return
25 | }
26 | } else {
27 | if consoleEn {
28 | cmd.Stdout = os.Stdout
29 | }
30 | }
31 |
32 | if stdErrEn {
33 | stderr, err = cmd.StderrPipe()
34 | if err != nil {
35 | return
36 | }
37 | } else {
38 | if consoleEn {
39 | cmd.Stderr = os.Stderr
40 | }
41 | }
42 |
43 | if err = cmd.Start(); err != nil {
44 | continue
45 | } else {
46 | if i != 0 {
47 | *cmdList = []string{cmdName}
48 | }
49 | //fmt.Printf("CMD: %#v\n", cmd.Args)
50 | return
51 | }
52 | }
53 |
54 | // prog not found
55 | cmd = nil
56 | return
57 | }
--------------------------------------------------------------------------------
/src/procs/ffmpeg/ffmpeg.go:
--------------------------------------------------------------------------------
1 | package ffmpeg
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os/exec"
7 | "../base"
8 | )
9 |
10 | var cmdList = []string{
11 | "./bin/ffmpeg/bin/ffmpeg",
12 | "./bin/ffmpeg/ffmpeg",
13 | "./bin/ffmpeg",
14 | "./ffmpeg/bin/ffmpeg",
15 | "./ffmpeg/ffmpeg",
16 | "./ffmpeg",
17 | "ffmpeg",
18 | }
19 |
20 | func Open(opt... string) (cmd *exec.Cmd, stdin io.WriteCloser, err error) {
21 | cmd, stdin, _, _, err = base.Open(&cmdList, true, false, false, true, opt)
22 | if cmd == nil {
23 | err = fmt.Errorf("ffmpeg not found")
24 | return
25 | }
26 | return
27 | }
28 |
--------------------------------------------------------------------------------
/src/procs/kill.go:
--------------------------------------------------------------------------------
1 | package procs
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | "log"
7 |
8 | "./base"
9 | )
10 |
11 | func Kill(pid int) {
12 | if runtime.GOOS == "windows" {
13 | options := []string{
14 | "/PID", fmt.Sprintf("%v", pid),
15 | "/T",
16 | "/F",
17 | }
18 | list := []string{"taskkill"}
19 | if taskkill, _, _, _, err := base.Open(&list, false, false, false, false, options); err == nil {
20 | taskkill.Wait()
21 | }
22 |
23 | } else {
24 | log.Fatalf("[FIXME] Kill for %v not supported", runtime.GOOS)
25 | }
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/src/procs/streamlink/streamlink.go:
--------------------------------------------------------------------------------
1 | package streamlink
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os/exec"
7 | "../base"
8 | )
9 |
10 | var cmdList = []string{
11 | "./bin/streamlink/streamlink",
12 | "./bin/Streamlink/Streamlink",
13 | "./bin/streamlink",
14 | "./bin/Streamlink",
15 | "./streamlink/streamlink",
16 | "./Streamlink/Streamlink",
17 | "./Streamlink",
18 | "streamlink",
19 | "Streamlink",
20 | }
21 |
22 | func Open(opt... string) (cmd *exec.Cmd, stdout, stderr io.ReadCloser, err error) {
23 | cmd, _, stdout, stderr, err = base.Open(&cmdList, false, true, true, false, opt)
24 | if cmd == nil {
25 | err = fmt.Errorf("streamlink not found")
26 | return
27 | }
28 | return
29 | }
30 |
--------------------------------------------------------------------------------
/src/procs/youtube_dl/youtube-dl.go:
--------------------------------------------------------------------------------
1 | package youtube_dl
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os/exec"
7 | "../base"
8 | )
9 |
10 | var cmdList = []string{
11 | "./bin/youtube-dl/youtube-dl",
12 | "./bin/youtube-dl",
13 | "./youtube-dl/youtube-dl",
14 | "./youtube-dl",
15 | "youtube-dl",
16 | }
17 |
18 | func Open(opt... string) (cmd *exec.Cmd, stdout, stderr io.ReadCloser, err error) {
19 | cmd, _, stdout, stderr, err = base.Open(&cmdList, false, true, true, false, opt)
20 | if cmd == nil {
21 | err = fmt.Errorf("youtube-dl not found")
22 | return
23 | }
24 | return
25 | }
26 |
--------------------------------------------------------------------------------
/src/rtmps/message.go:
--------------------------------------------------------------------------------
1 | package rtmps
2 |
3 | import (
4 | "encoding/binary"
5 | "time"
6 | "bytes"
7 | "log"
8 | "io"
9 |
10 | "fmt"
11 | "../amf"
12 | )
13 |
14 | const (
15 | TID_SETCHUNKSIZE = 1
16 | TID_ABORT = 2
17 | TID_ACKNOWLEDGEMENT = 3
18 | TID_USERCONTROL = 4
19 | TID_WINDOW_ACK_SIZE = 5
20 | TID_SETPEERBANDWIDTH = 6
21 | TID_AUDIO = 8
22 | TID_VIDEO = 9
23 | TID_AMF3COMMAND = 17
24 | TID_AMF0COMMAND = 20
25 | TID_AMF0DATA = 18
26 | TID_AMF3DATA = 15
27 | TID_AGGREGATE = 22
28 | )
29 |
30 | const (
31 | UC_STREAMBEGIN = 0
32 | UC_STREAMEOF = 1
33 | UC_STREAMDRY = 2
34 | UC_SETBUFFERLENGTH = 3
35 | UC_STREAMISRECORDED = 4
36 | UC_PINGREQUEST = 6
37 | UC_PINGRESPONSE = 7
38 |
39 | UC_BUFFEREMPTY = 31
40 | UC_BUFFERREADY = 32
41 | )
42 |
43 |
44 | func intToBE16(num int) (data []byte) {
45 | tmp := make([]byte, 2)
46 | binary.BigEndian.PutUint16(tmp, uint16(num))
47 | data = append(data, tmp[:]...)
48 | return
49 | }
50 | func intToBE24(num int) (data []byte) {
51 | tmp := make([]byte, 4)
52 | binary.BigEndian.PutUint32(tmp, uint32(num))
53 | data = append(data, tmp[1:]...)
54 | return
55 | }
56 | func intToBE32(num int) (data []byte) {
57 | tmp := make([]byte, 4)
58 | binary.BigEndian.PutUint32(tmp, uint32(num))
59 | data = append(data, tmp[:]...)
60 | return
61 | }
62 | func intToLE32(num int) (data []byte) {
63 | tmp := make([]byte, 4)
64 | binary.LittleEndian.PutUint32(tmp, uint32(num))
65 | data = append(data, tmp[:]...)
66 | return
67 | }
68 |
69 |
70 | func chunkBasicHeader(fmt, csid int) (data []byte) {
71 | if 2 <= csid && csid <= 63 {
72 | b := byte(((fmt & 3) << 6) | (csid & 0x3F))
73 | data = append(data, b)
74 |
75 | } else if 64 <= csid && csid <= 319 {
76 | b0 := byte((fmt & 3) << 6)
77 | b1 := byte(csid - 64)
78 | data = append(data, b0, b1)
79 |
80 | } else if 320 <= csid && csid <= 65599 {
81 | b0 := byte(((fmt & 3) << 6) | 1)
82 | b1 := byte((csid & 0xFF) - 64)
83 | b2 := byte(csid >> 8)
84 | data = append(data, b0, b1, b2)
85 |
86 | } else {
87 | log.Printf("[FIXME] Chunk basic header: csid out of range: %d", csid)
88 | }
89 |
90 | return
91 | }
92 |
93 | var start = millisec()
94 |
95 | func millisec() int64 {
96 | return time.Now().UnixNano() / int64(time.Millisecond)
97 | }
98 | func getTime() int {
99 | delta := millisec() - start
100 | return int(delta)
101 | }
102 |
103 | func type0(buff *bytes.Buffer, csId int, typeId byte, streamId int, length int) {
104 | buff.Write(chunkBasicHeader(0, csId))
105 |
106 | // timestamp
107 | //buff.Write(intToBE24(getTime()))
108 | buff.Write(intToBE24(0))
109 | // message length
110 | buff.Write(intToBE24(length))
111 | // message type id
112 | buff.WriteByte(typeId)
113 | // Stream ID
114 | buff.Write(intToLE32(streamId))
115 | // body
116 | //buff.Write(body)
117 |
118 | return
119 | }
120 | func type3(buff *bytes.Buffer, csId int) {
121 | buff.Write(chunkBasicHeader(3, csId))
122 | }
123 | func encodeAcknowledgement(asz int) (buff *bytes.Buffer, err error){
124 | buff = bytes.NewBuffer(nil)
125 | bsz := intToBE32(asz)
126 | type0(buff, 2, TID_ACKNOWLEDGEMENT, 0, len(bsz))
127 | if _, err = buff.Write(bsz); err != nil {
128 | return
129 | }
130 | return
131 | }
132 | func encodeWindowAckSize(asz int) (buff *bytes.Buffer, err error){
133 | buff = bytes.NewBuffer(nil)
134 | bsz := intToBE32(asz)
135 | type0(buff, 2, TID_WINDOW_ACK_SIZE, 0, len(bsz))
136 | if _, err = buff.Write(bsz); err != nil {
137 | return
138 | }
139 | return
140 | }
141 | func encodeSetPeerBandwidth(wsz, lim int) (buff *bytes.Buffer, err error){
142 | buff = bytes.NewBuffer(nil)
143 | b := intToBE32(wsz)
144 | b = append(b, byte(lim))
145 | type0(buff, 2, TID_SETPEERBANDWIDTH, 0, len(b))
146 | if _, err = buff.Write(b); err != nil {
147 | return
148 | }
149 | return
150 | }
151 |
152 | func encodePingResponse(timestamp int) (buff *bytes.Buffer, err error){
153 | buff = bytes.NewBuffer(nil)
154 |
155 | var body []byte
156 | body = append(body, intToBE16(UC_PINGRESPONSE)...)
157 | body = append(body, intToBE32(timestamp)...)
158 |
159 | type0(buff, 2, TID_USERCONTROL, 0, len(body))
160 | if _, err = buff.Write(body); err != nil {
161 | return
162 | }
163 | return
164 | }
165 | func encodeSetBufferLength(streamId, length int) (buff *bytes.Buffer, err error){
166 | buff = bytes.NewBuffer(nil)
167 |
168 | var body []byte
169 | body = append(body, intToBE16(UC_SETBUFFERLENGTH)...)
170 | body = append(body, intToBE32(streamId)...)
171 | body = append(body, intToBE32(int(length))...)
172 |
173 | type0(buff, 2, TID_USERCONTROL, 0, len(body))
174 | if _, err = buff.Write(body); err != nil {
175 | return
176 | }
177 | return
178 | }
179 | func amf0Command(chunkSize, csId, streamId int, body []byte) (wbuff *bytes.Buffer, err error) {
180 | wbuff = bytes.NewBuffer(nil)
181 | rbuff := bytes.NewBuffer(body)
182 |
183 | type0(wbuff, csId, TID_AMF0COMMAND, streamId, rbuff.Len())
184 | if chunkSize < rbuff.Len() {
185 | if _, err = io.CopyN(wbuff, rbuff, int64(chunkSize)); err != nil {
186 | return
187 | }
188 | } else {
189 | if _, err = io.CopyN(wbuff, rbuff, int64(rbuff.Len())); err != nil {
190 | return
191 | }
192 | }
193 |
194 | for rbuff.Len() > 0 {
195 | type3(wbuff, csId)
196 |
197 | if chunkSize < rbuff.Len() {
198 | if _, err = io.CopyN(wbuff, rbuff, int64(chunkSize)); err != nil {
199 | return
200 | }
201 | } else {
202 | if _, err = io.CopyN(wbuff, rbuff, int64(rbuff.Len())); err != nil {
203 | return
204 | }
205 | }
206 | }
207 |
208 | //log.Fatalf("amf0Command %#v", wbuff)
209 |
210 | return
211 | }
212 |
213 |
214 |
215 | func decodeFmtCsId(rdr io.Reader, msg *rtmpMsg) (err error) {
216 | b0 := make([]byte, 1)
217 | msg.hdrLength++
218 | _, err = io.ReadFull(rdr, b0); if err != nil {
219 | return
220 | }
221 | format := (int(b0[0]) >> 6) & 3
222 | csId := int(b0[0]) & 0x3F
223 | switch csId {
224 | case 0:
225 | b1 := make([]byte, 1)
226 | msg.hdrLength++
227 | if _, err = io.ReadFull(rdr, b1); err != nil {
228 | return
229 | }
230 | csId = int(b1[0]) + 64
231 |
232 | case 1:
233 | b1 := make([]byte, 2)
234 | msg.hdrLength += 2
235 | if _, err = io.ReadFull(rdr, b1); err != nil {
236 | return
237 | }
238 | csId = (int(b1[1]) << 8) | (int(b1[0]) + 64)
239 | }
240 |
241 | msg.format = format
242 | msg.csId = csId
243 | if (! msg.readingBody) {
244 | msg.formatOrigin = format
245 | msg.csIdOrigin = csId
246 | }
247 | // fmt.Printf("debug format type %v csid %v\n", format, csId)
248 | return
249 | }
250 |
251 | func decodeInt8(rdr io.Reader) (num int, err error) {
252 | buf := make([]byte, 1)
253 | if _, err = io.ReadFull(rdr, buf); err != nil {
254 | return
255 | }
256 | num = int(buf[0])
257 | return
258 | }
259 | func decodeBEInt16(rdr io.Reader) (num int, err error) {
260 | buf := make([]byte, 2)
261 | if _, err = io.ReadFull(rdr, buf); err != nil {
262 | return
263 | }
264 | num = (int(buf[0]) << 8) | int(buf[1])
265 | return
266 | }
267 | func decodeBEInt24(rdr io.Reader) (num int, err error) {
268 | buf := make([]byte, 3)
269 | if _, err = io.ReadFull(rdr, buf); err != nil {
270 | return
271 | }
272 | num = (int(buf[0]) << 16) | (int(buf[1]) << 8) | int(buf[2])
273 | return
274 | }
275 | func decodeBEInt32(rdr io.Reader) (num int, err error) {
276 | buf := make([]byte, 4)
277 | if _, err = io.ReadFull(rdr, buf); err != nil {
278 | return
279 | }
280 | num = (int(buf[0]) << 24) | (int(buf[1]) << 16) | (int(buf[2]) << 8) | int(buf[3])
281 | return
282 | }
283 | func decodeLEInt32(rdr io.Reader) (num int, err error) {
284 | buf := make([]byte, 4)
285 | if _, err = io.ReadFull(rdr, buf); err != nil {
286 | return
287 | }
288 | num = (int(buf[3]) << 24) | (int(buf[2]) << 16) | (int(buf[1]) << 8) | int(buf[0])
289 | return
290 | }
291 |
292 | func decodeTimestamp(rdr io.Reader, msg *rtmpMsg) (err error) {
293 | msg.hdrLength += 3
294 | timestamp, err := decodeBEInt24(rdr)
295 | if err != nil {
296 | return
297 | }
298 | msg.timestampField = timestamp
299 |
300 | return
301 | }
302 | func decodeTimestampEX(rdr io.Reader, msg *rtmpMsg) (err error) {
303 | msg.hdrLength += 4
304 | timestamp, err := decodeBEInt32(rdr)
305 | if err != nil {
306 | return
307 | }
308 | //fmt.Printf("decodeTimestampEX %v\n", timestamp)
309 | if (! msg.readingBody) {
310 | msg.timestampEx = timestamp
311 | }
312 |
313 | return
314 | }
315 | func decodeMsgLength(rdr io.Reader, msg *rtmpMsg) (err error) {
316 | msg.hdrLength += 3
317 | length, err := decodeBEInt24(rdr)
318 | msg.msgLength = length
319 | return
320 | }
321 | func decodeMsgType(rdr io.Reader, msg *rtmpMsg) (err error) {
322 | msg.hdrLength += 1
323 | msg_t, err := decodeInt8(rdr)
324 | msg.msgTypeId = msg_t
325 | return
326 | }
327 | func decodeStreamId(rdr io.Reader, msg *rtmpMsg) (err error) {
328 | msg.hdrLength += 4
329 | sid, err := decodeLEInt32(rdr)
330 | msg.msgStreamId = sid
331 | return
332 | }
333 |
334 | func decodeType0(rdr io.Reader, msg *rtmpMsg) (err error) {
335 | if err = decodeTimestamp(rdr, msg); err != nil {
336 | return
337 | }
338 | if err = decodeMsgLength(rdr, msg); err != nil {
339 | return
340 | }
341 | if err = decodeMsgType(rdr, msg); err != nil {
342 | return
343 | }
344 | err = decodeStreamId(rdr, msg)
345 | return
346 | }
347 | func decodeType1(rdr io.Reader, msg *rtmpMsg) (err error) {
348 | if err = decodeTimestamp(rdr, msg); err != nil {
349 | return
350 | }
351 | if err = decodeMsgLength(rdr, msg); err != nil {
352 | return
353 | }
354 | err = decodeMsgType(rdr, msg)
355 | return
356 | }
357 | func decodeType2(rdr io.Reader, msg *rtmpMsg) (err error) {
358 | err = decodeTimestamp(rdr, msg)
359 | return
360 | }
361 |
362 |
363 |
364 |
365 | type rtmpMsg struct {
366 | format int
367 | formatOrigin int
368 | csId int
369 | csIdOrigin int
370 | timestampField int
371 | timestampDelta int
372 | timestampEx int
373 | timestampActual int
374 | msgLength int
375 | msgTypeId int
376 | msgStreamId int
377 | bodyBuff *bytes.Buffer
378 |
379 | readingBody bool
380 | hdrLength int
381 | splitCount int
382 | }
383 |
384 | func readChunkBody(rdr io.Reader, msg *rtmpMsg, csz int) (err error) {
385 |
386 | if msg.bodyBuff == nil {
387 | msg.bodyBuff = bytes.NewBuffer(nil)
388 | }
389 | rem := msg.msgLength - msg.bodyBuff.Len()
390 | //fmt.Printf("readChunkBody: %v %v\n", msg.msgLength, msg.bodyBuff.Len())
391 | if rem > csz {
392 | _, err = io.CopyN(msg.bodyBuff, rdr, int64(csz))
393 | } else {
394 | _, err = io.CopyN(msg.bodyBuff, rdr, int64(rem))
395 | }
396 | if err != nil {
397 | return
398 | }
399 |
400 | return
401 | }
402 |
403 | func decodeHeader(rdr io.Reader, msg *rtmpMsg) (err error) {
404 | if err = decodeFmtCsId(rdr, msg); err != nil {
405 | return
406 | }
407 | switch msg.format {
408 | case 0:
409 | if err = decodeType0(rdr, msg); err != nil {
410 | return
411 | }
412 | case 1:
413 | if err = decodeType1(rdr, msg); err != nil {
414 | return
415 | }
416 | case 2:
417 | if err = decodeType2(rdr, msg); err != nil {
418 | return
419 | }
420 | case 3:
421 | if (msg.readingBody) {
422 | msg.splitCount++
423 | if msg.csId != msg.csIdOrigin {
424 | err = &DecodeError{
425 | Fun: "decodeHeader",
426 | Msg: fmt.Sprintf("msg.csId(%d) != msg.csIdOrigin(%d)", msg.csId, msg.csIdOrigin),
427 | }
428 | return
429 | }
430 | }
431 | default:
432 | err = &DecodeError{
433 | Fun: "decodeHeader",
434 | Msg: fmt.Sprintf("Unknown fmt: %v", msg.format),
435 | }
436 | return
437 | }
438 |
439 | return
440 | }
441 |
442 | func decodeSetChunkSize(rbuff *bytes.Buffer) (csz int, err error) {
443 | num, e := decodeBEInt32(rbuff)
444 | if e != nil {
445 | err = e
446 | return
447 | }
448 | csz = num & 0x7fffffff
449 | return
450 | }
451 |
452 | func decodeWindowAckSize(rbuff *bytes.Buffer) (asz int, err error) {
453 | asz, e := decodeBEInt32(rbuff)
454 | if e != nil {
455 | err = e
456 | return
457 | }
458 | return
459 | }
460 |
461 | func decodeSetPeerBandwidth(rbuff *bytes.Buffer) (res []int, err error) {
462 | wsz, err := decodeBEInt32(rbuff)
463 | if err != nil {
464 | return
465 | }
466 | lim, err := decodeInt8(rbuff)
467 | if err != nil {
468 | return
469 | }
470 | res = append(res, wsz, lim)
471 | return
472 | }
473 | func decodeUserControl(rbuff *bytes.Buffer) (res []int, err error) {
474 | evt, err := decodeBEInt16(rbuff)
475 | if err != nil {
476 | return
477 | }
478 | res = append(res, evt)
479 | switch evt {
480 | case UC_BUFFEREMPTY, UC_BUFFERREADY: // Buffer Empty, Buffer Ready
481 | // http://repo.or.cz/w/rtmpdump.git/blob/8880d1456b282ee79979adbe7b6a6eb8ad371081:/librtmp/rtmp.c#l2787
482 |
483 | case
484 | UC_STREAMBEGIN,
485 | UC_STREAMEOF,
486 | UC_STREAMDRY,
487 | UC_STREAMISRECORDED,
488 | UC_PINGREQUEST,
489 | UC_PINGRESPONSE:
490 | // 4-byte stream id
491 | num, e := decodeBEInt32(rbuff)
492 | if e != nil {
493 | err = e
494 | return
495 | }
496 | res = append(res, num)
497 |
498 | case UC_SETBUFFERLENGTH:
499 | // 4-byte stream id
500 | sid, e := decodeBEInt32(rbuff)
501 | if e != nil {
502 | err = e
503 | return
504 | }
505 | res = append(res, sid)
506 | // 4-byte buffer length
507 | bsz, e := decodeBEInt32(rbuff)
508 | if e != nil {
509 | err = e
510 | return
511 | }
512 | res = append(res, bsz)
513 |
514 | default:
515 | err = &DecodeError{
516 | Fun: "decodeUserControl",
517 | Msg: fmt.Sprintf("Unknown User control: %v", evt),
518 | }
519 | return
520 | }
521 | return
522 | }
523 |
524 | type message struct {
525 | msg_t int
526 | timestamp int
527 | data *bytes.Buffer
528 | }
529 | func decodeMessage(rbuff *bytes.Buffer) (res message, err error) {
530 | msg_t, err := decodeInt8(rbuff)
531 | if err != nil {
532 | return
533 | }
534 | plen, err := decodeBEInt24(rbuff)
535 | if err != nil {
536 | return
537 | }
538 | ts_0, err := decodeBEInt24(rbuff)
539 | if err != nil {
540 | return
541 | }
542 | ts_1, err := decodeInt8(rbuff)
543 | if err != nil {
544 | return
545 | }
546 | ts := (ts_1 << 24) | ts_0
547 |
548 | // stream id
549 | _, err = decodeBEInt24(rbuff)
550 | if err != nil {
551 | return
552 | }
553 | //fmt.Printf("debug decodeMessage: type(%v) len(%v) ts(%v)\n", msg_t, plen, ts_0)
554 | buff := bytes.NewBuffer(nil)
555 | if _, err = io.CopyN(buff, rbuff, int64(plen)); err != nil {
556 | return
557 | }
558 |
559 | // backPointer
560 | _, err = decodeBEInt32(rbuff)
561 | if err != nil {
562 | return
563 | }
564 |
565 | res = message{
566 | msg_t: msg_t,
567 | timestamp: ts,
568 | data: buff,
569 | }
570 |
571 | return
572 | }
573 | func decodeAggregate(rbuff *bytes.Buffer) (res []message, err error) {
574 | for rbuff.Len() > 0 {
575 | msg, e := decodeMessage(rbuff)
576 | if e != nil {
577 | err = e
578 | return
579 | }
580 | res = append(res, msg)
581 | }
582 | return
583 | }
584 |
585 | func decodeOne(rdr io.Reader, csz int, info map[int] chunkInfo) (ts int, msg_t int, res interface{}, rsz int, err error) {
586 | msg := rtmpMsg{}
587 |
588 | // rtmp header
589 | if err = decodeHeader(rdr, &msg); err != nil {
590 | return
591 | }
592 |
593 | // restore fields from previous chunk header
594 |
595 | var prevChunk chunkInfo
596 | if msg.formatOrigin != 0 {
597 | var ok bool
598 | if prevChunk, ok = info[msg.csIdOrigin]; (! ok) {
599 | err = &DecodeError{
600 | Fun: "decodeOne",
601 | Msg: fmt.Sprintf("Not exists previous chunk(csId = %v)", msg.csIdOrigin),
602 | }
603 | return
604 | }
605 | }
606 | //fmt.Printf("debug decodeOne msg.timestampField %d\n", msg.timestampField)
607 | if (msg.timestampField == 0xffffff) || ((msg.formatOrigin == 3) && (prevChunk.timestampField == 0xffffff)) {
608 | if err = decodeTimestampEX(rdr, &msg); err != nil {
609 | return
610 | }
611 | //fmt.Printf("%#v\n", msg)
612 | switch msg.formatOrigin {
613 | case 0:
614 | msg.timestampActual = msg.timestampEx
615 | msg.timestampDelta = msg.timestampEx
616 | case 1, 2:
617 | msg.timestampActual = prevChunk.timestampActual + msg.timestampEx
618 | msg.timestampDelta = msg.timestampEx
619 | case 3:
620 | msg.timestampActual = msg.timestampEx
621 | msg.timestampDelta = msg.timestampEx
622 | msg.timestampField = 0xffffff
623 | }
624 | } else {
625 | switch msg.formatOrigin {
626 | case 0:
627 | msg.timestampActual = msg.timestampField
628 | msg.timestampDelta = msg.timestampField
629 | case 1, 2:
630 | msg.timestampActual = prevChunk.timestampActual + msg.timestampField
631 | msg.timestampDelta = msg.timestampField
632 | case 3:
633 | msg.timestampActual = prevChunk.timestampActual + prevChunk.timestampDelta
634 | msg.timestampDelta = prevChunk.timestampDelta
635 | }
636 | }
637 |
638 | switch msg.formatOrigin {
639 | case 1:
640 | msg.msgStreamId = prevChunk.msgStreamId
641 | case 2, 3:
642 | msg.msgLength = prevChunk.msgLength
643 | msg.msgTypeId = prevChunk.msgTypeId
644 | msg.msgStreamId = prevChunk.msgStreamId
645 | }
646 |
647 | info[msg.csId] = chunkInfo{
648 | timestampField: msg.timestampField,
649 | timestampDelta: msg.timestampDelta,
650 | timestampActual: msg.timestampActual,
651 | msgLength: msg.msgLength,
652 | msgTypeId: msg.msgTypeId,
653 | msgStreamId: msg.msgStreamId,
654 | }
655 |
656 | ts = msg.timestampActual
657 |
658 | msg.readingBody = true
659 |
660 | // rtmp payload
661 | for {
662 | if err = readChunkBody(rdr, &msg, csz); err != nil {
663 | return
664 | }
665 |
666 | if msg.msgLength <= msg.bodyBuff.Len() {
667 | break
668 | }
669 |
670 | //if err = decodeFmtCsId(rdr, &msg); err != nil {
671 | if err = decodeHeader(rdr, &msg); err != nil {
672 | return
673 | }
674 |
675 | // timestamp extended
676 | if (msg.timestampField == 0xffffff) {
677 | if err = decodeTimestampEX(rdr, &msg); err != nil {
678 | return
679 | }
680 | }
681 | }
682 |
683 | //fmt.Printf("debug rtmp decodeOne: %#v\n", msg)
684 | // read byte count
685 | rsz = msg.hdrLength + msg.msgLength
686 |
687 | msg_t = msg.msgTypeId
688 | switch msg.msgTypeId {
689 | case TID_AGGREGATE:
690 | if res, err = decodeAggregate(msg.bodyBuff); err != nil {
691 | return
692 | }
693 |
694 | case TID_AUDIO, TID_VIDEO:
695 | res = msg.bodyBuff
696 |
697 | case TID_WINDOW_ACK_SIZE:
698 | if res, err = decodeWindowAckSize(msg.bodyBuff); err != nil {
699 | return
700 | }
701 | case TID_SETPEERBANDWIDTH:
702 | if res, err = decodeSetPeerBandwidth(msg.bodyBuff); err != nil {
703 | return
704 | }
705 | case TID_AMF0COMMAND:
706 | if res, err = amf.DecodeAmf0(msg.bodyBuff.Bytes()); err != nil {
707 | return
708 | }
709 | case TID_AMF3COMMAND:
710 | if res, err = amf.DecodeAmf0(msg.bodyBuff.Bytes(), true); err != nil {
711 | return
712 | }
713 | case TID_AMF0DATA:
714 | if res, err = amf.DecodeAmf0(msg.bodyBuff.Bytes()); err != nil {
715 | return
716 | }
717 | case TID_SETCHUNKSIZE:
718 | if res, err = decodeSetChunkSize(msg.bodyBuff); err != nil {
719 | return
720 | }
721 | case TID_USERCONTROL:
722 | if res, err = decodeUserControl(msg.bodyBuff); err != nil {
723 | return
724 | }
725 | default:
726 | err = &DecodeError{
727 | Fun: "decodeOne",
728 | Msg: fmt.Sprintf("msgTypeId: not implement: %v\n%#v", msg.msgTypeId, msg.bodyBuff.Bytes()),
729 | }
730 | return
731 | }
732 |
733 | return
734 | }
--------------------------------------------------------------------------------
/src/rtmps/rtmp.go:
--------------------------------------------------------------------------------
1 | package rtmps
2 |
3 | import (
4 | "net"
5 | "fmt"
6 | "bytes"
7 | "math/rand"
8 | "time"
9 | "io"
10 | "io/ioutil"
11 | "regexp"
12 | "../amf"
13 | "../flvs"
14 | "../objs"
15 | "../files"
16 | )
17 |
18 | type DecodeError struct {
19 | Fun string
20 | Msg string
21 | }
22 | func (e *DecodeError) Error() string {
23 | return fmt.Sprintf("%s: %s", e.Fun, e.Msg)
24 | }
25 |
26 | type chunkInfo struct {
27 | timestampField int
28 | timestampDelta int
29 | timestampActual int
30 | msgLength int
31 | msgTypeId int
32 | msgStreamId int
33 | }
34 |
35 | type Rtmp struct {
36 | proto string // No reset
37 | address string // No reset
38 | app string // No reset
39 | tcUrl string // No reset
40 | swfUrl string // No reset
41 | pageUrl string // No reset
42 | connectOpt []interface{}
43 |
44 | conn *net.TCPConn // RESET_ON_CONNECT
45 | chunkSizeSend int // RESET_ON_CONNECT
46 | chunkSizeRecv int // RESET_ON_CONNECT
47 | transactionId int // RESET_ON_CONNECT
48 | windowSize int // RESET_ON_CONNECT
49 | chunkInfo map[int] chunkInfo // RESET_ON_CONNECT
50 |
51 | readCount int // RESET_ON_CONNECT
52 | totalReadBytes int // RESET_ON_CONNECT
53 | isRecorded bool
54 |
55 | timestamp int // NO_RESET
56 | duration int
57 |
58 | flvName string
59 | flv *flvs.Flv
60 |
61 | fixAggrTimestamp bool
62 | streamId int
63 | nextLogTs int
64 |
65 | VideoExists bool
66 | noSeek bool
67 | flush bool
68 |
69 | startTime int
70 | }
71 |
72 | func NewRtmp(tc, swf, page string, opt... interface{})(rtmp *Rtmp, err error) {
73 | re := regexp.MustCompile(`\A(\w+)://([^/\s]+)/(\S+)\z`)
74 | mstr := re.FindStringSubmatch(tc)
75 | if mstr == nil {
76 | err = fmt.Errorf("tcUrl incorrect: %v", tc)
77 | return
78 | }
79 |
80 | rtmp = &Rtmp{
81 | proto: mstr[1],
82 | address: mstr[2],
83 | app: mstr[3],
84 | tcUrl: tc,
85 | swfUrl: swf,
86 | pageUrl: page,
87 | connectOpt: opt,
88 | }
89 |
90 | return
91 | }
92 | func (rtmp *Rtmp) Connect() (err error) {
93 | if rtmp.conn != nil {
94 | rtmp.conn.Close()
95 | rtmp.conn = nil
96 | time.Sleep(3)
97 | }
98 |
99 | rtmp.windowSize = 2500000
100 | rtmp.chunkInfo = make(map[int] chunkInfo)
101 | rtmp.chunkSizeSend = 128
102 | rtmp.chunkSizeRecv = 128
103 | rtmp.transactionId = 1
104 |
105 | rtmp.readCount = 0
106 | rtmp.totalReadBytes = 0
107 |
108 | err = rtmp.connect(
109 | rtmp.app,
110 | rtmp.tcUrl,
111 | rtmp.swfUrl,
112 | rtmp.pageUrl,
113 | rtmp.connectOpt...,
114 | )
115 | return
116 | }
117 | func (rtmp *Rtmp) SetFlush(b bool) {
118 | rtmp.flush = b
119 | }
120 | func (rtmp *Rtmp) SetNoSeek(b bool) {
121 | rtmp.noSeek = b
122 | }
123 | func (rtmp *Rtmp) SetConnectOpt(opt... interface{}) {
124 | rtmp.connectOpt = opt
125 | }
126 | func (rtmp *Rtmp) connect(app, tc, swf, page string, opt... interface{}) (err error) {
127 |
128 | raddr, err := net.ResolveTCPAddr("tcp", rtmp.address)
129 | if err != nil {
130 | fmt.Printf("%v\n", err)
131 | return
132 | }
133 |
134 | switch rtmp.proto {
135 | case "rtmp":
136 | conn, e := net.DialTCP("tcp", nil, raddr)
137 | if e != nil {
138 | err = e
139 | return
140 | }
141 | rtmp.conn = conn
142 |
143 | default:
144 | err = fmt.Errorf("Unknown protocol: %v", rtmp.proto)
145 | return
146 | }
147 |
148 | err = handshake(rtmp.conn)
149 | if err != nil {
150 | rtmp.conn.Close()
151 | return
152 | }
153 |
154 | var data []interface{}
155 | data = append(data, map[string]interface{} {
156 | "app" : app,
157 | "flashVer" : "WIN 29,0,0,113",
158 | "swfUrl" : swf,
159 | "tcUrl" : tc,
160 | "fpad" : false,
161 | "capabilities" : 239,
162 | "audioCodecs" : 0xFFF, //3575,
163 | "videoCodecs" : 0xFF, //252,
164 | "videoFunction" : 1,
165 | "pageUrl" : page,
166 | "objectEncoding": 3,
167 | })
168 |
169 | for _, o := range opt {
170 | data = append(data, o)
171 | }
172 |
173 | _, err = rtmp.Command("connect", data)
174 |
175 | return
176 | }
177 |
178 | const (
179 | NORMAL = iota
180 | COMMAND
181 | PAUSE
182 | TEST
183 | )
184 | func (rtmp *Rtmp) wait(findTrId int, pause bool, testTimeout int) (done, incomplete bool, trData interface{}, err error) {
185 | var mode int
186 | var endUnix int64
187 | var endTime time.Time
188 | if findTrId >= 0 {
189 | mode = COMMAND
190 | } else if pause {
191 | mode = PAUSE
192 | } else if testTimeout > 0 {
193 | mode = TEST
194 | endUnix = time.Now().Unix() + int64(testTimeout)
195 | endTime = time.Unix(endUnix, 0)
196 | }
197 |
198 | if mode != COMMAND {
199 | findTrId = -1
200 | }
201 |
202 | for {
203 | if mode == TEST {
204 | rtmp.conn.SetReadDeadline(endTime)
205 | } else {
206 | rtmp.conn.SetReadDeadline(time.Now().Add(300 * time.Second))
207 | }
208 | __done, __incomplete, trFound, pause, __trData, e := rtmp.recvChunk(findTrId, pause)
209 |
210 | if e != nil {
211 | err = e
212 | return
213 | }
214 | if __done || __incomplete {
215 | done = __done
216 | incomplete = __incomplete
217 | return
218 | }
219 |
220 | switch mode {
221 | case COMMAND:
222 | if trFound {
223 | trData = __trData
224 | return
225 | }
226 | case PAUSE:
227 | if pause {
228 | return
229 | }
230 | case TEST:
231 | if time.Now().Unix() >= endUnix {
232 | return
233 | }
234 | }
235 | }
236 | }
237 |
238 | func (rtmp *Rtmp) WaitPause() (done, incomplete bool, err error) {
239 | done, incomplete, _, err = rtmp.wait(-1, true, -1)
240 | return
241 | }
242 | func (rtmp *Rtmp) WaitTest(testTimeout int) (done, incomplete bool, err error) {
243 | done, incomplete, _, err = rtmp.wait(-1, false, testTimeout)
244 | return
245 | }
246 | func (rtmp *Rtmp) Wait() (done, incomplete bool, err error) {
247 | done, incomplete, _, err = rtmp.wait(-1, false, -1)
248 | return
249 | }
250 | func (rtmp *Rtmp) waitCommand(findTrId int) (done, incomplete bool, trData interface{}, err error) {
251 | done, incomplete, trData, err = rtmp.wait(findTrId, false, -1)
252 | return
253 | }
254 | func (rtmp *Rtmp) SetFlvName(name string) {
255 | rtmp.flvName = name
256 | }
257 | func (rtmp *Rtmp) openFlv(incr bool) (err error) {
258 | if rtmp.flvName == "" {
259 | err = fmt.Errorf("FLV file name not set: call SetFlvName(string)")
260 | return
261 | }
262 | var fileName string
263 | if incr {
264 | if fileName, err = files.GetFileNameNext(rtmp.flvName); err != nil {
265 | return
266 | }
267 | } else {
268 | fileName = rtmp.flvName
269 | }
270 | flv, err := flvs.Open(fileName)
271 | if err != nil {
272 | return
273 | }
274 | rtmp.flv = flv
275 | return
276 | }
277 | func (rtmp *Rtmp) GetTimestamp() int {
278 | return rtmp.timestamp
279 | }
280 | func (rtmp *Rtmp) SetTimestamp(t int) {
281 | rtmp.timestamp = t
282 | }
283 | func (rtmp *Rtmp) writeMetaData(body map[string]interface{}, ts int) (err error) {
284 |
285 | if rtmp.flv == nil {
286 | if err = rtmp.openFlv(false); err != nil {
287 | return
288 | }
289 | }
290 |
291 | //buf := new(bytes.Buffer)
292 | data := []interface{}{}
293 | data = append(data, "onMetaData")
294 | data = append(data, body)
295 |
296 | dat, err := amf.EncodeAmf0(data, true)
297 | //fmt.Printf("writeMetaData %v %#v\n", ts, dat)
298 | rdr := bytes.NewBuffer(dat)
299 | err = rtmp.flv.WriteMetaData(rdr, ts)
300 | return
301 | }
302 | func (rtmp *Rtmp) writeAudio(rdr *bytes.Buffer, ts int) (err error) {
303 | if rtmp.flv == nil {
304 | if err = rtmp.openFlv(false); err != nil {
305 | return
306 | }
307 | }
308 | err = rtmp.flv.WriteAudio(rdr, ts)
309 | return
310 | }
311 | func (rtmp *Rtmp) writeVideo(rdr *bytes.Buffer, ts int) (err error) {
312 | if rtmp.flv == nil {
313 | if err = rtmp.openFlv(false); err != nil {
314 | return
315 | }
316 | } /*else if (!rtmp.flv.VideoExists() && rtmp.flv.AudioExists()) && ts > 1000 {
317 | if err = rtmp.openFlv(true); err != nil {
318 | return
319 | }
320 | }*/
321 | err = rtmp.flv.WriteVideo(rdr, ts)
322 | return
323 | }
324 | func (rtmp *Rtmp) SetFixAggrTimestamp(sw bool) {
325 | rtmp.fixAggrTimestamp = sw
326 | }
327 | func (rtmp *Rtmp) CheckStatus(label string, ts int, data interface{}, waitPause bool) (done, incomplete, pauseFound bool, err error) {
328 | code, ok := objs.FindString(data, "code")
329 | if (! ok) {
330 | err = fmt.Errorf("%s: code Not found", label)
331 | return
332 | }
333 |
334 | switch code {
335 | case "NetStream.Pause.Notify":
336 | if waitPause {
337 | pauseFound = true
338 | }
339 | case "NetStream.Unpause.Notify":
340 | case "NetStream.Play.Stop":
341 | case "NetStream.Play.Complete":
342 | fmt.Printf("NetStream.Play.Complete: last timestamp: %d(flv)\n", rtmp.flv.GetLastTimestamp())
343 | if (ts + 1000) > rtmp.duration {
344 | done = true
345 | } else {
346 | incomplete = true
347 | }
348 | case "NetStream.Play.Start":
349 | case "NetStream.Play.Reset":
350 | case "NetStream.Seek.Notify":
351 | case "NetStream.Play.Failed":
352 | done = true
353 | default:
354 | fmt.Printf("[FIXME] Unknown Code: %s\n", code)
355 | }
356 | return
357 | }
358 | // trId: transaction id to find
359 | func (rtmp *Rtmp) recvChunk(findTrId int, waitPause bool) (done, incomplete, trFound, pauseFound bool, trData interface{}, err error) {
360 | ts, msg_t, res, rdbytes, err := decodeOne(rtmp.conn, rtmp.chunkSizeRecv, rtmp.chunkInfo)
361 | if err != nil {
362 | switch err.(type) {
363 | case *net.OpError:
364 | return
365 | case *DecodeError:
366 | // データを受信したが、パースエラーとなった場合はやり直したい
367 | fmt.Printf("Please retry: RTMP: %v\n", err.Error())
368 | incomplete = true
369 | err = nil
370 | return
371 | }
372 |
373 | return
374 | }
375 | ts = ts + rtmp.startTime
376 |
377 | // byte counter for acknowledgement
378 | rtmp.totalReadBytes += rdbytes
379 | rtmp.readCount += rdbytes
380 | if rtmp.readCount >= (rtmp.windowSize / 2) {
381 | rtmp.readCount = 0
382 | if err = rtmp.acknowledgement(); err != nil {
383 | return
384 | }
385 | }
386 |
387 | // print play timestamp
388 | if true {
389 | if rtmp.duration > 0 {
390 | switch msg_t {
391 | case TID_AUDIO, TID_VIDEO, TID_AGGREGATE:
392 | if ts >= rtmp.nextLogTs {
393 | fmt.Printf("#%8d/%d(%4.1f%%) : %s\n", ts, rtmp.duration, float64(ts)/float64(rtmp.duration)*100, rtmp.flvName)
394 | rtmp.nextLogTs = ts + 10000
395 | }
396 | }
397 | } else {
398 | switch msg_t {
399 | case TID_AUDIO, TID_VIDEO, TID_AGGREGATE:
400 | if ts >= rtmp.nextLogTs {
401 | fmt.Printf("#%8d : %s\n", ts, rtmp.flvName)
402 | rtmp.nextLogTs = ts + 10000
403 | }
404 | }
405 | }
406 | }
407 |
408 | switch msg_t {
409 | case TID_AUDIO:
410 | if ts > rtmp.timestamp {
411 | rtmp.timestamp = ts
412 | }
413 | if err = rtmp.writeAudio(res.(*bytes.Buffer), ts); err != nil {
414 | return
415 | }
416 |
417 | case TID_VIDEO:
418 | if ts > rtmp.timestamp {
419 | rtmp.timestamp = ts
420 | }
421 | if err = rtmp.writeVideo(res.(*bytes.Buffer), ts); err != nil {
422 | return
423 | }
424 |
425 | case TID_AGGREGATE:
426 | if ts > rtmp.timestamp {
427 | rtmp.timestamp = ts
428 | }
429 | var fstTs int
430 | for i, v := range res.([]message) {
431 | var tsAggr int
432 | if rtmp.fixAggrTimestamp {
433 | var delta int
434 | if i == 0 {
435 | fstTs = v.timestamp
436 | }
437 | delta = v.timestamp - fstTs
438 | tsAggr = ts + delta
439 | //fmt.Printf("FixAggrTs: fixed(%d), delta(%d), ts(%d), mts(%d)\n", tsAggr, delta, ts, v.timestamp)
440 | } else {
441 | if i == 0 {
442 | if ts != v.timestamp {
443 | err = fmt.Errorf("aggregate timestamp incorrect: ts:(%v) vs aggr[0].ts(%v)", ts, v.timestamp)
444 | return
445 | }
446 | }
447 | tsAggr = v.timestamp
448 | }
449 |
450 | if /*rtmp.isRecorded &&*/ rtmp.duration > 0 {
451 | switch v.msg_t {
452 | case TID_AUDIO, TID_VIDEO:
453 | // fmt.Printf(" %8d/%d(%4.1f%%) : %2d\n", tsAggr, rtmp.duration, float64(tsAggr)/float64(rtmp.duration)*100, v.msg_t)
454 | }
455 | }
456 |
457 | switch v.msg_t {
458 | case TID_AUDIO:
459 | // audio
460 | if err = rtmp.writeAudio(v.data, tsAggr); err != nil {
461 | return
462 | }
463 |
464 | case TID_VIDEO:
465 | // video
466 | if err = rtmp.writeVideo(v.data, tsAggr); err != nil {
467 | return
468 | }
469 | }
470 | }
471 |
472 | case TID_AMF0DATA, TID_AMF3DATA:
473 | objs.PrintAsJson(res)
474 | list, ok := res.([]interface{})
475 | if (! ok) {
476 | err = fmt.Errorf("result AMF Data is not array")
477 | return
478 | }
479 |
480 | if len(list) >= 2 {
481 | name, ok := list[0].(string)
482 | if (! ok) {
483 | err = fmt.Errorf("result AMF Data[0] is not string")
484 | return
485 | }
486 |
487 | switch name {
488 | case "onPlayStatus":
489 | done, incomplete, pauseFound, err = rtmp.CheckStatus("onPlayStatus", ts, list[1], waitPause)
490 |
491 | case "onMetaData":
492 | dur, ok := objs.FindFloat64(list[1], "duration")
493 | if ok {
494 | rtmp.duration = int(dur * 1000)
495 | } else {
496 | if rtmp.isRecorded {
497 | fmt.Println("[WARN] onMetaData: duration not found")
498 | }
499 | }
500 | if meta, ok := list[1].(map[string]interface{}); ok {
501 | rtmp.writeMetaData(meta, ts)
502 | }
503 |
504 | _, ok = objs.Find(list[1], "videoframerate")
505 | if ok {
506 | rtmp.VideoExists = true
507 | }
508 | }
509 | }
510 |
511 | case TID_AMF0COMMAND, TID_AMF3COMMAND:
512 | objs.PrintAsJson(res)
513 |
514 | list, ok := res.([]interface{})
515 | if (! ok) {
516 | err = fmt.Errorf("result AMF Command is not array")
517 | return
518 | }
519 |
520 | if len(list) >= 3 {
521 | name, ok := list[0].(string)
522 | if (! ok) {
523 | err = fmt.Errorf("result AMF Command name is not string")
524 | return
525 | }
526 | trIdFloat, ok := list[1].(float64)
527 | if (! ok) {
528 | err = fmt.Errorf("result AMF Command transaction id is not number")
529 | return
530 | }
531 | trId := int(trIdFloat)
532 | if (trId > 0) && (trId == findTrId) {
533 | trFound = true
534 | if len(list) >= 4 {
535 | trData = list[3]
536 | }
537 | }
538 |
539 | switch name {
540 | case "_error", "close":
541 | err = fmt.Errorf("AMF command not success: transaction id(%d) -> %s", trId, name)
542 | return
543 | case "onStatus":
544 | done, incomplete, pauseFound, err = rtmp.CheckStatus("onStatus", ts, list[3], waitPause)
545 | }
546 | }
547 |
548 | case TID_SETCHUNKSIZE:
549 | rtmp.chunkSizeRecv = res.(int)
550 |
551 | case TID_WINDOW_ACK_SIZE:
552 | rtmp.windowSize = res.(int)
553 |
554 | case TID_USERCONTROL:
555 | switch res.([]int)[0] {
556 | case UC_PINGREQUEST:
557 | //fmt.Printf("ping request %d\n", res.([]int)[1])
558 | if err = rtmp.pingResponse(res.([]int)[1]); err != nil {
559 | return
560 | }
561 |
562 | case UC_STREAMBEGIN:
563 | rtmp.streamId = res.([]int)[1]
564 |
565 | case UC_STREAMISRECORDED:
566 | fmt.Printf("stream is recorded\n")
567 | rtmp.isRecorded = true
568 |
569 | case UC_BUFFEREMPTY:
570 | if rtmp.isRecorded {
571 | fmt.Printf("required Seek: %d\n", rtmp.timestamp)
572 | // <-- test
573 | rtmp.PauseRaw()
574 | incomplete = true
575 | return
576 | // test -->
577 |
578 | if rtmp.noSeek {
579 | incomplete = true
580 | return
581 | }
582 | ts := rtmp.timestamp - 10000
583 | if ts < 0 {
584 | ts = 0
585 | }
586 | done, incomplete, err = rtmp.PauseUnpause(ts)
587 | if done || incomplete || err != nil {
588 | return
589 | }
590 | //rtmp.Seek(ts)
591 | }
592 | }
593 | default:
594 | //fmt.Printf("got: %8d %d %#v\n", ts, msg_t, res)
595 | }
596 | return
597 | }
598 |
599 | func (rtmp *Rtmp) Close() (err error) {
600 | if rtmp.conn != nil {
601 | err = rtmp.conn.Close()
602 | }
603 | if rtmp.flv != nil {
604 | rtmp.flv.Close()
605 | }
606 | return
607 | }
608 |
609 | func (rtmp *Rtmp) SetPeerBandwidth(wsz, lim int) (err error) {
610 | buff, err := encodeSetPeerBandwidth(wsz, lim)
611 | if err != nil {
612 | return
613 | }
614 | if _, err = buff.WriteTo(rtmp.conn); err != nil {
615 | return
616 | }
617 | return
618 | }
619 |
620 |
621 | func (rtmp *Rtmp) pingResponse(timestamp int) (err error) {
622 | buff, err := encodePingResponse(timestamp)
623 | if _, err = buff.WriteTo(rtmp.conn); err != nil {
624 | return
625 | }
626 | return
627 | }
628 | func (rtmp *Rtmp) acknowledgement() (err error) {
629 | buff, err := encodeAcknowledgement(rtmp.totalReadBytes)
630 | if _, err = buff.WriteTo(rtmp.conn); err != nil {
631 | return
632 | }
633 | return
634 | }
635 | func (rtmp *Rtmp) WindowAckSize(asz int) (err error) {
636 | buff, err := encodeWindowAckSize(asz)
637 | if _, err = buff.WriteTo(rtmp.conn); err != nil {
638 | return
639 | }
640 | return
641 | }
642 | func (rtmp *Rtmp) SetBufferLength(streamId, len int) (err error) {
643 | buff, err := encodeSetBufferLength(streamId, len)
644 | if _, err = buff.WriteTo(rtmp.conn); err != nil {
645 | return
646 | }
647 | return
648 | }
649 |
650 | // command name, transaction ID, and command object
651 | func (rtmp *Rtmp) Command(name string, args []interface{}) (trData interface{}, err error) {
652 | var trId int
653 | var csId int
654 | var streamId int
655 | switch name {
656 | case "connect":
657 | rtmp.transactionId = 1
658 | trId = rtmp.transactionId
659 | csId = 3
660 | streamId = 0
661 |
662 | case "play", "seek", "pause", "pauseRaw":
663 | trId = 0
664 | csId = 8
665 | streamId = 1
666 |
667 | default:
668 | // createStream, call, close, ...
669 | rtmp.transactionId++
670 | trId = rtmp.transactionId
671 | csId = 3
672 | streamId = 0
673 | }
674 | cmd := []interface{}{name, trId}
675 | cmd = append(cmd, args...)
676 | objs.PrintAsJson(cmd)
677 | body, err := amf.EncodeAmf0(cmd, false)
678 | wbuff, err := amf0Command(rtmp.chunkSizeSend, csId, streamId, body)
679 |
680 | if _, err = wbuff.WriteTo(rtmp.conn); err != nil {
681 | return
682 | }
683 |
684 | if trId > 0 {
685 | if _, _, trData, err = rtmp.waitCommand(trId); err != nil {
686 | return
687 | }
688 | }
689 |
690 | return
691 | }
692 |
693 | func (rtmp *Rtmp) Unpause(timestamp int) (err error) {
694 | var data []interface{}
695 | data = append(data, nil)
696 | data = append(data, false)
697 | data = append(data, timestamp)
698 |
699 | _, err = rtmp.Command("pause", data)
700 |
701 | return
702 | }
703 | func (rtmp *Rtmp) Pause(timestamp int) (err error) {
704 | var data []interface{}
705 | data = append(data, nil)
706 | data = append(data, true)
707 | data = append(data, timestamp)
708 |
709 | _, err = rtmp.Command("pause", data)
710 |
711 | return
712 | }
713 | func (rtmp *Rtmp) PauseRaw() (err error) {
714 | _, err = rtmp.Command("pauseRaw", []interface{}{
715 | nil,
716 | true,
717 | 0,
718 | })
719 |
720 | return
721 | }
722 | func (rtmp *Rtmp) PauseUnpause(timestamp int) (done, incomplete bool, err error) {
723 | if err = rtmp.Pause(timestamp); err != nil {
724 | return
725 | }
726 | fmt.Println("paused")
727 | done, incomplete, err = rtmp.WaitPause()
728 | if done || incomplete || err != nil {
729 | return
730 | }
731 | fmt.Println("wait pause")
732 | if err = rtmp.Unpause(timestamp); err != nil {
733 | return
734 | }
735 | fmt.Println("Unpaused")
736 | return
737 | }
738 | func (rtmp *Rtmp) PlayTime(stream string, timestamp int) (err error) {
739 |
740 | rtmp.startTime = timestamp
741 | if rtmp.startTime < 0 {
742 | rtmp.startTime = 0
743 | }
744 | //fmt.Printf("debug rtmp.startTime: %d\n", rtmp.startTime)
745 |
746 | var data []interface{}
747 | data = append(data, nil)
748 | data = append(data, stream)
749 |
750 | data = append(data, timestamp) // Start
751 | // NicoOfficialTs, Never append Duration and flush
752 | if rtmp.flush {
753 | data = append(data, -1) // Duration
754 | data = append(data, true) // flush
755 | }
756 |
757 | _, err = rtmp.Command("play", data)
758 |
759 | return
760 | }
761 | func (rtmp *Rtmp) Play(stream string) error {
762 | return rtmp.PlayTime(stream, -5000)
763 | }
764 | func (rtmp *Rtmp) Seek(timestamp int) (err error) {
765 | //fmt.Printf("debug Seek to %d\n", timestamp)
766 | var data []interface{}
767 | data = append(data, nil)
768 | data = append(data, timestamp)
769 |
770 | _, err = rtmp.Command("seek", data)
771 |
772 | //fmt.Printf("debug Seek done\n")
773 | return
774 | }
775 | func (rtmp *Rtmp) CreateStream() (err error) {
776 | var data []interface{}
777 | data = append(data, nil)
778 |
779 | _, err = rtmp.Command("createStream", data)
780 |
781 | return
782 | }
783 |
784 | func handshake(conn *net.TCPConn) (err error) {
785 |
786 | wbuff := bytes.NewBuffer(nil)
787 |
788 | // C0
789 | wbuff.WriteByte(3)
790 | // C1
791 | rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
792 | io.CopyN(wbuff, rnd, 1536)
793 |
794 | // Send C0+C1
795 | if _, err = wbuff.WriteTo(conn); err != nil {
796 | return
797 | }
798 |
799 | // Recv S0
800 | if _, err = io.CopyN(ioutil.Discard, conn, 1); err != nil {
801 | return
802 | }
803 |
804 | // Recv S1
805 | if _, err = io.CopyN(wbuff, conn, 1536); err != nil {
806 | return
807 | }
808 |
809 | // Send C2(=S1)
810 | if _, err = wbuff.WriteTo(conn); err != nil {
811 | return
812 | }
813 | // Recv S2
814 | if _, err = io.CopyN(ioutil.Discard, conn, 1536); err != nil {
815 | return
816 | }
817 | return
818 | }
819 |
820 |
821 |
--------------------------------------------------------------------------------
/src/twitcas/twicas.go:
--------------------------------------------------------------------------------
1 | package twitcas
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "net/url"
10 | "os"
11 | "time"
12 | "database/sql"
13 | _ "github.com/mattn/go-sqlite3"
14 | "github.com/gorilla/websocket"
15 | "../files"
16 | "../httpbase"
17 | "../procs/ffmpeg"
18 | "os/exec"
19 | "io"
20 | )
21 |
22 | type Twitcas struct {
23 | Conn *websocket.Conn
24 | }
25 |
26 | func connectStream(proto, host, mode string, id uint64, proxy string) (conn *websocket.Conn, err error) {
27 | streamUrl := fmt.Sprintf(
28 | //case A.InnerFrame:return"i";
29 | //case A.Pframe:return"p";
30 | //case A.DisposableProfile:return"bd";
31 | //case A.Bframe:return"b";
32 | //case A.Any:return"any";
33 | //case A.KeyFrame:default:return"k"}
34 | //"%s://%s/ws.app/stream/%d/fmp4/k/0/1?mode=%s",
35 | "%s://%s/ws.app/stream/%d/fmp4/bd/1/1500?mode=%s",
36 | proto, host, id, mode,
37 | )
38 | // fmt.Println(streamUrl)
39 |
40 | var origin string
41 | if proto == "wss" {
42 | origin = fmt.Sprintf("https://%s", host)
43 | } else {
44 | origin = fmt.Sprintf("http://%s", host)
45 | }
46 |
47 | header := http.Header{}
48 | header.Set("Origin", origin)
49 | //header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36")
50 | header.Set("User-Agent", httpbase.GetUserAgent())
51 |
52 | timeout, _ := time.ParseDuration("10s")
53 | dialer := websocket.Dialer{
54 | HandshakeTimeout: timeout,
55 | }
56 | if proxy != "" {
57 | dialer.Proxy = func(req *http.Request) (u *url.URL, err error) {
58 | var proxyUrl string
59 | if proto == "wss" {
60 | proxyUrl = fmt.Sprintf("https://%s", proxy)
61 | } else {
62 | proxyUrl = fmt.Sprintf("http://%s", proxy)
63 | }
64 | return url.ParseRequestURI(proxyUrl)
65 | }
66 | }
67 |
68 | conn, _, err = dialer.Dial(streamUrl, header)
69 |
70 | return
71 | }
72 |
73 | func getStream(user, proxy string) (conn *websocket.Conn, movieId uint64, err error) {
74 | url := fmt.Sprintf(
75 | "https://twitcasting.tv/streamserver.php?target=%s&mode=client",
76 | user,
77 | )
78 |
79 | type StreamServer struct {
80 | Movie struct {
81 | Id uint64 `json:"id"`
82 | Live bool `json:"live"`
83 | } `json:"movie"`
84 | Fmp4 struct {
85 | Host string `json:"host"`
86 | Proto string `json:"proto"`
87 | Source bool `json:"source"`
88 | MobileSource bool `json:"mobilesource"`
89 | } `json:"fmp4"`
90 | }
91 |
92 | req, err := http.NewRequest("GET", url, nil)
93 | if err != nil {
94 | return
95 | }
96 |
97 | client := new(http.Client)
98 | client.Timeout, _ = time.ParseDuration("10s")
99 |
100 | resp, err := client.Do(req)
101 | if err != nil {
102 | return
103 | }
104 |
105 | defer resp.Body.Close()
106 | respBytes, err := ioutil.ReadAll(resp.Body)
107 | if err != nil {
108 | return
109 | }
110 | //fmt.Printf("debug %s\n", string(respBytes))
111 |
112 | data := new(StreamServer)
113 |
114 | err = json.Unmarshal(respBytes, data)
115 | if err != nil {
116 | return
117 | }
118 |
119 | if !data.Movie.Live {
120 | // movie not active
121 | err = errors.New(user + " --> " + "Offline or User Not Found")
122 | return
123 | } else {
124 | var mode string
125 | if data.Fmp4.Source {
126 | // StreamQuality.High
127 | mode = "main"
128 | } else if data.Fmp4.MobileSource {
129 | // StreamQuality.Middle
130 | mode = "mobilesource"
131 | } else {
132 | // StreamQuality.Low
133 | mode = "base"
134 | }
135 |
136 | if data.Fmp4.Proto != "" && data.Fmp4.Host != "" && data.Movie.Id != 0 {
137 | conn, err = connectStream(data.Fmp4.Proto, data.Fmp4.Host, mode, data.Movie.Id, proxy)
138 | if err != nil {
139 | return
140 | }
141 | movieId = data.Movie.Id
142 | } else {
143 | err = errors.New(user + " --> " + "No Stream Defined")
144 | return
145 | }
146 | }
147 |
148 | return
149 | }
150 |
151 | func createFileUser(user string, movieId uint64) (f *os.File, filename string, err error) {
152 | user = files.ReplaceForbidden(user)
153 | filename = fmt.Sprintf("%s_%d.mp4", user, movieId)
154 | for i := 2; i < 1000; i++ {
155 | _, err := os.Stat(filename)
156 | if err != nil {
157 | break
158 | }
159 | filename = fmt.Sprintf("%s_%d_%d.mp4", user, movieId, i)
160 | }
161 | f, err = os.Create(filename)
162 | return
163 | }
164 |
165 | // FIXME: return codeの整理
166 | func TwitcasRecord(user, proxy string) (done, dbLocked bool) {
167 | conn, movieId, err := getStream(user, proxy)
168 | if err != nil {
169 | fmt.Printf("@err getStream: %v\n", err)
170 | return
171 | }
172 | if conn == nil {
173 | fmt.Println("[FIXME] conn is nil")
174 | return
175 | }
176 | defer conn.Close()
177 |
178 | dbName := fmt.Sprintf("tmp/tcas-%v-lock.db", movieId)
179 | files.MkdirByFileName(dbName)
180 | db, err := sql.Open("sqlite3", dbName)
181 | if err != nil {
182 | fmt.Println(err)
183 | return
184 | }
185 | defer db.Close()
186 |
187 | _, err = db.Exec(`BEGIN EXCLUSIVE`)
188 | if err != nil {
189 | dbLocked = true
190 | return
191 | }
192 | defer os.Remove(dbName)
193 |
194 | //func Open(opt... string) (cmd *exec.Cmd, stdin io.WriteCloser, err error) {
195 | var cmd *exec.Cmd
196 | var stdin io.WriteCloser
197 |
198 | var fileOpened bool
199 |
200 | filenameBase := fmt.Sprintf("%s_%d.ts", user, movieId)
201 | filenameBase = files.ReplaceForbidden(filenameBase) // fixed #8
202 |
203 | closeFF := func() {
204 | if stdin != nil {
205 | stdin.Close()
206 | }
207 | if cmd != nil {
208 | cmd.Wait()
209 | }
210 | stdin = nil
211 | cmd = nil
212 | }
213 |
214 | openFF := func() (err error) {
215 | closeFF()
216 |
217 | filename, err := files.GetFileNameNext(filenameBase)
218 | if err != nil {
219 | fmt.Println(err)
220 | return
221 | }
222 |
223 | c, in, err := ffmpeg.Open("-i", "-", "-c", "copy", "-y", filename)
224 | if err != nil {
225 | return
226 | }
227 | cmd = c
228 | stdin = in
229 |
230 | fileOpened = true
231 | return
232 | }
233 |
234 | for {
235 | conn.SetReadDeadline(time.Now().Add(60 * time.Second))
236 | messageType, data, err := conn.ReadMessage()
237 | if err != nil {
238 | fmt.Printf("@err ReadMessage: %v\n\n", err)
239 | return
240 | }
241 |
242 | if messageType == 2 {
243 | if cmd == nil || stdin == nil {
244 | if err = openFF(); err != nil {
245 | fmt.Println(err)
246 | return
247 | }
248 | defer closeFF()
249 | }
250 |
251 | if _, err := stdin.Write(data); err != nil {
252 | fmt.Println(err)
253 | return
254 | }
255 |
256 | } else if messageType == 1 {
257 |
258 | type TextMessage struct {
259 | Code int `json:"code"`
260 | }
261 | msg := new(TextMessage)
262 | err = json.Unmarshal(data, msg)
263 | if err != nil {
264 | // json decode error
265 | fmt.Printf("@err %v\n", err)
266 | return
267 | }
268 | if (msg.Code == 100) || (msg.Code == 101) || (msg.Code == 110) {
269 | // ignore
270 | } else if msg.Code == 400 { // invalid_parameter
271 | return
272 | } else if msg.Code == 401 { // passcode_required
273 | return
274 | } else if msg.Code == 403 { //access_forbidden
275 | return
276 | } else if msg.Code == 500 { // offline
277 | return
278 | } else if msg.Code == 503 { // server_error
279 | return
280 | } else if msg.Code == 504 { // live_ended
281 | break
282 | } else {
283 | fmt.Printf("@FIXME %v\n\n", string(data))
284 | return
285 | }
286 | }
287 | }
288 |
289 | closeFF()
290 |
291 | done = fileOpened
292 | return
293 | }
294 |
--------------------------------------------------------------------------------
/src/youtube/comment.go:
--------------------------------------------------------------------------------
1 | package youtube
2 |
3 | import (
4 | "fmt"
5 | "context"
6 | "time"
7 | "sync"
8 | "database/sql"
9 | _ "github.com/mattn/go-sqlite3"
10 | "strconv"
11 | "encoding/json"
12 | "log"
13 | "path/filepath"
14 | "os"
15 | "html"
16 | "io/ioutil"
17 | "../gorman"
18 | "../files"
19 | "../httpbase"
20 | "../objs"
21 | )
22 |
23 | type OBJ = map[string]interface{}
24 |
25 | func getComment(gm *gorman.GoroutineManager, ctx context.Context, sig <-chan struct{}, isReplay bool, continuation, name string) (done bool) {
26 |
27 | dbName := files.ChangeExtention(name, "yt.sqlite3")
28 | db, err := dbOpen(ctx, dbName)
29 | if err != nil {
30 | fmt.Println(err)
31 | return
32 | }
33 | defer db.Close()
34 |
35 | mtx := &sync.Mutex{}
36 |
37 | testContinuation, count, _ := dbGetContinuation(ctx, db, mtx)
38 | if testContinuation != "" {
39 | continuation = testContinuation
40 | }
41 |
42 | var printTime int64
43 |
44 | MAINLOOP: for {
45 | select {
46 | case <-ctx.Done(): break MAINLOOP
47 | case <-sig: break MAINLOOP
48 | default:
49 | }
50 | timeoutMs, _done, err, neterr := func() (timeoutMs int, _done bool, err, neterr error) {
51 | var uri string
52 | if isReplay {
53 | uri = fmt.Sprintf("https://www.youtube.com/youtubei/v1/live_chat/get_live_chat_replay?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8")
54 | } else {
55 | uri = fmt.Sprintf("https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8")
56 | }
57 |
58 | postData := OBJ{
59 | "context": OBJ{
60 | "client": OBJ{
61 | "clientName": "WEB",
62 | "clientVersion": "2.20210128.02.00",
63 | },
64 | },
65 | "continuation": continuation,
66 | }
67 | resp, err, neterr := httpbase.PostJson(uri, map[string]string {
68 | "Cookie": Cookie,
69 | "User-Agent": UserAgent,
70 | }, postData)
71 | if err != nil {
72 | return
73 | }
74 | if neterr != nil {
75 | return
76 | }
77 | buff, neterr := ioutil.ReadAll(resp.Body)
78 | resp.Body.Close()
79 | if neterr != nil {
80 | return
81 | }
82 | code := resp.StatusCode
83 | if code != 200 {
84 | if code == 404 {
85 | fmt.Printf("Status code: %v (ignored)\n", code)
86 | time.Sleep(1000 * time.Millisecond)
87 | return
88 | } else {
89 | neterr = fmt.Errorf("Status code: %v\n", code)
90 | return
91 | }
92 | }
93 |
94 | var data interface{}
95 | err = json.Unmarshal(buff, &data)
96 | if err != nil {
97 | err = fmt.Errorf("json decode error")
98 | return
99 | }
100 |
101 | liveChatContinuation, ok := objs.Find(data, "continuationContents", "liveChatContinuation")
102 | if (! ok) {
103 | err = fmt.Errorf("(response liveChatContinuation) not found")
104 | return
105 | }
106 |
107 | if actions, ok := objs.FindArray(liveChatContinuation, "actions"); ok {
108 | var videoOffsetTimeMsec string
109 |
110 | for _, a := range actions {
111 | var item interface{}
112 | var ok bool
113 | item, ok = objs.Find(a, "addChatItemAction", "item")
114 | if (! ok) {
115 | item, ok = objs.Find(a, "addLiveChatTickerItemAction", "item")
116 | if (! ok) {
117 | item, ok = objs.Find(a, "replayChatItemAction", "actions", "addChatItemAction", "item")
118 | if ok {
119 | videoOffsetTimeMsec, _ = objs.FindString(a, "replayChatItemAction", "videoOffsetTimeMsec")
120 | }
121 | }
122 | }
123 | if (! ok) {
124 | //objs.PrintAsJson(a)
125 | //fmt.Println("(actions item) not found")
126 | continue
127 | }
128 |
129 | var liveChatMessageRenderer interface{}
130 | liveChatMessageRenderer, ok = objs.Find(item, "liveChatTextMessageRenderer")
131 | if (! ok) {
132 | liveChatMessageRenderer, ok = objs.Find(item, "liveChatPaidMessageRenderer")
133 | }
134 | if (! ok) {
135 | continue
136 | }
137 |
138 | authorExternalChannelId, _ := objs.FindString(liveChatMessageRenderer, "authorExternalChannelId")
139 | authorName, _ := objs.FindString(liveChatMessageRenderer, "authorName", "simpleText")
140 | id, ok := objs.FindString(liveChatMessageRenderer, "id")
141 | if (! ok) {
142 | continue
143 | }
144 | var message string
145 | if runs, ok := objs.FindArray(liveChatMessageRenderer, "message", "runs"); ok {
146 | for _, run := range runs {
147 | if text, ok := objs.FindString(run, "text"); ok {
148 | message += text
149 | } else if emojis, ok := objs.FindArray(run, "emoji", "shortcuts"); ok {
150 | if emoji, ok := emojis[0].(string); ok {
151 | message += emoji
152 | }
153 | }
154 | }
155 | }
156 | var others string
157 | var amount string
158 | amount, _ = objs.FindString(liveChatMessageRenderer, "purchaseAmountText", "simpleText")
159 | if amount != "" {
160 | others += ` amount="` + html.EscapeString(amount) + `"`
161 | }
162 | timestampUsec, ok := objs.FindString(liveChatMessageRenderer, "timestampUsec")
163 | if (! ok) {
164 | continue
165 | }
166 |
167 |
168 | if false {
169 | fmt.Printf("%v ", videoOffsetTimeMsec)
170 | fmt.Printf("%v %v %v %v %v %v [%v ]\n", timestampUsec, count, authorName, authorExternalChannelId, message, id, others)
171 | }
172 |
173 | dbInsert(ctx, gm, db, mtx,
174 | id,
175 | timestampUsec,
176 | videoOffsetTimeMsec,
177 | authorName,
178 | authorExternalChannelId,
179 | message,
180 | continuation,
181 | others,
182 | count,
183 | )
184 | count++
185 | }
186 |
187 | // アーカイブ時、20秒毎に進捗を表示
188 | if videoOffsetTimeMsec != "" {
189 | now := time.Now().Unix()
190 | if now - printTime > 20 {
191 | printTime = now
192 | if msec, e := strconv.ParseInt(videoOffsetTimeMsec, 10, 64); e == nil {
193 | total := msec / 1000
194 | hour := total / 3600
195 | min := (total % 3600) / 60
196 | sec := (total % 3600) % 60
197 | fmt.Printf("comment pos: %02d:%02d:%02d\n", hour, min, sec)
198 | }
199 | }
200 | }
201 |
202 | //fmt.Println("------------")
203 | }
204 |
205 | if continuations, ok := objs.Find(liveChatContinuation, "continuations"); ok {
206 | //objs.PrintAsJson(continuations)
207 |
208 | if c, ok := objs.FindString(continuations, "timedContinuationData", "continuation"); ok {
209 | continuation = c
210 | } else if c, ok := objs.FindString(continuations, "liveChatReplayContinuationData", "continuation"); ok {
211 | continuation = c
212 | } else if c, ok := objs.FindString(continuations, "invalidationContinuationData", "continuation"); ok {
213 | continuation = c
214 | } else if c, ok := objs.FindString(continuations, "playerSeekContinuationData", "continuation"); ok {
215 | if isReplay {
216 | _done = true
217 | return
218 | }
219 | continuation = c
220 | } else {
221 | objs.PrintAsJson(continuations)
222 | err = fmt.Errorf("(liveChatContinuation continuation) not found")
223 | return
224 | }
225 |
226 | if t, ok := objs.FindString(continuations, "timedContinuationData", "timeoutMs"); ok {
227 | timeout, err := strconv.Atoi(t)
228 | if err != nil {
229 | timeoutMs = timeout
230 | }
231 | } else if t, ok := objs.FindString(continuations, "invalidationContinuationData", "continuation"); ok {
232 | timeout, err := strconv.Atoi(t)
233 | if err != nil {
234 | timeoutMs = timeout
235 | }
236 | }
237 |
238 | } else {
239 | objs.PrintAsJson(liveChatContinuation)
240 | err = fmt.Errorf("(liveChatContinuation>continuations) not found")
241 | return
242 | }
243 |
244 | return
245 | }()
246 | if err != nil {
247 | fmt.Println(err)
248 | break
249 | }
250 | if neterr != nil {
251 | fmt.Println(neterr)
252 | break
253 | }
254 | if _done {
255 | done = true
256 | break MAINLOOP
257 | }
258 |
259 | if timeoutMs < 1000 {
260 | if isReplay {
261 | timeoutMs = 1000
262 | } else {
263 | timeoutMs = 6000
264 | }
265 | }
266 | time.Sleep(time.Duration(timeoutMs) * time.Millisecond)
267 | }
268 | return
269 | }
270 |
271 | func dbOpen(ctx context.Context, name string) (db *sql.DB, err error) {
272 | db, err = sql.Open("sqlite3", name)
273 | if err != nil {
274 | return
275 | }
276 |
277 | _, err = db.ExecContext(ctx, `
278 | PRAGMA synchronous = OFF;
279 | PRAGMA journal_mode = WAL;
280 | `)
281 | if err != nil {
282 | db.Close()
283 | return
284 | }
285 |
286 | err = dbCreate(ctx, db)
287 | if err != nil {
288 | db.Close()
289 | }
290 | return
291 | }
292 |
293 | func dbCreate(ctx context.Context, db *sql.DB) (err error) {
294 | // table media
295 |
296 | _, err = db.ExecContext(ctx, `
297 | CREATE TABLE IF NOT EXISTS comment (
298 | id TEXT PRIMARY KEY NOT NULL UNIQUE,
299 | timestampUsec INTEGER NOT NULL,
300 | videoOffsetTimeMsec INTEGER,
301 | authorName TEXT,
302 | channelId TEXT,
303 | message TEXT,
304 | continuation TEXT,
305 | others TEXT,
306 | count INTEGER NOT NULL
307 | )
308 | `)
309 | if err != nil {
310 | return
311 | }
312 |
313 | _, err = db.ExecContext(ctx, `
314 | CREATE UNIQUE INDEX IF NOT EXISTS comment0 ON comment(id);
315 | CREATE INDEX IF NOT EXISTS comment1 ON comment(timestampUsec);
316 | CREATE INDEX IF NOT EXISTS comment2 ON comment(videoOffsetTimeMsec);
317 | CREATE INDEX IF NOT EXISTS comment3 ON comment(count);
318 | `)
319 | if err != nil {
320 | return
321 | }
322 |
323 | return
324 | }
325 |
326 | func dbInsert(ctx context.Context, gm *gorman.GoroutineManager, db *sql.DB, mtx *sync.Mutex,
327 | id, timestampUsec, videoOffsetTimeMsec, authorName, authorExternalChannelId, message, continuation, others string, count int) {
328 |
329 | usec, err := strconv.ParseInt(timestampUsec, 10, 64)
330 | if err != nil {
331 | fmt.Printf("ParseInt error: %s\n", timestampUsec)
332 | return
333 | }
334 | var offset interface{}
335 | if videoOffsetTimeMsec == "" {
336 | offset = nil
337 | } else {
338 | n, err := strconv.ParseInt(videoOffsetTimeMsec, 10, 64)
339 | if err != nil {
340 | offset = nil
341 | } else {
342 | offset = n
343 | }
344 | }
345 |
346 | query := `INSERT OR IGNORE INTO comment
347 | (id, timestampUsec, videoOffsetTimeMsec, authorName, channelId, message, continuation, others, count) VALUES (?,?,?,?,?,?,?,?,?)`
348 |
349 | gm.Go(func(<-chan struct{}) int {
350 | mtx.Lock()
351 | defer mtx.Unlock()
352 |
353 | if _, err := db.ExecContext(ctx, query,
354 | id, usec, offset, authorName, authorExternalChannelId, message, continuation, others, count,
355 | ); err != nil {
356 | if err.Error() != "context canceled" {
357 | fmt.Println(err)
358 | }
359 | return 1
360 | }
361 | return 0
362 | })
363 |
364 | return
365 | }
366 |
367 | func dbGetContinuation(ctx context.Context, db *sql.DB, mtx *sync.Mutex) (res string, cnt int, err error) {
368 | mtx.Lock()
369 | defer mtx.Unlock()
370 |
371 | err = db.QueryRowContext(ctx, "SELECT continuation, count FROM comment ORDER BY count DESC LIMIT 1").Scan(&res, &cnt)
372 | return
373 | }
374 |
375 | var SelComment = `SELECT
376 | timestampUsec,
377 | IFNULL(videoOffsetTimeMsec, -1),
378 | authorName,
379 | channelId,
380 | message,
381 | others,
382 | count
383 | FROM comment
384 | ORDER BY timestampUsec
385 | `
386 |
387 | func WriteComment(db *sql.DB, fileName string) {
388 |
389 | rows, err := db.Query(SelComment)
390 | if err != nil {
391 | log.Println(err)
392 | return
393 | }
394 | defer rows.Close()
395 |
396 | fileName = files.ChangeExtention(fileName, "xml")
397 |
398 | dir := filepath.Dir(fileName)
399 | base := filepath.Base(fileName)
400 | base, err = files.GetFileNameNext(base)
401 | if err != nil {
402 | fmt.Println(err)
403 | os.Exit(1)
404 | }
405 | fileName = filepath.Join(dir, base)
406 | f, err := os.Create(fileName)
407 | if err != nil {
408 | log.Fatalln(err)
409 | }
410 | defer f.Close()
411 | fmt.Fprintf(f, "%s\r\n", ``)
412 | fmt.Fprintf(f, "%s\r\n", ``)
413 |
414 | firstOffsetUsec := int64(-1)
415 |
416 | for rows.Next() {
417 | var timestampUsec int64
418 | var videoOffsetTimeMsec int64
419 | var authorName string
420 | var channelId string
421 | var message string
422 | var others string
423 | var count int64
424 |
425 | err = rows.Scan(
426 | ×tampUsec,
427 | &videoOffsetTimeMsec,
428 | &authorName,
429 | &channelId,
430 | &message,
431 | &others,
432 | &count,
433 | )
434 | if err != nil {
435 | log.Println(err)
436 | return
437 | }
438 |
439 | var vpos int64
440 | if videoOffsetTimeMsec >= 0 {
441 | vpos = videoOffsetTimeMsec / 10
442 | } else {
443 | if firstOffsetUsec < 0 {
444 | firstOffsetUsec = timestampUsec
445 | }
446 | diff := timestampUsec - firstOffsetUsec
447 | vpos = diff / (10 * 1000)
448 | }
449 |
450 | line := fmt.Sprintf(
451 | `"
462 | message = html.EscapeString(message)
463 | line += message
464 | line += ""
465 | fmt.Fprintf(f, "%s\r\n", line)
466 | }
467 | fmt.Fprintf(f, "%s\r\n", ``)
468 | }
469 |
--------------------------------------------------------------------------------
/src/youtube/youtube.go:
--------------------------------------------------------------------------------
1 | package youtube
2 |
3 | import (
4 | "fmt"
5 | // "net/http"
6 | // "io/ioutil"
7 | "bufio"
8 | "regexp"
9 | "encoding/json"
10 | "html"
11 | "context"
12 | "os"
13 | "os/signal"
14 | "syscall"
15 | "../objs"
16 | "../files"
17 | "../procs/streamlink"
18 | "../procs/youtube_dl"
19 | "../gorman"
20 | "../httpbase"
21 |
22 | "strings"
23 | "time"
24 | "sync"
25 | "../procs"
26 | )
27 |
28 | var Cookie = "PREF=f1=50000000&f4=4000000&hl=en"
29 | var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
30 |
31 | var split = func(data []byte, atEOF bool) (advance int, token []byte, err error) {
32 | for i := 0; i < len(data) ; i++ {
33 | if data[i] == '\n' {
34 | return i + 1, data[:i + 1], nil
35 | }
36 | if data[i] == '\r' {
37 | if (i + 1) == len(data) {
38 | return 0, nil, nil
39 | }
40 | if data[i + 1] == '\n' {
41 | return i + 2, data[:i + 2], nil
42 | }
43 | return i + 1, data[:i + 1], nil
44 | }
45 | }
46 |
47 | if atEOF && len(data) > 0 {
48 | return len(data), data, nil
49 | }
50 |
51 | return 0, nil, nil
52 | }
53 |
54 | func getChatContinuation(buff []byte) (isReplay bool, continuation string, err error) {
55 |
56 | if ma := regexp.MustCompile(`(?s)\WytInitialData\p{Zs}*=\p{Zs}*({.*?})\p{Zs}*;`).FindSubmatch(buff); len(ma) > 1 {
57 | var data interface{}
58 | err = json.Unmarshal(ma[1], &data)
59 | if err != nil {
60 | err = fmt.Errorf("ytInitialData parse error")
61 | return
62 | }
63 |
64 | //objs.PrintAsJson(data);
65 |
66 | liveChatRenderer, ok := objs.Find(data,
67 | "contents",
68 | "twoColumnWatchNextResults",
69 | "conversationBar",
70 | "liveChatRenderer",
71 | )
72 | if (! ok) {
73 | err = fmt.Errorf("liveChatRenderer not found")
74 | return
75 | }
76 | isReplay, _ = objs.FindBool(liveChatRenderer, "isReplay")
77 |
78 | subMenuItems, ok := objs.FindArray(liveChatRenderer,
79 | "header",
80 | "liveChatHeaderRenderer",
81 | "viewSelector",
82 | "sortFilterSubMenuRenderer",
83 | "subMenuItems",
84 | )
85 | if (! ok) {
86 | err = fmt.Errorf("subMenuItems not found")
87 | return
88 | }
89 |
90 | for _, item := range subMenuItems {
91 | title, _ := objs.FindString(item, "title")
92 | //selected, _ := objs.FindBool(item, "selected")
93 | c, _ := objs.FindString(item, "continuation", "reloadContinuationData", "continuation")
94 |
95 | if (title != "") && (! strings.Contains(title, "Top")) {
96 | continuation = c
97 | return
98 | }
99 | continuation = c
100 | }
101 |
102 | } else {
103 | err = fmt.Errorf("ytInitialData not found")
104 | return
105 | }
106 |
107 | if continuation == "" {
108 | err = fmt.Errorf("continuation not found")
109 | }
110 | return
111 | }
112 |
113 | func getInfo(buff []byte) (title, ucid, author string, err error) {
114 | var data interface{}
115 | re := regexp.MustCompile(`(?s)\WytInitialPlayerResponse\p{Zs}*=\p{Zs}*({.*?})\p{Zs}*;`)
116 | if ma := re.FindSubmatch(buff); len(ma) > 1 {
117 | str := html.UnescapeString(string(ma[1]))
118 | if err = json.Unmarshal([]byte(str), &data); err != nil {
119 | err = fmt.Errorf("ytInitialPlayerResponse parse error")
120 | return
121 | }
122 | } else {
123 | err = fmt.Errorf("ytInitialPlayerResponse not found")
124 | return
125 | }
126 |
127 | //objs.PrintAsJson(data); return
128 |
129 | title, ok := objs.FindString(data, "videoDetails", "title")
130 | if (! ok) {
131 | err = fmt.Errorf("title not found")
132 | return
133 | }
134 | ucid, _ = objs.FindString(data, "videoDetails", "channelId")
135 | author, _ = objs.FindString(data, "videoDetails", "author")
136 | return
137 | }
138 |
139 | func execStreamlink(gm *gorman.GoroutineManager, uri, name string) (notSupport bool, err error) {
140 | cmd, stdout, stderr, err := streamlink.Open(uri, "best", "--retry-max", "10", "-o", name)
141 | if err != nil {
142 | return
143 | }
144 | defer stdout.Close()
145 | defer stderr.Close()
146 |
147 | chStdout := make(chan string, 10)
148 | chStderr := make(chan string, 10)
149 | chEof := make(chan struct{}, 2)
150 |
151 | // stdout
152 | gm.Go(func(c <-chan struct{}) int {
153 | defer func(){
154 | chEof <- struct{}{}
155 | }()
156 | scanner := bufio.NewScanner(stdout)
157 | scanner.Split(split)
158 |
159 | for scanner.Scan() {
160 | chStdout <- scanner.Text()
161 | }
162 |
163 | return 0
164 | })
165 |
166 | // stderr
167 | gm.Go(func(c <-chan struct{}) int {
168 | defer func(){
169 | chEof <- struct{}{}
170 | }()
171 | scanner := bufio.NewScanner(stderr)
172 | scanner.Split(split)
173 |
174 | for scanner.Scan() {
175 | chStderr <- scanner.Text()
176 | }
177 |
178 | return 0
179 | })
180 |
181 |
182 | // outputs
183 | gm.Go(func(c <-chan struct{}) int {
184 | for {
185 | var s string
186 | select {
187 | case s = <-chStdout:
188 | case s = <-chStderr:
189 | case <-chEof:
190 | return 0
191 | }
192 |
193 | if strings.HasPrefix(s, "[cli][error]") {
194 | fmt.Print(s)
195 |
196 | notSupport = true
197 | procs.Kill(cmd.Process.Pid)
198 | break
199 | } else if strings.HasPrefix(s, "Traceback (most recent call last):") {
200 | fmt.Print(s)
201 |
202 | notSupport = true
203 | //procs.Kill(cmd.Process.Pid)
204 | //break
205 | } else {
206 | fmt.Print(s)
207 | }
208 | }
209 | return 0
210 | })
211 |
212 | cmd.Wait()
213 |
214 | return
215 | }
216 |
217 | func execYoutube_dl(gm *gorman.GoroutineManager, uri, name string) (err error) {
218 | defer func() {
219 | part := name + ".part"
220 | if _, test := os.Stat(part); test == nil {
221 | if _, test := os.Stat(name); test != nil {
222 | os.Rename(part, name)
223 | }
224 | }
225 | }()
226 |
227 | cmd, stdout, stderr, err := youtube_dl.Open("--no-mtime", "--no-color", "-o", name, uri)
228 | if err != nil {
229 | return
230 | }
231 | defer stdout.Close()
232 | defer stderr.Close()
233 |
234 | chStdout := make(chan string, 10)
235 | chStderr := make(chan string, 10)
236 | chEof := make(chan struct{}, 2)
237 |
238 | // stdout
239 | gm.Go(func(c <-chan struct{}) int {
240 | defer func(){
241 | chEof <- struct{}{}
242 | }()
243 | scanner := bufio.NewScanner(stdout)
244 | scanner.Split(split)
245 |
246 | for scanner.Scan() {
247 | chStdout <- scanner.Text()
248 | }
249 |
250 | return 0
251 | })
252 |
253 | // stderr
254 | gm.Go(func(c <-chan struct{}) int {
255 | defer func(){
256 | chEof <- struct{}{}
257 | }()
258 | scanner := bufio.NewScanner(stderr)
259 | scanner.Split(split)
260 |
261 | for scanner.Scan() {
262 | chStderr <- scanner.Text()
263 | }
264 |
265 | return 0
266 | })
267 |
268 | // outputs
269 | gm.Go(func(c <-chan struct{}) int {
270 | var old int64
271 | for {
272 | var s string
273 | select {
274 | case s = <-chStdout:
275 | case s = <-chStderr:
276 | case <-chEof:
277 | return 0
278 | }
279 |
280 | if strings.HasPrefix(s, "[https @ ") {
281 | // ffmpeg unwanted logs
282 | } else {
283 | if strings.HasPrefix(s, "[download]") {
284 | var now = time.Now().UnixNano()
285 | if now - old > 2 * 1000 * 1000 * 1000 {
286 | old = now
287 | } else {
288 | continue
289 | }
290 | }
291 | fmt.Print(s)
292 | }
293 | }
294 | return 0
295 | })
296 |
297 | cmd.Wait()
298 | return
299 | }
300 |
301 | var COMMENT_DONE = 1000
302 |
303 | func Record(id string, ytNoStreamlink, ytNoYoutube_dl bool) (err error) {
304 |
305 | uri := fmt.Sprintf("https://www.youtube.com/watch?v=%s", id)
306 | code, buff, err, neterr := httpbase.GetBytes(uri, map[string]string {
307 | "Cookie": Cookie,
308 | "User-Agent": UserAgent,
309 | })
310 | if err != nil {
311 | return
312 | }
313 | if neterr != nil {
314 | return
315 | }
316 | if code != 200 {
317 | neterr = fmt.Errorf("Status code: %v\n", code)
318 | return
319 | }
320 |
321 | title, ucid, author, err := getInfo(buff)
322 | if err != nil {
323 | return
324 | }
325 |
326 | if false {
327 | fmt.Println(ucid)
328 | }
329 |
330 | isReplay, continuation, err := getChatContinuation(buff)
331 |
332 |
333 | origName := fmt.Sprintf("%s-%s_%s.mp4", author, title, id)
334 | origName = files.ReplaceForbidden(origName)
335 | name, err := files.GetFileNameNext(origName)
336 | if err != nil {
337 | fmt.Println(err)
338 | return
339 | }
340 |
341 | fmt.Println(name)
342 |
343 | mtxComDone := &sync.Mutex{}
344 | var commentDone bool
345 |
346 | var gm *gorman.GoroutineManager
347 | var gmCom *gorman.GoroutineManager
348 |
349 | gm = gorman.WithChecker(func(c int) {
350 | switch c {
351 | case 0:
352 | default:
353 | gm.Cancel()
354 | if gmCom != nil {
355 | gmCom.Cancel();
356 | }
357 | }
358 | })
359 |
360 | gmCom = gorman.WithChecker(func(c int) {
361 | switch c {
362 | case 0:
363 | case COMMENT_DONE:
364 | func() {
365 | mtxComDone.Lock()
366 | defer mtxComDone.Unlock()
367 | commentDone = true;
368 | }()
369 | default:
370 | gmCom.Cancel()
371 | }
372 | })
373 |
374 | chInterrupt := make(chan os.Signal, 10)
375 | signal.Notify(chInterrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
376 | defer signal.Stop(chInterrupt)
377 |
378 | ctx, cancel := context.WithCancel(context.Background())
379 |
380 | var interrupt bool
381 | gm.Go(func(c <-chan struct{}) int {
382 | select {
383 | case <-chInterrupt:
384 | interrupt = true
385 | case <-c:
386 | }
387 |
388 | cancel()
389 | gm.Cancel()
390 | return 1
391 | })
392 |
393 | if continuation != "" {
394 | gmCom.Go(func(c <-chan struct{}) int {
395 | getComment(gmCom, ctx, c, isReplay, continuation, origName)
396 | fmt.Printf("\ncomment done\n")
397 | return COMMENT_DONE
398 | })
399 | }
400 |
401 | gm.Go(func(c <-chan struct{}) int {
402 | select {
403 | case <-c: cancel()
404 | }
405 | return 0
406 | })
407 |
408 | var retry bool
409 | if (! ytNoStreamlink) {
410 | retry, err = execStreamlink(gm, uri, name)
411 | }
412 | if !interrupt {
413 | if err != nil || retry || (ytNoStreamlink && (! ytNoYoutube_dl)) {
414 | execYoutube_dl(gm, uri, name)
415 | }
416 | }
417 |
418 | if continuation != "" {
419 | if isReplay {
420 | if !commentDone {
421 | fmt.Printf("\nwaiting comment\n")
422 | gmCom.Wait()
423 | } else {
424 | gmCom.Wait()
425 | }
426 |
427 | } else {
428 | if !ytNoStreamlink || !ytNoYoutube_dl {
429 | gmCom.Cancel()
430 | }
431 | gmCom.Wait()
432 | }
433 | }
434 |
435 | gm.Cancel()
436 | gm.Wait()
437 |
438 | return
439 | }
440 |
--------------------------------------------------------------------------------
/src/youtube/youtube.gox:
--------------------------------------------------------------------------------
1 | package youtube
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "io/ioutil"
8 | "regexp"
9 | "encoding/json"
10 | "html"
11 | "strings"
12 | "net/url"
13 | "os"
14 | "strconv"
15 | "bytes"
16 | "os/exec"
17 | "os/signal"
18 | "archive/zip"
19 | "sync"
20 | "../obj"
21 | "io"
22 | "../files"
23 |
24 | "../httpsub"
25 | "../zip2mp4"
26 | "log"
27 | )
28 |
29 | type YtDash struct {
30 | SeqNo int
31 | SeqNoFound bool
32 | SeqNoBack int
33 | VAddr string
34 | VQuery url.Values
35 | AAddr string
36 | AQuery url.Values
37 | TsFile *os.File
38 | FFCmd *exec.Cmd
39 | FFBuffer *bytes.Buffer
40 | TryBack bool
41 | StartBack bool
42 | ChEnd chan bool
43 | ChEndBack chan bool
44 | zipFile *os.File
45 | zipWriter *zip.Writer
46 | mZip sync.Mutex
47 | fileName string
48 | Title string
49 | Id string
50 | }
51 | func (yt *YtDash) SetFileName(fileName string) {
52 | yt.fileName = files.ReplaceForbidden(fileName)
53 | }
54 | func (yt *YtDash) fetch(isVideo, isBack bool) (fileName string, err error) {
55 |
56 | var addr string
57 | var query url.Values
58 | var sn int
59 |
60 | if isVideo {
61 | addr = yt.VAddr
62 | query = yt.VQuery
63 | } else {
64 | addr = yt.AAddr
65 | query = yt.AQuery
66 | }
67 |
68 | if isBack && (! yt.SeqNoFound) {
69 | err = fmt.Errorf("isBack && (! SeqNoFound)")
70 | return
71 | }
72 |
73 | if yt.SeqNoFound {
74 | //fmt.Printf("SQ set to %d\n", yt.SeqNo)
75 | if isBack {
76 | sn = yt.SeqNoBack
77 | } else {
78 | sn = yt.SeqNo
79 | }
80 | query.Set("sq", fmt.Sprintf("%d", sn))
81 |
82 | //fmt.Printf("%v\n", query)
83 | }
84 |
85 | uri := fmt.Sprintf("%s?%s", addr, query.Encode())
86 | req, _ := http.NewRequest("GET", uri, nil)
87 |
88 | client := new(http.Client)
89 | resp, err := client.Do(req)
90 | if err != nil {
91 | return
92 | }
93 | defer resp.Body.Close()
94 |
95 | switch resp.StatusCode {
96 | case 200:
97 | default:
98 | err = fmt.Errorf("StatusCode is %v\n%v\n%v", resp.StatusCode, uri, query)
99 | return
100 | }
101 |
102 | switch query.Get("source") {
103 | case "yt_live_broadcast":
104 |
105 | bs, e := ioutil.ReadAll(resp.Body)
106 | if e != nil {
107 | err = e
108 | return
109 | }
110 |
111 | if (! yt.SeqNoFound) && (! isBack) {
112 |
113 | if ma := regexp.MustCompile(`Sequence-Number\s*:\s*(\d+)`).FindSubmatch(bs); len(ma) > 0 {
114 | sn, err = strconv.Atoi(string(ma[1]))
115 | if err != nil {
116 | err = fmt.Errorf("Sequence-Number Not a Number: %v", ma)
117 | return
118 | }
119 | yt.SeqNo = sn
120 | yt.SeqNoBack = sn - 1
121 | yt.SeqNoFound = true
122 | fmt.Printf("start SeqNo: %d\n", sn)
123 |
124 | } else {
125 | err = fmt.Errorf("Sequence-Number Not found")
126 | return
127 | }
128 |
129 | yt.RecordBack()
130 | }
131 |
132 | if isVideo {
133 | fileName = fmt.Sprintf("video-%d.mp4", sn)
134 | } else {
135 | fileName = fmt.Sprintf("audio-%d.mp4", sn)
136 | }
137 |
138 | buff := bytes.NewBuffer(bs)
139 | if err = yt.WriteZip(fileName, buff); err != nil {
140 | return
141 | }
142 | }
143 |
144 | return
145 | }
146 | func (yt *YtDash) fetchVideo() (string, error) {
147 | return yt.fetch(true, false)
148 | }
149 | func (yt *YtDash) fetchAudio() (string, error) {
150 | return yt.fetch(false, false)
151 | }
152 | func (yt *YtDash) IncrSeqNo() {
153 | yt.SeqNo++
154 | }
155 | func (yt *YtDash) fetchVideoBack() (string, error) {
156 | return yt.fetch(true, true)
157 | }
158 | func (yt *YtDash) fetchAudioBack() (string, error) {
159 | return yt.fetch(false, true)
160 | }
161 | func (yt *YtDash) DecrSeqNoBack() {
162 | yt.SeqNoBack--
163 | }
164 | func (yt *YtDash) RecordYoutube() {
165 | var vname string
166 | var aname string
167 | func() {
168 | uri := fmt.Sprintf("%s?%s", yt.VAddr, yt.VQuery.Encode())
169 | vname = fmt.Sprintf("%s(%s)-v.mp4", yt.Title, yt.Id)
170 | fmt.Println(uri)
171 | sub := httpsub.Get(uri, vname)
172 | sub.Concurrent(4)
173 | sub.Wait()
174 | }()
175 | func() {
176 | uri := fmt.Sprintf("%s?%s", yt.AAddr, yt.AQuery.Encode())
177 | aname = fmt.Sprintf("%s(%s)-a.mp4", yt.Title, yt.Id)
178 | sub := httpsub.Get(uri, aname)
179 | sub.Concurrent(4)
180 | sub.Wait()
181 | }()
182 | if zip2mp4.FFmpegExists() {
183 | exts := []string{"mp4", "mkv"}
184 | for _, ext := range exts {
185 | oname := fmt.Sprintf("%s(%s).%s", yt.Title, yt.Id, ext)
186 | if zip2mp4.MergeVA(vname, aname, oname) {
187 | os.Remove(vname)
188 | os.Remove(aname)
189 | return
190 | } else {
191 | os.Remove(oname)
192 | }
193 | }
194 | }
195 | // ffmpeg Not exists OR merge NG
196 | fv, e := os.Open(vname)
197 | if e != nil {
198 | log.Fatalln(e)
199 | }
200 | yt.WriteZip("video.mp4", fv)
201 | fv.Close()
202 |
203 | fa, e := os.Open(aname)
204 | if e != nil {
205 | log.Fatalln(e)
206 | }
207 | yt.WriteZip("audio.mp4", fa)
208 | fa.Close()
209 |
210 | os.Remove(vname)
211 | os.Remove(aname)
212 |
213 | }
214 | func (yt *YtDash) Wait() {
215 |
216 | yt.ChEnd = make(chan bool)
217 | yt.ChEndBack = make(chan bool)
218 |
219 | switch yt.VQuery.Get("source") {
220 | case "youtube":
221 | yt.RecordYoutube()
222 |
223 | case "yt_live_broadcast":
224 | yt.RecordForward()
225 | <-yt.ChEnd
226 | if yt.StartBack {
227 | <-yt.ChEndBack
228 | }
229 | }
230 | }
231 | func (yt *YtDash) Close() {
232 | if yt.zipWriter != nil {
233 | yt.zipWriter.Close()
234 | }
235 | if yt.zipFile != nil {
236 | yt.zipFile.Close()
237 | }
238 | }
239 | func (yt *YtDash) OpenFile() (err error) {
240 |
241 | fileName, err := files.GetFileNameNext(yt.fileName)
242 | if err != nil {
243 | return
244 | }
245 |
246 | file, err := os.Create(fileName)
247 | if err != nil {
248 | log.Fatalln(err)
249 | }
250 | yt.zipFile = file
251 |
252 | yt.zipWriter = zip.NewWriter(file)
253 |
254 | chSig := make(chan os.Signal, 1)
255 | signal.Notify(chSig, os.Interrupt)
256 | go func() {
257 | <-chSig
258 | yt.mZip.Lock()
259 | defer yt.mZip.Unlock()
260 | if yt.zipWriter != nil {
261 | yt.zipWriter.Close()
262 | }
263 | os.Exit(0)
264 | }()
265 | return
266 | }
267 |
268 | func (yt *YtDash) WriteZip(name string, rdr io.Reader) (err error) {
269 | yt.mZip.Lock()
270 | defer yt.mZip.Unlock()
271 |
272 | if yt.zipFile == nil || yt.zipWriter == nil {
273 | yt.OpenFile()
274 | }
275 |
276 | wr, err := yt.zipWriter.Create(name)
277 | if err != nil {
278 | return
279 | }
280 |
281 | if _, err = io.Copy(wr, rdr); err != nil {
282 | return
283 | }
284 | return
285 | }
286 | func (yt *YtDash) RecordForward() {
287 | go func() {
288 | defer func() {
289 | close(yt.ChEnd)
290 | }()
291 | for {
292 | vfile, err := yt.fetchVideo()
293 | if err != nil {
294 | fmt.Printf("RecordForward: %v\n", err)
295 | return
296 | }
297 | afile, err := yt.fetchAudio()
298 | if err != nil {
299 | fmt.Printf("RecordForward: %v\n", err)
300 | return
301 | }
302 | if true {
303 | fmt.Printf("%s, %s\n", vfile, afile)
304 | }
305 | yt.IncrSeqNo()
306 | }
307 | }()
308 | }
309 | func (yt *YtDash) RecordBack() {
310 | if yt.TryBack && (! yt.StartBack) {
311 | yt.StartBack = true
312 | go func() {
313 | defer func() {
314 | close(yt.ChEndBack)
315 | }()
316 | for yt.SeqNoBack >= 0 {
317 | vfile, err := yt.fetchVideoBack()
318 | if err != nil {
319 | fmt.Printf("RecordBack: %v\n", err)
320 | return
321 | }
322 | afile, err := yt.fetchAudioBack()
323 | if err != nil {
324 | fmt.Printf("RecordBack: %v\n", err)
325 | return
326 | }
327 | if true {
328 | fmt.Printf("%s, %s\n", vfile, afile)
329 | }
330 | yt.DecrSeqNoBack()
331 | }
332 | }()
333 | }
334 | }
335 |
336 | func Record(id string) (err error) {
337 | uri := fmt.Sprintf("https://www.youtube.com/watch?v=%s", id)
338 | req, _ := http.NewRequest("GET", uri, nil)
339 |
340 | client := new(http.Client)
341 | resp, err := client.Do(req)
342 | if err != nil {
343 | fmt.Println(err)
344 | return
345 | }
346 | defer resp.Body.Close()
347 | dat, _ := ioutil.ReadAll(resp.Body)
348 |
349 | var a interface{}
350 |
351 | re := regexp.MustCompile(`\Wytplayer\.config\p{Zs}*=\p{Zs}*({.*?})\p{Zs}*;`)
352 | if ma := re.FindSubmatch(dat); len(ma) > 0 {
353 | str := html.UnescapeString(string(ma[1]))
354 | if err = json.Unmarshal([]byte(str), &a); err != nil {
355 | fmt.Println(str)
356 | fmt.Println(err)
357 | return
358 | }
359 | } else {
360 | fmt.Println("ytplayer.config not found")
361 | return
362 | }
363 |
364 | // debug print
365 | //obj.PrintAsJson(a)
366 |
367 | title, ok := obj.FindString(a, "args", "title")
368 | if (! ok) {
369 | fmt.Println("title not found")
370 | return
371 | }
372 |
373 | res, ok := obj.FindString(a, "args", "adaptive_fmts")
374 | if (! ok) {
375 | if res, ok := obj.FindString(a, "args", "hlsvp"); ok {
376 | fmt.Printf("hls: %s\n", res)
377 | return
378 | }
379 | obj.PrintAsJson(a)
380 | return
381 | }
382 |
383 | var maxVideoBr int
384 | var maxAudioBr int
385 | var videoUrl string
386 | var audioUrl string
387 | var qualityLabel string
388 | for _, s := range strings.Split(res, ",") {
389 | //fmt.Println(s)
390 | f, e := url.ParseQuery(s)
391 | //obj.PrintAsJson(f)
392 | //fmt.Println(f)
393 | if e != nil {
394 | fmt.Println(e)
395 | return
396 | }
397 | // type
398 | // bitrate
399 | t := f.Get("type")
400 | br, err := strconv.Atoi(f.Get("bitrate"))
401 | if err != nil {
402 | continue
403 | }
404 |
405 | if strings.HasPrefix(t, "video") {
406 | if br > maxVideoBr {
407 | maxVideoBr = br
408 | videoUrl = f.Get("url")
409 | qualityLabel = f.Get("quality_label")
410 | }
411 | } else if strings.HasPrefix(t, "audio") {
412 | if br > maxAudioBr {
413 | maxAudioBr = br
414 | audioUrl = f.Get("url")
415 | }
416 | }
417 | }
418 | fmt.Printf("Quality: %s\n", qualityLabel)
419 |
420 | varr := strings.SplitN(videoUrl, "?", 2)
421 | if len(varr) != 2 {
422 | return
423 | }
424 | aarr := strings.SplitN(audioUrl, "?", 2)
425 | if len(aarr) != 2 {
426 | return
427 | }
428 |
429 | yt := new(YtDash)
430 | defer yt.Close()
431 |
432 | yt.Id = id
433 | yt.Title = files.ReplaceForbidden(title)
434 |
435 | yt.SetFileName(fmt.Sprintf("%s(%s).zip", title, id))
436 |
437 | yt.VAddr = varr[0]
438 | vQuery, e := url.ParseQuery(varr[1])
439 | if e != nil {
440 | return
441 | }
442 | yt.VQuery = vQuery
443 |
444 | //obj.PrintAsJson(vQuery)
445 | //fmt.Println(yt.VAddr + "?" + vQuery.Encode())
446 |
447 | yt.AAddr = aarr[0]
448 | aQuery, e := url.ParseQuery(aarr[1])
449 | if e != nil {
450 | return
451 | }
452 | yt.AQuery = aQuery
453 |
454 | yt.TryBack = true
455 | yt.Wait()
456 |
457 | return
458 | }
459 |
--------------------------------------------------------------------------------
/src/zip2mp4/zip2mp4.go:
--------------------------------------------------------------------------------
1 | package zip2mp4
2 |
3 | import (
4 | "fmt"
5 | "archive/zip"
6 | "regexp"
7 | "strconv"
8 | "log"
9 | "sort"
10 | "io"
11 | "os"
12 | "os/exec"
13 | "io/ioutil"
14 | "../files"
15 | "../log4gui"
16 |
17 | "database/sql"
18 | _ "github.com/mattn/go-sqlite3"
19 | "bytes"
20 | "time"
21 | "../niconico"
22 | "../youtube"
23 | "../procs/ffmpeg"
24 |
25 | "context"
26 | "github.com/gin-gonic/gin"
27 | "net/http"
28 | )
29 |
30 | type ZipMp4 struct {
31 | ZipName string
32 | Mp4NameOpened string
33 | mp4List []string
34 |
35 | FFMpeg *exec.Cmd
36 | FFStdin io.WriteCloser
37 | }
38 | var cmdListFF = []string{
39 | "./bin/ffmpeg/ffmpeg",
40 | "./bin/ffmpeg",
41 | "./ffmpeg/ffmpeg",
42 | "./ffmpeg",
43 | "ffmpeg",
44 | }
45 | var cmdListMP42TS = []string{
46 | "./bin/bento4/bin/mp42ts",
47 | "./bento4/bin/mp42ts",
48 | "./bento4/mp42ts",
49 | "./bin/bento4/mp42ts",
50 | "./bin/mp42ts",
51 | "./mp42ts",
52 | "mp42ts",
53 | }
54 | // return cmd = nil if cmd not exists
55 | func openProg(cmdList *[]string, stdinEn, stdoutEn, stdErrEn, consoleEn bool, args []string) (cmd *exec.Cmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) {
56 |
57 | for i, cmdName := range *cmdList {
58 | cmd = exec.Command(cmdName, args...)
59 |
60 | var err error
61 | if stdinEn {
62 | stdin, err = cmd.StdinPipe()
63 | if err != nil {
64 | log.Fatalln(err)
65 | }
66 | }
67 |
68 | if stdoutEn {
69 | stdout, err = cmd.StdoutPipe()
70 | if err != nil {
71 | log.Fatalln(err)
72 | }
73 | } else {
74 | if consoleEn {
75 | cmd.Stdout = os.Stdout
76 | }
77 | }
78 |
79 | if stdErrEn {
80 | stderr, err = cmd.StderrPipe()
81 | if err != nil {
82 | log.Fatalln(err)
83 | }
84 | } else {
85 | if consoleEn {
86 | cmd.Stderr = os.Stderr
87 | }
88 | }
89 |
90 | if err = cmd.Start(); err != nil {
91 | continue
92 | } else {
93 | if i != 0 {
94 | *cmdList = []string{cmdName}
95 | }
96 | //fmt.Printf("CMD: %#v\n", cmd.Args)
97 | return
98 | }
99 | }
100 | cmd = nil
101 | return
102 | }
103 | func MergeVA(vFileName, aFileName, oFileName string) bool {
104 | cmd, _, _, _ := openProg(&cmdListFF, false, false, false, true, []string{
105 | "-i", vFileName,
106 | "-i", aFileName,
107 | "-c", "copy",
108 | "-y",
109 | oFileName,
110 | })
111 | if cmd == nil {
112 | return false
113 | }
114 | if err := cmd.Wait(); err != nil {
115 | fmt.Println(err)
116 | return false
117 | }
118 | return true
119 | }
120 | func FFmpegExists() bool {
121 | cmd, _, _, _ := openProg(&cmdListFF, false, false, false, false, []string{"-version"})
122 | if cmd == nil {
123 | return false
124 | }
125 | cmd.Wait()
126 | return true
127 | }
128 | func GetFormat(fileName string) (vFormat, aFormat string) {
129 | cmd, _, stdout, stderr := openProg(&cmdListFF, false, true, true, false, []string{"-i", fileName})
130 | if cmd == nil {
131 | return
132 | }
133 | b1, _ := ioutil.ReadAll(stdout)
134 | b2, _ := ioutil.ReadAll(stderr)
135 | cmd.Wait()
136 |
137 | s := string(b1) + string(b2)
138 | if ma := regexp.MustCompile(`(?i)Stream\s+#.+?:\s+Video:\s+(.*?),`).FindStringSubmatch(s); len(ma) > 0 {
139 | vFormat = ma[1]
140 | }
141 | if ma := regexp.MustCompile(`(?i)Stream\s+#.+?:\s+Audio:\s+(.*?),`).FindStringSubmatch(s); len(ma) > 0 {
142 | aFormat = ma[1]
143 | }
144 |
145 | return
146 | }
147 | func openFFMpeg(stdinEn, stdoutEn, stdErrEn, consoleEn bool, args []string) (cmd *exec.Cmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) {
148 | return openProg(&cmdListFF, stdinEn, stdoutEn, stdErrEn, consoleEn, args)
149 | }
150 | func openMP42TS(consoleEn bool, args []string) (cmd *exec.Cmd) {
151 | cmd, _, _, _ = openProg(&cmdListMP42TS, false, false, false, consoleEn, args)
152 | return
153 | }
154 | func (z *ZipMp4) Wait() {
155 |
156 | if z.FFStdin != nil {
157 | z.FFStdin.Close()
158 | }
159 |
160 | if z.FFMpeg != nil {
161 | if err := z.FFMpeg.Wait(); err != nil {
162 | log.Fatalln(err)
163 | }
164 | z.FFMpeg = nil
165 | }
166 | }
167 | func (z *ZipMp4) CloseFFInput() {
168 | z.FFStdin.Close()
169 | }
170 | func (z *ZipMp4) OpenFFMpeg(ext string) {
171 | //
172 | z.Wait()
173 |
174 | if ext == "" {
175 | ext = "mp4"
176 | }
177 | name := files.ChangeExtention(z.ZipName, ext)
178 | name, err := files.GetFileNameNext(name)
179 | if err != nil {
180 | fmt.Println(err)
181 | os.Exit(1)
182 | }
183 | z.Mp4NameOpened = name
184 | z.mp4List = append(z.mp4List, name)
185 |
186 |
187 | cmd, stdin, err := ffmpeg.Open(
188 | "-i", "-",
189 | "-c", "copy",
190 | //"-movflags", "faststart", // test
191 | "-y",
192 | name,
193 | )
194 | if err != nil {
195 | log.Fatalln(err)
196 | }
197 |
198 | z.FFMpeg = cmd
199 | z.FFStdin = stdin
200 | }
201 |
202 | func (z *ZipMp4) FFInputCombFromFile(videoFile, audioFile string) {
203 |
204 | vTs := fmt.Sprintf("%s.ts", videoFile)
205 | cmdV := openMP42TS(false, []string{
206 | videoFile,
207 | vTs,
208 | })
209 | if cmdV == nil {
210 | fmt.Println("mp42ts not found OR command failed")
211 | os.Exit(1)
212 | }
213 | defer os.Remove(vTs)
214 |
215 | aTs := fmt.Sprintf("%s.ts", audioFile)
216 | cmdA := openMP42TS(false, []string{
217 | audioFile,
218 | aTs,
219 | })
220 | if cmdA == nil {
221 | fmt.Println("mp42ts not found OR command failed")
222 | os.Exit(1)
223 | }
224 | defer os.Remove(aTs)
225 |
226 | if err := cmdV.Wait(); err != nil {
227 | log.Fatalln(err)
228 | }
229 | if err := cmdA.Wait(); err != nil {
230 | log.Fatalln(err)
231 | }
232 |
233 | cmd, _, stdout, _ := openFFMpeg(false, true, false, false, []string{
234 | "-i", vTs,
235 | "-i", aTs,
236 | "-c", "copy",
237 | "-f", "mpegts",
238 | "-",
239 | })
240 | if cmd == nil {
241 | log.Fatalln("ffmpeg not installed")
242 | }
243 |
244 | z.FFInput(stdout)
245 |
246 | if err := cmd.Wait(); err != nil {
247 | log.Fatalln(err)
248 | }
249 | }
250 | func (z *ZipMp4) FFInput(rdr io.Reader) {
251 | if _, err := io.Copy(z.FFStdin, rdr); err != nil {
252 | log.Fatalln(err)
253 | }
254 | }
255 |
256 | type Index struct {
257 | int
258 | }
259 | type Chunk struct {
260 | VideoIndex *Index
261 | AudioIndex *Index
262 | VAIndex *Index
263 | }
264 |
265 | func Convert(fileName string) (err error) {
266 | zr, err := zip.OpenReader(fileName)
267 | if err != nil {
268 | return
269 | }
270 |
271 | chunks := make(map[int64]Chunk)
272 |
273 | for i, r := range zr.File {
274 | //fmt.Printf("X %v %v\n", i, r.Name)
275 |
276 | if ma := regexp.MustCompile(`\Avideo-(\d+)\.\w+\z`).FindStringSubmatch(r.Name); len(ma) > 0 {
277 | num, err := strconv.ParseInt(string(ma[1]), 10, 64)
278 | if err != nil {
279 | log.Fatal(err)
280 | }
281 | if v, ok := chunks[num]; ok {
282 | v.VideoIndex = &Index{i}
283 | chunks[num] = v
284 | } else {
285 | chunks[num] = Chunk{VideoIndex: &Index{i}}
286 | }
287 |
288 | //fmt.Printf("V %v %v\n", i, r.Name)
289 | } else if ma := regexp.MustCompile(`\Aaudio-(\d+)\.\w+\z`).FindStringSubmatch(r.Name); len(ma) > 0 {
290 | num, err := strconv.ParseInt(string(ma[1]), 10, 64)
291 | if err != nil {
292 | log.Fatal(err)
293 | }
294 | if v, ok := chunks[num]; ok {
295 | v.AudioIndex = &Index{i}
296 | chunks[num] = v
297 | } else {
298 | chunks[num] = Chunk{AudioIndex: &Index{i}}
299 | }
300 | //fmt.Printf("A %v %v\n", num, r.Name)
301 | } else if ma := regexp.MustCompile(`\A(\d+)\.\w+\z`).FindStringSubmatch(r.Name); len(ma) > 0 {
302 | num, err := strconv.ParseInt(string(ma[1]), 10, 64)
303 | if err != nil {
304 | log.Fatal(err)
305 | }
306 | if v, ok := chunks[num]; ok {
307 | v.VAIndex = &Index{i}
308 | chunks[num] = v
309 | } else {
310 | chunks[num] = Chunk{VAIndex: &Index{i}}
311 | }
312 | //fmt.Printf("V+A %v %v\n", num, r.Name)
313 | } else {
314 | fmt.Printf("%v %v\n", i, r.Name)
315 | log4gui.Info(fmt.Sprintf("Unsupported zip: %s", fileName))
316 | os.Exit(1)
317 | }
318 | }
319 |
320 | keys := make([]int64, 0, len(chunks))
321 | for k := range chunks {
322 | keys = append(keys, k)
323 | }
324 |
325 | sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
326 |
327 | var tmpVideoName string
328 | var tmpAudioName string
329 |
330 | var zm *ZipMp4
331 | defer func() {
332 | if zm != nil {
333 | zm.CloseFFInput()
334 | zm.Wait()
335 | }
336 | }()
337 |
338 | zm = &ZipMp4{ZipName: fileName}
339 | zm.OpenFFMpeg("mp4")
340 |
341 | prevIndex := int64(-1)
342 | for _, key := range keys {
343 | if prevIndex >= 0 {
344 | if key != prevIndex + 1 {
345 | // [FIXME] reopen new mp4file?
346 | //return fmt.Errorf("\n\nError: seq skipped: %d --> %d\n\n", prevIndex, key)
347 |
348 | fmt.Printf("\nSeqNo. skipped: %d --> %d\n", prevIndex, key)
349 | if zm != nil {
350 | zm.CloseFFInput()
351 | zm.Wait()
352 | }
353 | zm = &ZipMp4{ZipName: fileName}
354 | zm.OpenFFMpeg("mp4")
355 | }
356 | }
357 | prevIndex = key
358 |
359 | if chunks[key].VAIndex != nil {
360 | r, e := zr.File[chunks[key].VAIndex.int].Open()
361 | if e != nil {
362 | log.Fatalln(e)
363 | }
364 | zm.FFInput(r)
365 | r.Close()
366 |
367 | } else if chunks[key].VideoIndex != nil && chunks[key].AudioIndex != nil {
368 |
369 | if tmpVideoName == "" {
370 | f, e := ioutil.TempFile(".", "__temp-")
371 | if e != nil {
372 | log.Fatalln(e)
373 | }
374 | f.Close()
375 | tmpVideoName = f.Name()
376 | }
377 | if tmpAudioName == "" {
378 | f, e := ioutil.TempFile(".", "__temp-")
379 | if e != nil {
380 | log.Fatalln(e)
381 | }
382 | f.Close()
383 | tmpAudioName = f.Name()
384 | }
385 |
386 | // open temporary file
387 | tmpVideo, err := os.Create(tmpVideoName)
388 | if err != nil {
389 | log.Fatalln(err)
390 | }
391 | tmpAudio, err := os.Create(tmpAudioName)
392 | if err != nil {
393 | log.Fatalln(err)
394 | }
395 |
396 | // copy Video to file
397 | rv, e := zr.File[chunks[key].VideoIndex.int].Open()
398 | if e != nil {
399 | log.Fatalln(e)
400 | }
401 | if _, e := io.Copy(tmpVideo, rv); e != nil {
402 | log.Fatalln(e)
403 | }
404 | rv.Close()
405 | tmpVideo.Close()
406 |
407 | // copy Audio to file
408 | ra, e := zr.File[chunks[key].AudioIndex.int].Open()
409 | if e != nil {
410 | log.Fatalln(e)
411 | }
412 | if _, e := io.Copy(tmpAudio, ra); e != nil {
413 | log.Fatalln(e)
414 | }
415 | ra.Close()
416 | tmpAudio.Close()
417 |
418 | // combine video + audio using ffmpeg(+mp42ts)
419 | zm.FFInputCombFromFile(tmpVideoName, tmpAudioName)
420 | os.Remove(tmpVideoName)
421 | os.Remove(tmpAudioName)
422 | } else {
423 | if (chunks[key].VideoIndex == nil && chunks[key].AudioIndex != nil) ||
424 | (chunks[key].VideoIndex != nil && chunks[key].AudioIndex == nil) {
425 | fmt.Printf("\nIncomplete sequence. skipped: %d\n", key)
426 | if zm != nil {
427 | zm.CloseFFInput()
428 | zm.Wait()
429 | }
430 | zm = &ZipMp4{ZipName: fileName}
431 | zm.OpenFFMpeg("mp4")
432 | }
433 | }
434 | }
435 |
436 | zm.CloseFFInput()
437 | zm.Wait()
438 | fmt.Printf("\nfinish: %s\n", zm.Mp4NameOpened)
439 |
440 | return
441 | }
442 |
443 |
444 | func ExtractChunks(fileName string, skipHb bool) (done bool, err error) {
445 | db, err := sql.Open("sqlite3", fileName)
446 | if err != nil {
447 | return
448 | }
449 | defer db.Close()
450 |
451 | niconico.WriteComment(db, fileName, skipHb)
452 |
453 | rows, err := db.Query(niconico.SelMedia)
454 | if err != nil {
455 | return
456 | }
457 | defer rows.Close()
458 |
459 | dir := files.RemoveExtention(fileName)
460 | if err = files.MkdirByFileName(dir + "/"); err != nil {
461 | return
462 | }
463 | var printTime int64
464 | for rows.Next() {
465 | var seqno int64
466 | var bw int
467 | var size int
468 | var data []byte
469 | err = rows.Scan(&seqno, &bw, &size, &data)
470 | if err != nil {
471 | return
472 | }
473 | name := fmt.Sprintf("%s/%d.ts", dir, seqno)
474 | // print
475 | now := time.Now().Unix()
476 | if now != printTime {
477 | printTime = now
478 | fmt.Println(name)
479 | }
480 |
481 | err = func() (e error) {
482 | f, e := os.Create(name)
483 | if e != nil {
484 | return
485 | }
486 | defer f.Close()
487 | _, e = f.Write(data)
488 | return
489 | }()
490 | if err != nil {
491 | return
492 | }
493 | }
494 |
495 | done = true
496 | return
497 | }
498 |
499 | func ConvertDB(fileName, ext string, skipHb, forceConcat bool, seqnoStart, seqnoEnd int64) (done bool, nMp4s int, skipped bool, err error) {
500 | db, err := sql.Open("sqlite3", fileName)
501 | if err != nil {
502 | return
503 | }
504 | defer db.Close()
505 |
506 | niconico.WriteComment(db, fileName, skipHb)
507 |
508 | var zm *ZipMp4
509 | defer func() {
510 | if zm != nil {
511 | //zm.CloseFFInput()
512 | zm.Wait()
513 | }
514 | }()
515 |
516 | zm = &ZipMp4{ZipName: fileName}
517 | zm.OpenFFMpeg(ext)
518 |
519 | rows, err := db.Query(niconico.SelMediaF(seqnoStart, seqnoEnd))
520 | if err != nil {
521 | return
522 | }
523 | defer rows.Close()
524 |
525 | prevBw := -1
526 | prevIndex := int64(-1)
527 | for rows.Next() {
528 | var seqno int64
529 | var bw int
530 | var size int
531 | var data []byte
532 | err = rows.Scan(&seqno, &bw, &size, &data)
533 | if err != nil {
534 | return
535 | }
536 |
537 | // チャンクが飛んでいる場合はファイルを分ける
538 | // BANDWIDTHが変わる場合はファイルを分ける
539 | if (prevIndex >= 0 && seqno != prevIndex + 1) || (prevBw >= 0 && bw != prevBw) {
540 | if bw != prevBw {
541 | fmt.Printf("\nBandwitdh changed: %d --> %d\n\n", prevBw, bw)
542 | } else {
543 | fmt.Printf("\nSeqNo. skipped: %d --> %d\n\n", prevIndex, seqno)
544 | }
545 |
546 | //if zm != nil {
547 | // zm.CloseFFInput()
548 | // zm.Wait()
549 | //}
550 | if ! forceConcat {
551 | zm.OpenFFMpeg(ext)
552 | }
553 | skipped = true
554 | }
555 | prevBw = bw
556 | prevIndex = seqno
557 |
558 | zm.FFInput(bytes.NewBuffer(data))
559 | }
560 |
561 | //zm.CloseFFInput()
562 | zm.Wait()
563 | fmt.Printf("\nfinish:\n")
564 | for _, s := range zm.mp4List {
565 | fmt.Println(s)
566 | }
567 | done = true
568 | nMp4s = len(zm.mp4List)
569 |
570 | return
571 | }
572 |
573 | func ReplayDB(fileName string, hlsPort int, seqnoStart int64) (err error) {
574 | db, err := sql.Open("sqlite3", fileName)
575 | if err != nil {
576 | return
577 | }
578 | defer db.Close()
579 |
580 | var isTimeshift bool
581 | if m := regexp.MustCompile(`\(TS\)\.sqlite3$`).FindStringSubmatch(fileName); len(m) > 0 {
582 | isTimeshift = true
583 | }
584 | fmt.Println("isTimeshift:", isTimeshift)
585 |
586 | seqnoInit := seqnoStart
587 |
588 | timeStart := time.Now()
589 | timeLast := time.Now()
590 |
591 | seqnoCurrent := seqnoStart
592 |
593 | if (true) {
594 | gin.SetMode(gin.ReleaseMode)
595 | gin.DefaultErrorWriter = ioutil.Discard
596 | gin.DefaultWriter = ioutil.Discard
597 | router := gin.Default()
598 |
599 | router.GET("", func(c *gin.Context) {
600 | c.Redirect(http.StatusMovedPermanently, "/m3u8/2/0/index.m3u8")
601 | c.Abort()
602 | })
603 |
604 | router.GET("/m3u8/:delay/:shift/index.m3u8", func(c *gin.Context) {
605 | secPerSegment := 1.5
606 | targetDuration := "2"
607 | targetDurationFloat := 2.0
608 | extInf := "1.5"
609 | if isTimeshift {
610 | secPerSegment = 5.0
611 | targetDuration = "3"
612 | targetDurationFloat = 3.0
613 | extInf = "3.0"
614 | }
615 | shift, err := strconv.Atoi(c.Param("shift"))
616 | if err != nil {
617 | shift = 0
618 | }
619 | if shift < 0 {
620 | shift = 0
621 | }
622 | delay, err := strconv.Atoi(c.Param("delay"))
623 | if err != nil {
624 | delay = 0
625 | }
626 | if delay < 2 {
627 | delay = 2
628 | }
629 | if (! isTimeshift) {
630 | if delay < 4 {
631 | delay = 4
632 | }
633 | }
634 | seqnoRewind := int64(delay)
635 | timeout := targetDurationFloat * float64(delay + 1) * 2 + 1
636 | timeNow := time.Now()
637 | if float64(timeNow.Sub(timeLast) / time.Second) > timeout {
638 | fmt.Printf("(%s) CONTINUE\n", timeNow.Format("15:04:05"))
639 | seqnoStart = seqnoCurrent - seqnoRewind
640 | if seqnoStart < seqnoInit {
641 | seqnoStart = seqnoInit
642 | }
643 | timeStart = timeNow
644 | seqnoCurrent = seqnoStart
645 | } else {
646 | seqnoCurrent = int64(float64(timeNow.Sub(timeStart) / time.Second) / secPerSegment) + seqnoStart
647 | }
648 | timeLast = timeNow
649 | seqno := seqnoCurrent - int64(shift)
650 | body := fmt.Sprintf(
651 | `#EXTM3U
652 | #EXT-X-VERSION:3
653 | #EXT-X-TARGETDURATION:%s
654 | #EXT-X-MEDIA-SEQUENCE:%d
655 |
656 | `, targetDuration, seqno)
657 | for i := int64(delay); i >= 0; i-- {
658 | body += fmt.Sprintf(
659 | `#EXTINF:%s,
660 | /ts/%d/test.ts
661 |
662 | `, extInf, seqno - i)
663 | }
664 | if shift > 0 {
665 | fmt.Printf("(%s) Current SeqNo: %d(-%d)\n", timeNow.Format("15:04:05"), seqnoCurrent, shift)
666 | } else {
667 | fmt.Printf("(%s) Current SeqNo: %d\n", timeNow.Format("15:04:05"), seqnoCurrent)
668 | }
669 | c.Data(http.StatusOK, "application/x-mpegURL", []byte(body))
670 | return
671 | })
672 |
673 | router.GET("/ts/:idx/test.ts", func(c *gin.Context) {
674 | i, _ := strconv.Atoi(c.Param("idx"))
675 | var b []byte
676 | db.QueryRow("SELECT data FROM media WHERE seqno = ?", i).Scan(&b)
677 | c.Data(http.StatusOK, "video/MP2T", b)
678 | return
679 | })
680 |
681 | srv := &http.Server{
682 | Addr: fmt.Sprintf("127.0.0.1:%d", hlsPort),
683 | Handler: router,
684 | ReadTimeout: 10 * time.Second,
685 | WriteTimeout: 10 * time.Second,
686 | MaxHeaderBytes: 1 << 20,
687 | }
688 |
689 | chLocal := make(chan struct{})
690 | idleConnsClosed := make(chan struct{})
691 | defer func(){
692 | close(chLocal)
693 | }()
694 | go func() {
695 | select {
696 | case <-chLocal:
697 | }
698 | if err := srv.Shutdown(context.Background()); err != nil {
699 | log.Printf("srv.Shutdown: %v\n", err)
700 | }
701 | close(idleConnsClosed)
702 | }()
703 |
704 | // クライアントはlocalhostでなく127.0.0.1で接続すること
705 | // localhostは遅いため
706 | if err := srv.ListenAndServe(); err != http.ErrServerClosed {
707 | log.Printf("srv.ListenAndServe: %v\n", err)
708 | }
709 |
710 | <-idleConnsClosed
711 | }
712 |
713 | return
714 | }
715 |
716 | func YtComment(fileName string) (done bool, err error) {
717 | db, err := sql.Open("sqlite3", fileName)
718 | if err != nil {
719 | return
720 | }
721 | defer db.Close()
722 |
723 | youtube.WriteComment(db, fileName)
724 | return
725 | }
726 |
--------------------------------------------------------------------------------
/updatebuildno.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 | "io/ioutil"
7 | "time"
8 | "strconv"
9 | "regexp"
10 | "fmt"
11 | )
12 |
13 | func main() {
14 | f, err := os.OpenFile("src/buildno/buildno.go", os.O_RDWR, 0755)
15 | if err != nil {
16 | log.Fatal(err)
17 | }
18 | defer f.Close()
19 |
20 | if _, err := f.Seek(0, 0); err != nil {
21 | log.Fatal(err)
22 | }
23 | data, err := ioutil.ReadAll(f)
24 | if err != nil {
25 | log.Fatal(err)
26 | }
27 |
28 | var buildNo int64
29 | if ma := regexp.MustCompile(`BuildNo\s*=\s*"(\d+)"`).FindSubmatch(data); len(ma) > 0 {
30 | buildNo, err = strconv.ParseInt(string(ma[1]), 10, 64)
31 | if err != nil {
32 | log.Fatal(err)
33 | }
34 | } else {
35 | log.Fatal("BuildNo not match")
36 | }
37 | buildNo++
38 |
39 | var now = time.Now()
40 | buildDate := fmt.Sprintf("%04d%02d%02d",
41 | now.Year(),
42 | now.Month(),
43 | now.Day(),
44 | )
45 |
46 | fmt.Printf("%v.%v\n", buildDate, buildNo)
47 |
48 | if _, err := f.Seek(0, 0); err != nil {
49 | log.Fatal(err)
50 | }
51 | if err := f.Truncate(0); err != nil {
52 | log.Fatal(err)
53 | }
54 |
55 | f.WriteString(fmt.Sprintf(`
56 | package buildno
57 |
58 | var BuildDate = "%s"
59 | var BuildNo = "%d"
60 | `, buildDate, buildNo))
61 |
62 | }
63 |
--------------------------------------------------------------------------------