├── .gitignore ├── stopbmitune.sh ├── prebmitune.sh ├── bmitune.sh ├── go.mod ├── main.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | /androidhdmi-for-channels 2 | -------------------------------------------------------------------------------- /stopbmitune.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | STOP="am force-stop com.google.android.youtube.tvunplugged; sleep 2" 3 | 4 | #Stop Video 5 | adb shell $STOP 6 | adb shell input keyevent KEYCODE_SLEEP 7 | -------------------------------------------------------------------------------- /prebmitune.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | CONNECT="connect 192.168.1.171" 3 | WAKE="input keyevent KEYCODE_WAKEUP" 4 | HOME="input keyevent KEYCODE_HOME" 5 | 6 | adb $CONNECT 7 | adb $CONNECT 8 | adb $CONNECT 9 | adb shell $WAKE 10 | adb shell $WAKE 11 | #adb shell $WAKE 12 | adb shell $HOME; sleep 2 13 | #adb shell am start com.google.android.youtube.tvunplugged; sleep 2 14 | #adb shell am force-stop com.google.android.youtube.tvunplugged 15 | -------------------------------------------------------------------------------- /bmitune.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | HOME="input keyevent KEYCODE_HOME; sleep 1" 3 | PRIME1="input keyevent 19 19 19 19; sleep 1; input keyevent 21 21 21; sleep 1; input keyevent 22; sleep 1; input keyevent 23; sleep 1" 4 | PRIME2="input keyevent 19; sleep 1" 5 | PRIME3="input keyevent --longpress 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67" 6 | SEARCH1="input keyevent 66; sleep 2" 7 | SEARCH2="input keyevent 66" 8 | 9 | #USA 10 | if [ $1 = "111" ];then 11 | adb shell $HOME 12 | adb shell $PRIME1 13 | adb shell $PRIME2 14 | #adb shell $PRIME3 15 | adb shell input text "stream\ usa\ channel\ on\ YouTube\ TV" 16 | adb shell $SEARCH1 17 | adb shell $SEARCH2 18 | fi 19 | 20 | #SYFY 21 | if [ $1 = "135" ];then 22 | adb shell $HOME 23 | adb shell $PRIME1 24 | adb shell $PRIME2 25 | #adb shell $PRIME3 26 | adb shell input text "stream\ syfy\ on\ YouTube\ TV" 27 | adb shell $SEARCH1 28 | adb shell $SEARCH2 29 | fi 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module androidhdmi-for-channels 2 | 3 | go 1.18 4 | 5 | require github.com/gin-gonic/gin v1.9.0 6 | 7 | require ( 8 | github.com/bytedance/sonic v1.8.0 // indirect 9 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 10 | github.com/gin-contrib/sse v0.1.0 // indirect 11 | github.com/go-playground/locales v0.14.1 // indirect 12 | github.com/go-playground/universal-translator v0.18.1 // indirect 13 | github.com/go-playground/validator/v10 v10.11.2 // indirect 14 | github.com/goccy/go-json v0.10.0 // indirect 15 | github.com/json-iterator/go v1.1.12 // indirect 16 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 17 | github.com/leodido/go-urn v1.2.1 // indirect 18 | github.com/mattn/go-isatty v0.0.17 // indirect 19 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 20 | github.com/modern-go/reflect2 v1.0.2 // indirect 21 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 22 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 23 | github.com/ugorji/go/codec v1.2.9 // indirect 24 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 25 | golang.org/x/crypto v0.5.0 // indirect 26 | golang.org/x/net v0.7.0 // indirect 27 | golang.org/x/sys v0.5.0 // indirect 28 | golang.org/x/text v0.7.0 // indirect 29 | google.golang.org/protobuf v1.28.1 // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "strconv" 12 | "sync" 13 | "time" 14 | 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | var ( 19 | tunerLock sync.Mutex 20 | 21 | tuners = []tuner{ 22 | { 23 | url: "http://192.168.1.168/0.ts", 24 | pre: "/opt/opendct/prebmitune.sh", 25 | start: "/opt/opendct/bmitune.sh", 26 | stop: "/opt/opendct/stopbmitune.sh", 27 | }, 28 | { 29 | url: "http://192.168.1.169/main", 30 | pre: "/opt/opendct/prebmituneb.sh", 31 | start: "/opt/opendct/bmituneb.sh", 32 | stop: "/opt/opendct/stopbmituneb.sh", 33 | }, 34 | } 35 | ) 36 | 37 | type tuner struct { 38 | url string 39 | pre, start, stop string 40 | active bool 41 | } 42 | 43 | type reader struct { 44 | io.ReadCloser 45 | t *tuner 46 | channel string 47 | started bool 48 | } 49 | 50 | func init() { 51 | transport := http.DefaultTransport.(*http.Transport).Clone() 52 | transport.ResponseHeaderTimeout = 5 * time.Second 53 | transport.DialContext = (&net.Dialer{ 54 | Timeout: 5 * time.Second, 55 | }).DialContext 56 | http.DefaultClient.Transport = transport 57 | } 58 | 59 | func (r *reader) Read(p []byte) (int, error) { 60 | if !r.started { 61 | r.started = true 62 | go func() { 63 | if err := execute(r.t.pre); err != nil { 64 | log.Printf("[ERR] Failed to run pre script: %v", err) 65 | return 66 | } 67 | if err := execute(r.t.start, r.channel); err != nil { 68 | log.Printf("[ERR] Failed to run start script: %v", err) 69 | return 70 | } 71 | }() 72 | } 73 | return r.ReadCloser.Read(p) 74 | } 75 | 76 | func (r *reader) Close() error { 77 | if err := execute(r.t.stop); err != nil { 78 | log.Printf("[ERR] Failed to run stop script: %v", err) 79 | } 80 | tunerLock.Lock() 81 | r.t.active = false 82 | tunerLock.Unlock() 83 | return r.ReadCloser.Close() 84 | } 85 | 86 | func execute(args ...string) error { 87 | t0 := time.Now() 88 | log.Printf("Running %v", args) 89 | cmd := exec.Command(args[0], args[1:]...) 90 | cmd.Stdout = os.Stdout 91 | cmd.Stderr = os.Stderr 92 | err := cmd.Run() 93 | log.Printf("Finished running %v in %v", args[0], time.Since(t0)) 94 | return err 95 | } 96 | 97 | func tune(idx, channel string) (io.ReadCloser, error) { 98 | tunerLock.Lock() 99 | defer tunerLock.Unlock() 100 | 101 | var t *tuner 102 | log.Printf("tune for %v %v", idx, channel) 103 | if idx == "" || idx == "auto" { 104 | for i, ti := range tuners { 105 | if ti.active { 106 | continue 107 | } 108 | t = &tuners[i] 109 | break 110 | } 111 | } else { 112 | i, _ := strconv.Atoi(idx) 113 | if i < len(tuners) && i >= 0 { 114 | t = &tuners[i] 115 | } 116 | } 117 | if t == nil { 118 | return nil, fmt.Errorf("tuner not available") 119 | } 120 | 121 | resp, err := http.Get(t.url) 122 | if err != nil { 123 | log.Printf("[ERR] Failed to fetch source: %v", err) 124 | return nil, err 125 | } else if resp.StatusCode != 200 { 126 | log.Printf("[ERR] Failed to fetch source: %v", resp.Status) 127 | return nil, fmt.Errorf("invalid response: %v", resp.Status) 128 | } 129 | 130 | t.active = true 131 | return &reader{ 132 | ReadCloser: resp.Body, 133 | channel: channel, 134 | t: t, 135 | }, nil 136 | } 137 | 138 | func run() error { 139 | r := gin.Default() 140 | r.SetTrustedProxies(nil) 141 | r.GET("/play/tuner:tuner/:channel", func(c *gin.Context) { 142 | tuner := c.Param("tuner") 143 | channel := c.Param("channel") 144 | 145 | c.Header("Transfer-Encoding", "identity") 146 | c.Header("Content-Type", "video/mp2t") 147 | c.Writer.WriteHeaderNow() 148 | c.Writer.Flush() 149 | 150 | reader, err := tune(tuner, channel) 151 | if err != nil { 152 | c.JSON(500, gin.H{"error": err.Error()}) 153 | return 154 | } 155 | 156 | defer func() { 157 | reader.Close() 158 | }() 159 | 160 | io.Copy(c.Writer, reader) 161 | }) 162 | return r.Run(":7654") 163 | } 164 | 165 | func main() { 166 | err := run() 167 | if err != nil { 168 | panic(err) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 2 | github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= 3 | github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 4 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 5 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 11 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 12 | github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= 13 | github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= 14 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 15 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 16 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 17 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 18 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 19 | github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= 20 | github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= 21 | github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= 22 | github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 23 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 24 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 25 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 26 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 27 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 28 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 29 | github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= 30 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 31 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 32 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 33 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 34 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 35 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 36 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 37 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 38 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 39 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 40 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 41 | github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= 42 | github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 43 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 44 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 45 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 46 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 47 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 48 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 49 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 50 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 51 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 52 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 53 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 54 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 55 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 56 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 57 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 58 | github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= 59 | github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 60 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= 61 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 62 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 63 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 64 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 65 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 66 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 68 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 70 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 71 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 72 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 74 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 75 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 76 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 77 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 78 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 79 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 80 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 81 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 82 | --------------------------------------------------------------------------------