package live import ( "crypto/md5" cryptorand "crypto/rand" "encoding/hex" "fmt" "strconv" "time" ) type DisabledAdapter struct{} func (DisabledAdapter) Provider() string { return "disabled" } func (DisabledAdapter) CreateStream(StreamRequest) (StreamCredentials, error) { return StreamCredentials{}, ErrDisabled } func (DisabledAdapter) CreatePlayURLs(PlayRequest) (PlayURLs, error) { return PlayURLs{}, ErrDisabled } func (DisabledAdapter) QueryOnline(string) (OnlineStatus, error) { return OnlineStatus{}, ErrDisabled } // FakeAdapter is a local provider that never contacts a cloud service. type FakeAdapter struct { PlayDomain string PushDomain string AppName string } func (f FakeAdapter) Provider() string { return "fake" } func (f FakeAdapter) CreateStream(req StreamRequest) (StreamCredentials, error) { return StreamCredentials{ Provider: f.Provider(), PushURL: "fake://" + f.PushDomain + "/" + f.AppName + "/" + req.StreamName, ExpiresAt: req.ExpiresAt, }, nil } func (f FakeAdapter) CreatePlayURLs(req PlayRequest) (PlayURLs, error) { return PlayURLs{ HLS: "fake://" + f.PlayDomain + "/" + f.AppName + "/" + req.StreamName + ".m3u8", ExpiresAt: req.ExpiresAt, }, nil } func (f FakeAdapter) QueryOnline(string) (OnlineStatus, error) { return OnlineStatus{Online: true}, nil } type AliyunAdapter struct { cfg Config client *aliyunLiveClient } func NewAliyunAdapter(cfg Config) (Adapter, error) { if !cfg.AllowRealCloud || cfg.PushDomain == "" || cfg.PlayDomain == "" || cfg.AppName == "" || cfg.AuthKey == "" || cfg.PlayAuthKey == "" || cfg.AccessKeyID == "" || cfg.AccessKeySecret == "" || cfg.RegionID == "" { return nil, ErrInvalidConfig } if cfg.Protocol == "" { cfg.Protocol = "srt" } if cfg.Protocol != "srt" && cfg.Protocol != "rtmps" { return nil, ErrInvalidConfig } if cfg.SRTPort <= 0 { cfg.SRTPort = 1105 } client, err := newAliyunLiveClient(cfg) if err != nil { return nil, err } return AliyunAdapter{cfg: cfg, client: client}, nil } func (a AliyunAdapter) Provider() string { return "aliyun" } func (a AliyunAdapter) CreateStream(req StreamRequest) (StreamCredentials, error) { return StreamCredentials{Provider: a.Provider(), PushURL: a.pushURL(req.StreamName, req.ExpiresAt), ExpiresAt: req.ExpiresAt}, nil } func (a AliyunAdapter) CreatePlayURLs(req PlayRequest) (PlayURLs, error) { expires := req.ExpiresAt path := "/" + a.cfg.AppName + "/" + req.StreamName // Alibaba Live authenticates the complete playback path. HLS and FLV // therefore need independent signatures because their file extensions are // part of the URI used by the CDN auth check. hlsPath := path + ".m3u8" flvPath := path + ".flv" hlsSignature := sign(hlsPath, expires.Unix(), a.cfg.PlayAuthKey) flvSignature := sign(flvPath, expires.Unix(), a.cfg.PlayAuthKey) base := "http://" + a.cfg.PlayDomain + "/" + a.cfg.AppName + "/" + req.StreamName rtmpPath := path rtmpSignature := sign(rtmpPath, expires.Unix(), a.cfg.PlayAuthKey) rtmp := "rtmp://" + a.cfg.PlayDomain + "/" + a.cfg.AppName + "/" + req.StreamName + "?auth_key=" + rtmpSignature return PlayURLs{ HLS: base + ".m3u8?auth_key=" + hlsSignature, FLV: base + ".flv?auth_key=" + flvSignature, RTMP: rtmp, ExpiresAt: expires, }, nil } func (a AliyunAdapter) QueryOnline(streamName string) (OnlineStatus, error) { return a.client.queryOnline(streamName) } func sign(path string, expiresAt int64, key string) string { var random [4]byte if _, err := cryptorand.Read(random[:]); err != nil { panic(fmt.Sprintf("generate live URL nonce: %v", err)) } rand := strconv.FormatUint(uint64(random[0])<<24|uint64(random[1])<<16|uint64(random[2])<<8|uint64(random[3]), 10) sum := md5.Sum([]byte(fmt.Sprintf("%s-%d-%s-0-%s", path, expiresAt, rand, key))) return fmt.Sprintf("%d-%s-0-%s", expiresAt, rand, hex.EncodeToString(sum[:])) } func (a AliyunAdapter) pushURL(streamName string, expires time.Time) string { path := "/" + a.cfg.AppName + "/" + streamName auth := sign(path, expires.Unix(), a.cfg.AuthKey) if a.cfg.Protocol == "srt" { return "srt://" + a.cfg.PushDomain + ":" + strconv.Itoa(a.cfg.SRTPort) + "?streamid=#!::h=" + a.cfg.PushDomain + ",r=" + path + "?auth_key=" + auth + ",m=publish" } return "rtmps://" + a.cfg.PushDomain + "/" + a.cfg.AppName + "/" + streamName + "?auth_key=" + auth }