├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.sh └── go ├── .gitignore ├── database.go ├── fifocache.go ├── go.mod ├── go.sum └── main.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ blinkinglight ] 2 | patreon: streamingriver 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # video stream recorder 2 | hls live video stream recorder / vod service provider 3 | 4 | for latest version check https://github.com/streamingriver/video-stream-recorder/releases 5 | 6 | downloader: 7 | ``` 8 | ./vsr --url http://full/url/with/m3u8 --tail 24 9 | 10 | url = m3u8 url to process 11 | tail = how many hours keep 12 | ``` 13 | 14 | wath live (last downloaded files): 15 | ``` 16 | http://youripaddress:8080/live/stream.m3u8 17 | ``` 18 | 19 | 20 | watch recorder stream via your favorite video player: 21 | ``` 22 | http://youripaddress:8080/start//300/stream.m3u8 23 | 24 | or with normal full date with seconds: 25 | 26 | http://youripaddress:8080/start/20191225150000/300/vod.m3u8 27 | 28 | ``` 29 | replace "{timestamp}" to localtime unix timestamp (https://www.epochconverter.com/) 30 | replace 300 with how long video is (if you dont know, keep 300) 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove old binaries (if any) 4 | rm -rf dist 5 | 6 | pushd go 7 | 8 | env GOOS=linux GOARCH=386 go build -ldflags="-s -w" -o "../dist/vsr_linux_i386" # Linux i386 9 | env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o "../dist/vsr_linux_x86_64" # Linux 64bit 10 | env GOOS=linux GOARCH=arm GOARM=5 go build -ldflags="-s -w" -o "../dist/vsr_linux_arm" # Linux armv5/armel/arm (it also works on armv6) 11 | env GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w" -o "../dist/vsr_linux_armhf" # Linux armv7/armhf 12 | env GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o "../dist/vsr_linux_aarch64" # Linux armv8/aarch64 13 | env GOOS=freebsd GOARCH=amd64 go build -ldflags="-s -w" -o "../dist/vsr_freebsd_x86_64" # FreeBSD 64bit 14 | env GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o "../dist/vsr_darwin_x86_64" # Darwin 64bit 15 | # env GOOS=windows GOARCH=386 go build -ldflags="-s -w" -o "../dist/vsr_windows_i386.exe" # Windows 32bit 16 | # env GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o "../dist/vsr_windows_x86_64.exe" # Windows 64bit 17 | 18 | popd 19 | -------------------------------------------------------------------------------- /go/.gitignore: -------------------------------------------------------------------------------- 1 | files 2 | db.db 3 | -------------------------------------------------------------------------------- /go/database.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "os" 10 | "strconv" 11 | "time" 12 | 13 | "go.etcd.io/bbolt" 14 | ) 15 | 16 | type DatabaseItem struct { 17 | ID uint64 18 | Name string 19 | Len float64 20 | T int64 21 | } 22 | 23 | var ( 24 | database *bbolt.DB 25 | dbbucket = []byte("main") 26 | ) 27 | 28 | func database_init() { 29 | var err error 30 | database, err = bbolt.Open("./db.db", 0755, nil) 31 | if err != nil { 32 | log.Fatalf("Open database error: %v", err) 33 | } 34 | database.Update(func(tx *bbolt.Tx) error { 35 | tx.CreateBucketIfNotExists(dbbucket) 36 | return nil 37 | }) 38 | } 39 | 40 | func database_store(item *DatabaseItem) { 41 | 42 | database.Update(func(tx *bbolt.Tx) error { 43 | bucket := tx.Bucket(dbbucket) 44 | ID, _ := bucket.NextSequence() 45 | item.ID = ID 46 | encoded, _ := json.Marshal(item) 47 | bucket.Put(itob(int(item.T)), encoded) 48 | return nil 49 | 50 | }) 51 | } 52 | 53 | func database_last_5() []*DatabaseItem { 54 | 55 | var rt []*DatabaseItem 56 | var tmp []*DatabaseItem 57 | 58 | database.View(func(tx *bbolt.Tx) error { 59 | bucket := tx.Bucket(dbbucket) 60 | cursor := bucket.Cursor() 61 | 62 | n := 0 63 | for k, v := cursor.Last(); k != nil; k, v = cursor.Prev() { 64 | if n >= 7 { 65 | break 66 | } 67 | n++ 68 | var item *DatabaseItem 69 | err := json.Unmarshal(v, &item) 70 | if err != nil { 71 | log.Printf("error %v", err) 72 | continue 73 | } 74 | tmp = append(tmp, item) 75 | 76 | } 77 | return nil 78 | }) 79 | 80 | for i := len(tmp) - 1; i > 0; i-- { 81 | rt = append(rt, tmp[i]) 82 | } 83 | 84 | return rt 85 | } 86 | 87 | func database_get(s, l string) []*DatabaseItem { 88 | 89 | si, err := strconv.Atoi(s) 90 | if err != nil { 91 | log.Printf("database_get error: %v", err) 92 | return nil 93 | } 94 | li, err := strconv.Atoi(l) 95 | if err != nil { 96 | log.Printf("database_get error: %v", err) 97 | return nil 98 | } 99 | 100 | var rt []*DatabaseItem 101 | 102 | database.View(func(tx *bbolt.Tx) error { 103 | start := itob(si * 1000000000) 104 | end := itob(si + li*60*1000000000) 105 | 106 | bucket := tx.Bucket(dbbucket) 107 | cursor := bucket.Cursor() 108 | 109 | for k, v := cursor.Seek(start); k != nil; k, v = cursor.Next() { 110 | if bytes.Compare(k, end) < 0 { 111 | return nil 112 | } 113 | var item *DatabaseItem 114 | err := json.Unmarshal(v, &item) 115 | if err != nil { 116 | log.Printf("error %v", err) 117 | continue 118 | } 119 | rt = append(rt, item) 120 | 121 | } 122 | return nil 123 | }) 124 | 125 | return rt 126 | } 127 | 128 | func database_worker() { 129 | for { 130 | current := time.Now().UnixNano() - int64(*flagTail*60*60*int(time.Nanosecond)) 131 | err := database.Update(func(tx *bbolt.Tx) error { 132 | bucket := tx.Bucket(dbbucket) 133 | cursor := bucket.Cursor() 134 | 135 | for k, v := cursor.First(); k != nil; k, v = cursor.Next() { 136 | _ = v 137 | if bytes.Compare(k, itob(int(current))) < 0 { 138 | return nil 139 | } 140 | var item DatabaseItem 141 | err := json.Unmarshal(v, &item) 142 | if err != nil { 143 | return err 144 | } 145 | log.Printf("removing ./files/%v", item.Name) 146 | os.RemoveAll(fmt.Sprintf("./files/%v", item.Name)) 147 | bucket.Delete(k) 148 | 149 | } 150 | return nil 151 | }) 152 | if err != nil { 153 | log.Printf("error %v", err) 154 | } 155 | time.Sleep(time.Second) 156 | } 157 | } 158 | 159 | func itob(v int) []byte { 160 | b := make([]byte, 8) 161 | binary.BigEndian.PutUint64(b, uint64(v)) 162 | return b 163 | } 164 | -------------------------------------------------------------------------------- /go/fifocache.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | ) 7 | 8 | var ( 9 | fifomap = make(map[string]struct{}) 10 | fifolist = list.New() 11 | fifomu = &sync.RWMutex{} 12 | ) 13 | 14 | func cache_set(url string) bool { 15 | fifomu.Lock() 16 | defer fifomu.Unlock() 17 | 18 | _, ok := fifomap[url] 19 | if ok { 20 | return false 21 | } 22 | 23 | fifomap[url] = struct{}{} 24 | fifolist.PushFront(url) 25 | 26 | for fifolist.Len() > 10 { 27 | item := fifolist.Back() 28 | delete(fifomap, item.Value.(string)) 29 | fifolist.Remove(item) 30 | } 31 | return true 32 | } 33 | -------------------------------------------------------------------------------- /go/go.mod: -------------------------------------------------------------------------------- 1 | module vsr 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gorilla/mux v1.7.4 7 | github.com/grafov/m3u8 v0.11.1 8 | go.etcd.io/bbolt v1.3.3 9 | ) 10 | -------------------------------------------------------------------------------- /go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 2 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 3 | github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA= 4 | github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= 5 | go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= 6 | go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 7 | -------------------------------------------------------------------------------- /go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "time" 13 | 14 | "github.com/gorilla/mux" 15 | 16 | "github.com/grafov/m3u8" 17 | ) 18 | 19 | var ( 20 | flagURL = flag.String("url", "", "url to fetch") 21 | flagTail = flag.Int("tail", 24, "how much hours to keep") 22 | flagHost = flag.String("host", "http://localhos:8080", "add host to m3u8") 23 | flagBindTo = flag.String("bind-to", ":8080", "bind to ip:port") 24 | 25 | flagDebug = flag.Bool("debug", false, "") 26 | 27 | flagVersion = flag.Bool("version", false, "show version") 28 | ) 29 | 30 | func main() { 31 | flag.Parse() 32 | 33 | if *flagVersion { 34 | println("v0.2") 35 | return 36 | } 37 | 38 | if *flagURL == "" { 39 | log.Printf("Set url to fetch: ./app --url [url-here]") 40 | return 41 | } 42 | 43 | err := os.MkdirAll("./files", 0755) 44 | if err != nil { 45 | log.Fatalf("mkdir fail: %v", err) 46 | } 47 | 48 | database_init() 49 | 50 | if !*flagDebug { 51 | go fetcher() 52 | } 53 | 54 | go database_worker() 55 | 56 | router := mux.NewRouter() 57 | 58 | router.HandleFunc("/live/stream.m3u8", func(w http.ResponseWriter, r *http.Request) { 59 | 60 | utc := r.URL.Query().Get("utc") 61 | if utc != "" { 62 | // w.Header().Set("Location", "/") 63 | vod1(w, r) 64 | return 65 | } 66 | 67 | out := "#EXTM3U\n" 68 | out += "#EXT-X-TARGETDURATION:2\n" 69 | out += "#EXT-X-VERSION:4\n" 70 | 71 | items := database_last_5() 72 | 73 | if len(items) <= 0 { 74 | return 75 | } 76 | 77 | last := items[len(items)-1] 78 | 79 | out += fmt.Sprintf("#EXT-X-MEDIA-SEQUENCE:%d\n", last.ID) 80 | 81 | for _, item := range items { 82 | out += fmt.Sprintf("#EXTINF:%f\n", item.Len) 83 | out += fmt.Sprintf("%s\n", item.Name) 84 | } 85 | 86 | w.Header().Set("Content-Type", "application/vnd.apple.mpegurl") 87 | w.Header().Set("Content-Lenght", fmt.Sprintf("%d", len(out))) 88 | w.Write([]byte(out)) 89 | 90 | }) 91 | 92 | router.HandleFunc("/live/{ts:.+}", serve_ts_file) 93 | 94 | router.HandleFunc("/start/{start}/{limit}/vod.m3u8", func(w http.ResponseWriter, r *http.Request) { 95 | 96 | varz := mux.Vars(r) 97 | 98 | out := "#EXTM3U\n" 99 | out += "#EXT-X-PLAYLIST-TYPE:VOD\n" 100 | out += "#EXT-X-TARGETDURATION:20\n" 101 | out += "#EXT-X-VERSION:4\n" 102 | out += "#EXT-X-MEDIA-SEQUENCE:0\n" 103 | 104 | t, err := time.Parse("20060102150405", varz["start"]) 105 | if err != nil { 106 | perr, ok := err.(*time.ParseError) 107 | log.Printf("error %v %v", perr, ok) 108 | return 109 | } 110 | start := fmt.Sprintf("%d", t.Unix()) 111 | println(start) 112 | 113 | items := database_get(start, varz["limit"]) 114 | 115 | for _, item := range items { 116 | out += fmt.Sprintf("#EXTINF:%f\n", item.Len) 117 | out += fmt.Sprintf("%s\n", item.Name) 118 | } 119 | 120 | out += "#EXT-X-ENDLIST\n" 121 | 122 | w.Header().Set("Content-Type", "application/vnd.apple.mpegurl") 123 | w.Header().Set("Content-Lenght", fmt.Sprintf("%d", len(out))) 124 | w.Write([]byte(out)) 125 | }) 126 | 127 | router.HandleFunc("/start/{start}/{limit}/stream.m3u8", vod1) 128 | 129 | router.HandleFunc("/start/{start}/{limit}/{ts:.+}", serve_ts_file) 130 | 131 | log.Printf("Starting server on %v", *flagBindTo) 132 | log.Fatal(http.ListenAndServe(*flagBindTo, router)) 133 | } 134 | 135 | func vod1(w http.ResponseWriter, r *http.Request) { 136 | 137 | varz := mux.Vars(r) 138 | 139 | utc := r.URL.Query().Get("utc") 140 | if utc != "" { 141 | varz["start"] = utc 142 | varz["limit"] = "300" 143 | } 144 | 145 | out := "#EXTM3U\n" 146 | out += "#EXT-X-PLAYLIST-TYPE:VOD\n" 147 | out += "#EXT-X-TARGETDURATION:20\n" 148 | out += "#EXT-X-VERSION:4\n" 149 | out += "#EXT-X-MEDIA-SEQUENCE:0\n" 150 | 151 | items := database_get(varz["start"], varz["limit"]) 152 | 153 | for _, item := range items { 154 | out += fmt.Sprintf("#EXTINF:%f\n", item.Len) 155 | out += fmt.Sprintf("%s\n", item.Name) 156 | } 157 | 158 | out += "#EXT-X-ENDLIST\n" 159 | 160 | w.Header().Set("Content-Type", "application/vnd.apple.mpegurl") 161 | w.Header().Set("Content-Lenght", fmt.Sprintf("%d", len(out))) 162 | w.Write([]byte(out)) 163 | } 164 | 165 | func serve_ts_file(w http.ResponseWriter, r *http.Request) { 166 | varz := mux.Vars(r) 167 | w.Header().Set("Content-Type", "text/vnd.trolltech.linguist") 168 | b, err := ioutil.ReadFile(fmt.Sprintf("./files/%s", varz["ts"])) 169 | if err != nil { 170 | log.Printf("error %v", err) 171 | return 172 | } 173 | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(b))) 174 | w.Write(b) 175 | 176 | } 177 | 178 | func fetcher() { 179 | mainurl, _ := url.Parse(*flagURL) 180 | for { 181 | start_at: 182 | b := fetch(mainurl.String()) 183 | buf := bytes.NewBuffer(b) 184 | pl, pt, err := m3u8.Decode(*buf, true) 185 | if err != nil { 186 | log.Printf("fetcher error: %v %v", mainurl.String(), err) 187 | time.Sleep(1 * time.Second) 188 | continue 189 | } 190 | if pt == m3u8.MASTER { 191 | masterpl := pl.(*m3u8.MasterPlaylist) 192 | for _, variant := range masterpl.Variants { 193 | mainurl, _ = mainurl.Parse(variant.URI) 194 | log.Printf("%v", mainurl.String()) 195 | goto start_at 196 | } 197 | 198 | } else if pt == m3u8.MEDIA { 199 | mediapl := pl.(*m3u8.MediaPlaylist) 200 | for _, segment := range mediapl.Segments { 201 | if segment == nil { 202 | continue 203 | } 204 | fetchurl, _ := mainurl.Parse(segment.URI) 205 | fetchurl.RawQuery = mainurl.RawQuery 206 | if cache_set(fetchurl.String()) { 207 | log.Printf("%v", fetchurl.String()) 208 | currenttime := time.Now().UnixNano() 209 | item := &DatabaseItem{ 210 | Name: fmt.Sprintf("%v.ts", currenttime), 211 | Len: segment.Duration, 212 | T: currenttime, 213 | } 214 | database_store(item) 215 | 216 | b := fetch(fetchurl.String()) 217 | if b != nil { 218 | err := ioutil.WriteFile("./files/"+item.Name, b, 0755) 219 | if err != nil { 220 | log.Printf("error on write file to fs %v", err) 221 | continue 222 | } 223 | } 224 | } 225 | } 226 | } 227 | time.Sleep(3 * time.Second) 228 | } 229 | } 230 | 231 | func fetch(url string) []byte { 232 | hc := http.Client{Timeout: 10 * time.Second} 233 | 234 | request, _ := http.NewRequest("GET", url, nil) 235 | request.Header.Set("User-Agent", "iptv/1.0") 236 | 237 | response, err := hc.Do(request) 238 | if err != nil { 239 | log.Printf("fetch error %v %v", url, err) 240 | return nil 241 | } 242 | defer response.Body.Close() 243 | if response.StatusCode/100 != 2 { 244 | log.Printf("Invalid response code %v %v", url, response.StatusCode) 245 | return nil 246 | } 247 | b, err := ioutil.ReadAll(response.Body) 248 | if err != nil { 249 | return nil 250 | } 251 | return b 252 | } 253 | --------------------------------------------------------------------------------