Initial commit

This commit is contained in:
Donny
2019-04-22 20:46:32 +08:00
commit 49ab8aadd1
25441 changed files with 4055000 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
# Owner
liaodada
# Author
liaodada
zhuxiang
sunyuanyuan
# Reviewer
liaodada
zhuxiang
sunyuanyuan

14
app/service/video/OWNERS Normal file
View File

@@ -0,0 +1,14 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- liaodada
- sunyuanyuan
- zhuxiang
labels:
- new-project
- service
- video
reviewers:
- liaodada
- sunyuanyuan
- zhuxiang

View File

@@ -0,0 +1,4 @@
# v1.0.0
1. 上线功能基本功能
2. 上线tidb
3. 集成main-stream,优先取main-stream中origin字段

View File

@@ -0,0 +1,12 @@
# Owner
liaodada
# Author
liaodada
sunyuanyuan
zhuxiang
# Reviewer
liaodada
sunyuanyuan
zhuxiang

View File

@@ -0,0 +1,15 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- liaodada
- sunyuanyuan
- zhuxiang
labels:
- service
- service/video/stream-mng
options:
no_parent_owners: true
reviewers:
- liaodada
- sunyuanyuan
- zhuxiang

View File

@@ -0,0 +1,12 @@
# stream-mng-service
# 项目简介
1. 迁移stream-service的流服务
# 编译环境
1. golang 1.9+以上版本编译执行。
# 依赖包
1.公共包go-common
# 编译执行

View File

@@ -0,0 +1 @@
# HTTP API文档

View File

@@ -0,0 +1,56 @@
load(
"@io_bazel_rules_go//proto:def.bzl",
"go_proto_library",
)
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
proto_library(
name = "v1_proto",
srcs = ["api.proto"],
tags = ["automanaged"],
deps = ["@gogo_special_proto//github.com/gogo/protobuf/gogoproto"],
)
go_proto_library(
name = "v1_go_proto",
compilers = ["@io_bazel_rules_go//proto:gogofast_grpc"],
importpath = "go-common/app/service/video/stream-mng/api/v1",
proto = ":v1_proto",
tags = ["automanaged"],
deps = ["@com_github_gogo_protobuf//gogoproto:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = [],
embed = [":v1_go_proto"],
importpath = "go-common/app/service/video/stream-mng/api/v1",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//vendor/golang.org/x/net/context:go_default_library",
"@com_github_gogo_protobuf//gogoproto:go_default_library",
"@com_github_gogo_protobuf//proto:go_default_library",
"@org_golang_google_grpc//:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,48 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
proto_library(
name = "v1_proto",
srcs = ["api.proto"],
tags = ["automanaged"],
deps = ["@gogo_special_proto//github.com/gogo/protobuf/gogoproto"],
)
go_proto_library(
name = "v1_go_proto",
compilers = ["@io_bazel_rules_go//proto:gogofast_grpc"],
importpath = "go-common/app/service/video/stream-mng/api/v1",
proto = ":v1_proto",
tags = ["automanaged"],
deps = ["@com_github_gogo_protobuf//gogoproto:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = ["client.go"],
embed = [":v1_go_proto"],
importpath = "go-common/app/service/video/stream-mng/api/v1",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/net/rpc/warden:go_default_library",
"@com_github_gogo_protobuf//gogoproto:go_default_library",
"@com_github_gogo_protobuf//proto:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_x_net//context:go_default_library",
],
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,376 @@
syntax = "proto3";
package video.live.streamng.v1;
option go_package = "v1";
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
// EmptyStruct 空的message对应真实service只返回error没有具体返回值
message EmptyStruct {
}
// ChangeSrcReq 切换上行
message ChangeSrcReq {
// 房间号
int64 room_id = 1;
// src
int64 src = 2;
// 来源平台
string source = 3;
// 操作人
string operate_name = 4;
// 原因
string reason = 5;
}
// ClearStreamStatusReq 清理互推
message ClearStreamStatusReq {
// 房间号
int64 room_id = 1;
}
// CloseReply CloseReply
message CloseReply {
}
// CloseReq CloseReq
message CloseReq {
}
// CreateOfficalStreamReply 创建流
message CreateOfficalStreamReply {
// 是否成功
bool success = 1;
}
// CreateOfficalStreamReq 创建流
message CreateOfficalStreamReq {
// 流名
string stream_name = 1;
// key
string key = 2;
// 房间号
int64 room_id = 3;
// 是否线下测试
string debug = 4;
// 线下测试必传
int64 uid = 5;
}
// AdapterStream 上行流信息
message AdapterStream {
// src
int64 src = 1;
// 房间号
int64 room_id = 2 [(gogoproto.jsontag) = "room_id"];
// 上行
int64 up_rank = 3 [(gogoproto.jsontag) = "up_rank"];
// 上行名称
string src_name = 4 [(gogoproto.jsontag) = "src_name"];
}
// GetAdapterStreamByStreamNameReply 适配PHP代码
message GetAdapterStreamByStreamNameReply {
// 流名map流具体信息
map<string, AdapterStream> list = 1;
}
// GetAdapterStreamByStreamNameReq 适配PHP代码
message GetAdapterStreamByStreamNameReq {
// 流名
string stream_names = 1 [(gogoproto.jsontag) = "stream_names"];
}
// LineList 线路信息
message LineList {
// 上行
int64 src = 1;
// 是否被使用
bool use = 2;
// 描述
string desc = 3;
}
// GetLineListByRoomIDReply 获取线路信息
message GetLineListByRoomIDReply {
// 线路信息, 多条
repeated LineList list = 1;
}
// GetLineListByRoomIDReq 获取线路信息
message GetLineListByRoomIDReq {
// 房间号
int64 room_id = 1;
}
// GetMultiScreenShotReply 获取批量截图
message GetMultiScreenShotReply {
// 房间号map截图地址
map<int64, string> list = 1;
}
// GetMultiScreenShotReq 获取批量截图
message GetMultiScreenShotReq {
// 房间号
string room_ids = 1;
// 时间戳
int64 ts = 2;
// channel 是否为鉴黄
string channel = 3;
}
// GetMultiStreamInfoReply 批量获取房间信息
message GetMultiStreamInfoReply {
// code
int32 code = 1;
// message
string message = 2;
// 房间号map流详细信息
map<uint32, StreamFullInfo> data = 3;
}
// GetMultiStreamInfoReq 批量获取房间信息
message GetMultiStreamInfoReq {
// 房间号
repeated uint32 room_ids = 1;
}
// GetOriginScreenShotPicReply 获取原图
message GetOriginScreenShotPicReply {
// 房间号map截图地址
map<int64, string> list = 1;
}
// GetOriginScreenShotPicReq 获取原图
message GetOriginScreenShotPicReq {
// 房间号
string room_ids = 1;
// 时间戳
int64 ts = 2;
}
// GetRoomIDByStreamNameReply 获取房间号
message GetRoomIDByStreamNameReply {
// 房间号
int64 room_id = 1;
}
// GetRoomIDByStreamNameReq 获取房间号
message GetRoomIDByStreamNameReq {
// 流名
string stream_name = 1;
}
// GetSingleScreeShotReply 获取单个截图
message GetSingleScreeShotReply {
// 截图列表
repeated string list = 1;
}
// GetSingleScreeShotReq 获取单个截图
message GetSingleScreeShotReq {
// 房间号
int64 room_id = 1;
// 开始时间
string start_time = 2;
// 结束时间
string end_time = 3;
// channel
string channel = 4;
}
// RoomSrcCheck 上行
message RoomSrcCheck {
// 上行
int64 src = 1;
// 是否被选择, 1或者0
int32 checked = 2;
// 上行名称
string desc = 3;
}
// GetSrcByRoomIDReply 获取上行信息
message GetSrcByRoomIDReply {
// 上行列表
repeated RoomSrcCheck list = 1;
}
// GetSrcByRoomIDReq 获取上行信息
message GetSrcByRoomIDReq {
// 房间号
int64 room_id = 1;
}
// GetStreamInfoReply 获取单个流信息
message GetStreamInfoReply {
// code
int32 code = 1;
// message
string message = 2;
// 流详细信息
StreamFullInfo data = 3;
}
// GetStreamInfoReq 获取单个流信息
message GetStreamInfoReq {
// 房间号
uint32 room_id = 1;
// 流名
string stream_name = 2;
}
// GetStreamLastTimeReply 获取最后推流时间
message GetStreamLastTimeReply {
// 最后推流时间, -1 正在推流
int64 last_time = 1;
}
// GetStreamLastTimeReq 获取最后推流时间
message GetStreamLastTimeReq {
// 房间号
int64 room_id = 1;
}
// GetStreamNameByRoomIDReply 获取流名
message GetStreamNameByRoomIDReply {
// 流名
string stream_name = 1;
}
// GetStreamNameByRoomIDReq 获取流名
message GetStreamNameByRoomIDReq {
// 房间号
int64 room_id = 1;
}
// GetUpStreamRtmpReply 获取上行推流地址
message GetUpStreamRtmpReply {
// 上行推流地址
UpStreamRtmp up = 1;
}
// GetUpStreamRtmpReq 获取上行推流地址
message GetUpStreamRtmpReq {
// 房间号
int64 room_id = 1;
// 免流
string free_flow = 2;
// ip
string ip = 3;
// 所在区域
int64 area_id = 4;
// 关注数
int32 attentions = 5;
// uid
int64 uid = 6;
// 平台
string platform = 7;
}
// StreamBase 流基本信息
message StreamBase {
// 流名
string stream_name = 1;
// 默认上行
uint32 default_upstream = 2 [(gogoproto.jsontag) = "default_upstream"];
// 正在推流上行
uint32 origin = 3;
// 转推cdn
repeated uint32 forward = 4;
// 主流1或者备用流2
uint32 type = 5;
// 推流码
string key = 6;
// options
uint32 options = 7;
}
// StreamFullInfo 流信息完整结构
message StreamFullInfo {
// 房间号
uint32 room_id = 1;
// 是否为冷热流
uint32 hot = 2;
// 流列表
repeated StreamBase list = 3;
}
// PingReply PingReply
message PingReply {
}
// PingReq PingReq
message PingReq {
}
// UpStreamRtmp 上行推流地址详细信息
message UpStreamRtmp {
// 推流地址
string addr = 1;
// 推流码
string code = 2;
// new link
string new_link = 3;
}
// StreamCutReq 切流
message StreamCutReq {
// 房间号
int64 room_id = 1;
// 切流时间
int64 cut_time = 2;
}
// TimeTime 时间
message TimeTime {
}
message CheckLiveStreamReq {
repeated int64 room_id = 1;
}
message CheckLiveStreamReply {
map<int64, bool> list = 1;
}
// Stream service
service Stream {
// GetSingleScreeShot
rpc GetSingleScreeShot (GetSingleScreeShotReq) returns (GetSingleScreeShotReply);
// GetMultiScreenShot
rpc GetMultiScreenShot (GetMultiScreenShotReq) returns (GetMultiScreenShotReply);
// GetOriginScreenShotPic
rpc GetOriginScreenShotPic (GetOriginScreenShotPicReq) returns (GetOriginScreenShotPicReply);
// CreateOfficeStream 创建正式流
rpc CreateOfficalStream (CreateOfficalStreamReq) returns (CreateOfficalStreamReply);
// GetStreamInfo 获取单个流信息
rpc GetStreamInfo (GetStreamInfoReq) returns (GetStreamInfoReply);
// GetMultiStreamInfo 批量获取流信息
rpc GetMultiStreamInfo (GetMultiStreamInfoReq) returns (GetMultiStreamInfoReply);
// ChangeSrc 切换cdn
rpc ChangeSrc (ChangeSrcReq) returns (EmptyStruct);
// GetStreamLastTime 得到流到最后推流时间;主流的推流时间up_rank = 1
rpc GetStreamLastTime (GetStreamLastTimeReq) returns (GetStreamLastTimeReply);
// GetStreamNameByRoomID 需要考虑备用流 + 考虑短号
rpc GetStreamNameByRoomID (GetStreamNameByRoomIDReq) returns (GetStreamNameByRoomIDReply);
// GetRoomIDByStreamName 查询房间号
rpc GetRoomIDByStreamName (GetRoomIDByStreamNameReq) returns (GetRoomIDByStreamNameReply);
// GetAdapterStreamByStreamName 适配结果输出, 此处也可以输入备用流, 该结果只输出直推上行
rpc GetAdapterStreamByStreamName (GetAdapterStreamByStreamNameReq) returns (GetAdapterStreamByStreamNameReply);
// GetSrcByRoomID
rpc GetSrcByRoomID (GetSrcByRoomIDReq) returns (GetSrcByRoomIDReply);
// GetLineListByRoomID
rpc GetLineListByRoomID (GetLineListByRoomIDReq) returns (GetLineListByRoomIDReply);
// GetUpStreamRtmp UpStream
rpc GetUpStreamRtmp (GetUpStreamRtmpReq) returns (GetUpStreamRtmpReply);
// StreamCut 切流的房间和时间, 内部调用
rpc StreamCut (StreamCutReq) returns (EmptyStruct);
// Ping Service
rpc Ping (PingReq) returns (PingReply);
// Close Service
rpc Close (CloseReq) returns (CloseReply);
// ClearRetweetRecord
rpc ClearStreamStatus (ClearStreamStatusReq) returns (EmptyStruct);
// CheckLiveStreamList
rpc CheckLiveStreamList(CheckLiveStreamReq) returns (CheckLiveStreamReply);
}

View File

@@ -0,0 +1,24 @@
package v1
import (
"context"
"fmt"
"go-common/library/net/rpc/warden"
"google.golang.org/grpc"
)
// DiscoveryAppID .
const DiscoveryAppID = "video.live.streamng"
// NewClient 建立grpc连接
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (StreamClient, error) {
client := warden.NewClient(cfg, opts...)
cc, err := client.Dial(context.Background(), fmt.Sprintf("discovery://default/%s", DiscoveryAppID))
if err != nil {
return nil, err
}
return NewStreamClient(cc), nil
}

View File

@@ -0,0 +1,49 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
data = ["stream-mng.toml"],
importpath = "go-common/app/service/video/stream-mng/cmd",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/video/stream-mng/conf:go_default_library",
"//app/service/video/stream-mng/server/grpc:go_default_library",
"//app/service/video/stream-mng/server/http:go_default_library",
"//app/service/video/stream-mng/service:go_default_library",
"//library/conf/env:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log:go_default_library",
"//library/net/trace:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/service/video/stream-mng/cmd/client:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,40 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "client",
embed = [":go_default_library"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "go-common/app/service/video/stream-mng/cmd/client",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/video/stream-mng/api/v1:go_default_library",
"//library/ecode:go_default_library",
"//library/net/rpc/warden:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,31 @@
package main
import (
"context"
"fmt"
"go-common/app/service/video/stream-mng/api/v1"
"go-common/library/ecode"
"go-common/library/net/rpc/warden"
)
func main() {
cc, err := warden.NewClient(nil).Dial(context.Background(), "127.0.0.1:9000")
if err != nil {
return
}
client := v1.NewStreamClient(cc)
resp, err := client.GetRoomIDByStreamName(context.Background(), &v1.GetRoomIDByStreamNameReq{
StreamName: "",
})
fmt.Printf("%v=%v", resp, err)
e := ecode.Cause(err)
fmt.Printf("%v=%v=%v=%v", e, e.Code(), e.Message(), e.Details())
}

View File

@@ -0,0 +1,57 @@
package main
import (
"flag"
"os"
"os/signal"
"syscall"
"time"
"context"
"go-common/app/service/video/stream-mng/conf"
"go-common/app/service/video/stream-mng/server/grpc"
"go-common/app/service/video/stream-mng/server/http"
"go-common/app/service/video/stream-mng/service"
"go-common/library/conf/env"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
"go-common/library/net/trace"
)
func main() {
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
if env.DeployEnv == env.DeployEnvProd {
log.Init(nil)
} else {
log.Init(conf.Conf.Log)
}
defer log.Close()
log.Info("stream-mng-service start")
trace.Init(conf.Conf.Tracer)
defer trace.Close()
ecode.Init(conf.Conf.Ecode)
svc := service.New(conf.Conf)
http.Init(conf.Conf, svc)
ws := grpc.New(nil, svc)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
svc.Close()
ws.Shutdown(context.Background())
log.Info("stream-mng-service exit")
time.Sleep(time.Second)
return
case syscall.SIGHUP:
default:
return
}
}
}

View File

@@ -0,0 +1,69 @@
[log]
stdout=true
[mysql]
addr = "172.16.33.205:3306"
dsn = "bvc_live:2Yu4VAaSmY6mI4A80uO13br3xcTSpRhK@tcp(172.16.33.205:3306)/bvc_live?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4"
readDSN = ["bvc_live_reader:MVArfTM1l6xBNpLUC1JviZOPuGhl5WNP@tcp(172.16.33.205:3306)/bvc_live?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4","bvc_live_reader:MVArfTM1l6xBNpLUC1JviZOPuGhl5WNP@tcp(172.16.33.205:3306)/bvc_live?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4"]
active = 20
idle = 10
idleTimeout ="4h"
queryTimeout = "500ms"
execTimeout = "500ms"
tranTimeout = "500ms"
[tidb]
addr = "172.16.33.205:3306"
dsn = "bvc_live:2Yu4VAaSmY6mI4A80uO13br3xcTSpRhK@tcp(172.16.33.205:3306)/bvc_live?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4"
readDSN = ["bvc_live_reader:MVArfTM1l6xBNpLUC1JviZOPuGhl5WNP@tcp(172.16.33.205:3306)/bvc_live?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4","bvc_live_reader:MVArfTM1l6xBNpLUC1JviZOPuGhl5WNP@tcp(172.16.33.205:3306)/bvc_live?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4"]
active = 20
idle = 10
idleTimeout ="4h"
queryTimeout = "1s"
execTimeout = "500ms"
tranTimeout = "500ms"
[httpClient]
key = "8491859e21fd4aa0"
secret = "0f2d9d5df64511bd40d6bf085c981c40"
dial = "500ms"
timeout = "1s"
keepAlive = "60s"
timer = 10
[httpClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[redis]
name = "stream-mng-service"
proto = "tcp"
addr = "liveredis:6379"
idle = 10
active = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
expire = "1m"
[memcache]
name = "stream-mng-service"
proto = "tcp"
addr = ""
active = 50
idle = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
expire = "24h"
[upDispatch]
dispatch = [8,4,2,8,4,2,8,4,2,1]
[dropCDN]
dropCDN = []

View File

@@ -0,0 +1,28 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["common.go"],
importpath = "go-common/app/service/video/stream-mng/common",
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,136 @@
package common
const (
BitWiseBVC = 1
BitWiseKS = 2
BitWiseQN = 4
BitWiseTC = 8
BitWiseWS = 16
WSSrc = 1
TXYSrc = 16
BVCSrc = 32
JSSrc = 68
QNSrc = 70
WSName = "ws"
TXYName = "txy"
BVCName = "bvc"
JSName = "js"
QNName = "qn"
WsChinaName = "网宿"
TXYChinaName = "腾讯云"
BVCChinaName = "视频云"
JSChinaName = "金山"
QNChinaName = "七牛"
AREAIDCHICKEN = 80
ChickenAttention = 1000
)
// CdnMapSrc cdn&src映射
var CdnMapSrc = map[string]int8{
WSName: WSSrc,
TXYName: TXYSrc,
BVCName: BVCSrc,
JSName: JSSrc,
QNName: QNSrc,
}
// SrcMapBitwise 新老src直接的映射关系
var SrcMapBitwise = map[int8]int64{
WSSrc: BitWiseWS,
TXYSrc: BitWiseTC,
BVCSrc: BitWiseBVC,
JSSrc: BitWiseKS,
QNSrc: BitWiseQN,
}
// BitwiseMapSrc 新老src直接的映射关系
var BitwiseMapSrc = map[int64]int8{
BitWiseWS: WSSrc,
BitWiseTC: TXYSrc,
BitWiseBVC: BVCSrc,
BitWiseKS: JSSrc,
BitWiseQN: QNSrc,
}
// ChinaNameMapBitwise
var ChinaNameMapBitwise = map[string]int64{
WsChinaName: BitWiseWS,
TXYChinaName: BitWiseTC,
BVCChinaName: BitWiseBVC,
JSChinaName: BitWiseKS,
QNChinaName: BitWiseQN,
}
// NameMapBitwise
var NameMapBitwise = map[int64]string{
BitWiseWS: WsChinaName,
BitWiseTC: TXYChinaName,
BitWiseBVC: BVCChinaName,
BitWiseKS: JSChinaName,
BitWiseQN: QNChinaName,
}
// CdnBitwiseMap 位运算对应表
var CdnBitwiseMap = map[string]int64{
BVCName: BitWiseBVC,
JSName: BitWiseKS,
QNName: BitWiseQN,
TXYName: BitWiseTC,
WSName: BitWiseWS,
}
// BitwiseMapName 新src对应关系
var BitwiseMapName = map[int64]string{
BitWiseWS: WSName,
BitWiseTC: TXYName,
BitWiseBVC: BVCName,
BitWiseKS: JSName,
BitWiseQN: QNName,
}
// NewLinkMap 新origin
var NewLinkMap = map[int64]map[string]string{
BitWiseBVC: {
"newLink": "http://live-schedule.acgvideo.com/live-upbvc?up_rtmp=",
"newLinkHttps": "https://live-schedule.acgvideo.com/live-upbvc?up_rtmp=",
},
BitWiseTC: {
"newLink": "http://tcdns.myqcloud.com:8086/bilibili_redirect?up_rtmp=",
"newLinkHttps": "https://tcdns.myqcloud.com/bilibili_redirect?up_rtmp=",
},
BitWiseKS: {
"newLink": "http://cwwshdns.ksyun.com/a?up_rtmp=",
"newLinkHttps": "https://cwwshdns.ksyun.com/a?up_rtmp=",
},
BitWiseWS: {
"newLink": "http://sdkbilibili.wscdns.com/bilibili?up_rtmp=",
"newLinkHttps": "https://sdkbilibili.wscdns.com?up_rtmp=",
},
BitWiseQN: {
"newLink": "http://pili-ipswitch.qiniuapi.com/v1/bilibili/publish?up_rtmp=",
"newLinkHttps": "https://pili-ipswitch.qiniuapi.com/v1/bilibili/publish?up_rtmp=",
},
}
// 流线路名称
var LineName = map[int64]string{
BitWiseBVC: "线路二",
BitWiseTC: "线路一",
BitWiseKS: "线路四",
BitWiseWS: "线路三",
BitWiseQN: "线路五",
0: "默认路线",
}
// CDNSalt cdn&salt map
var CDNSalt = map[string]string{
"bvc": "bvc_1701101740",
"js": "js_1703271720",
"qn": "qn_1703271730",
"txy": "txy_1610171720",
"ws": "ws_1608121700",
}

View File

@@ -0,0 +1,41 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["conf.go"],
importpath = "go-common/app/service/video/stream-mng/conf",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/conf:go_default_library",
"//library/database/sql:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/auth:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//library/net/trace:go_default_library",
"//vendor/github.com/BurntSushi/toml:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,94 @@
package conf
import (
"errors"
"flag"
"go-common/library/cache/memcache"
"go-common/library/cache/redis"
"go-common/library/conf"
"go-common/library/database/sql"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/auth"
"go-common/library/net/http/blademaster/middleware/verify"
"go-common/library/net/trace"
"github.com/BurntSushi/toml"
)
var (
confPath string
client *conf.Client
// Conf config
Conf = &Config{}
)
// Config .
type Config struct {
Log *log.Config
BM *bm.ServerConfig
Verify *verify.Config
Auth *auth.Config
Tracer *trace.Config
Redis *redis.Config
Memcache *memcache.Config
MySQL *sql.Config
TIDB *sql.Config
Ecode *ecode.Config
// http client
HTTPClient *bm.ClientConfig
DropCDN map[string]interface{}
}
func init() {
flag.StringVar(&confPath, "conf", "", "default config path")
}
// Init init conf
func Init() error {
if confPath != "" {
return local()
}
return remote()
}
func local() (err error) {
_, err = toml.DecodeFile(confPath, &Conf)
return
}
func remote() (err error) {
if client, err = conf.New(); err != nil {
return
}
if err = load(); err != nil {
return
}
go func() {
for range client.Event() {
log.Info("config reload")
if load() != nil {
log.Error("config reload error (%v)", err)
}
}
}()
return
}
func load() (err error) {
var (
s string
ok bool
tmpConf *Config
)
if s, ok = client.Toml2(); !ok {
return errors.New("load config center error")
}
if _, err = toml.Decode(s, &tmpConf); err != nil {
return errors.New("could not decode config")
}
*Conf = *tmpConf
return
}

View File

@@ -0,0 +1,89 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"backup-stream.go",
"cache.go",
"dao.cache.go",
"dao.go",
"http.go",
"localcache.go",
"main-stream.go",
"mysql.go",
"notify.go",
"official-stream.go",
"redis.go",
"stream-change-log.go",
"upstream-summary.go",
],
importpath = "go-common/app/service/video/stream-mng/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/video/stream-mng/common:go_default_library",
"//app/service/video/stream-mng/conf:go_default_library",
"//app/service/video/stream-mng/model:go_default_library",
"//library/cache:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/conf/env:go_default_library",
"//library/database/sql:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/metadata:go_default_library",
"//library/stat/prom:go_default_library",
"//library/sync/errgroup:go_default_library",
"//library/sync/pipeline/fanout:go_default_library",
"//vendor/github.com/bluele/gcache:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/golang.org/x/sync/singleflight:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"backup-stream_test.go",
"cache_test.go",
"dao.cache_test.go",
"dao_test.go",
"http_test.go",
"localcache_test.go",
"main-stream_test.go",
"mysql_test.go",
"notify_test.go",
"official-stream_test.go",
"redis_test.go",
"stream-change-log_test.go",
"upstream-summary_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/video/stream-mng/conf:go_default_library",
"//app/service/video/stream-mng/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,290 @@
package dao
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"go-common/app/service/video/stream-mng/model"
"go-common/library/log"
"math/rand"
"strings"
"time"
"go-common/library/database/sql"
"github.com/pkg/errors"
"go-common/app/service/video/stream-mng/common"
)
const (
_maxRetryTimes = 5
_vendorBVC = 1
_vendorKS = 2
_vendorQN = 4
_vendorTC = 8
_vendorWS = 16
_getBackupStreamByRoomID = "SELECT `room_id`,`stream_name`, `key`,`default_vendor`, `origin_upstream`,`streaming`,`last_stream_time`, `expires_at`, `options` from `backup_stream` WHERE `room_id` = ? and status = 1"
_getBackupStreamByStreamName = "SELECT `room_id`,`stream_name`, `key`, `default_vendor`, `origin_upstream`, `streaming`, `last_stream_time`, `expires_at`, `options` from `backup_stream` WHERE `stream_name` = ? and status = 1;"
_getMultiBackupStreamByRID = "SELECT `room_id`,`stream_name`, `key`, `default_vendor`, `origin_upstream`, `streaming`, `last_stream_time`, `expires_at`, `options` from `backup_stream` WHERE `room_id` = %d and status = 1"
_getBackupRoom = "SELECT distinct room_id from `backup_stream`"
_insertBackupStream = "INSERT INTO `backup_stream` (room_id, stream_name, `key`, default_vendor, expires_at, options) VALUES (?, ?, ?, ?, ?, ?);"
)
// GetBackupStreamByRoomID 根据roomid获取备用流信息
func (d *Dao) GetBackupStreamByRoomID(ctx context.Context, rid int64) (infos []*model.BackupStream, err error) {
var rows *sql.Rows
if rows, err = d.db.Query(ctx, _getBackupStreamByRoomID, rid); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
bs := new(model.BackupStream)
if err = rows.Scan(&bs.RoomID, &bs.StreamName, &bs.Key, &bs.DefaultVendor, &bs.OriginUpstream, &bs.Streaming, &bs.LastStreamTime, &bs.ExpiresAt, &bs.Options); err != nil {
err = errors.WithStack(err)
return
}
infos = append(infos, bs)
}
err = rows.Err()
return
}
// GetMultiBackupStreamByRID 批量查询备用流
func (d *Dao) GetMultiBackupStreamByRID(c context.Context, rids []int64) (infos []*model.BackupStream, err error) {
len := len(rids)
muSql := ""
for i := 0; i < len; i++ {
ss := fmt.Sprintf(_getMultiBackupStreamByRID, rids[i])
if i == 0 {
muSql = fmt.Sprintf("%s%s", muSql, ss)
} else {
muSql = fmt.Sprintf("%s UNION %s", muSql, ss)
}
}
var rows *sql.Rows
if rows, err = d.db.Query(c, muSql); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
bs := new(model.BackupStream)
if err = rows.Scan(&bs.RoomID, &bs.StreamName, &bs.Key, &bs.DefaultVendor, &bs.OriginUpstream, &bs.Streaming, &bs.LastStreamTime, &bs.ExpiresAt, &bs.Options); err != nil {
if err == sql.ErrNoRows {
continue
}
err = errors.WithStack(err)
return
}
infos = append(infos, bs)
}
err = rows.Err()
return
}
// GetBackupRoom 临时获取所有的房间号
func (d *Dao) GetBackupRoom(ctx context.Context) (res map[int64]int64, err error) {
res = map[int64]int64{}
var rows *sql.Rows
if rows, err = d.db.Query(ctx, _getBackupRoom); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
bs := new(model.BackupStream)
if err = rows.Scan(&bs.RoomID); err != nil {
err = errors.WithStack(err)
return
}
res[bs.RoomID] = bs.RoomID
}
err = rows.Err()
return
}
// CreateBackupStream 创建备用流
func (d *Dao) CreateBackupStream(ctx context.Context, bs *model.BackupStream) (*model.BackupStream, error) {
if bs.StreamName == "" {
bs.StreamName = fmt.Sprintf("live_%d_bs_%d", bs.RoomID, rand.Intn(9899999)+100000)
}
if bs.Key == "" {
h := md5.New()
h.Write([]byte(fmt.Sprintf("%s%d", bs.StreamName, time.Now().Nanosecond())))
bs.Key = hex.EncodeToString(h.Sum(nil))
}
if bs.DefaultVendor == 0 {
bs.DefaultVendor = 1
}
// 当传入的默认上行不是五家cdn
if _, ok := common.BitwiseMapSrc[bs.DefaultVendor]; !ok {
bs.DefaultVendor = 1
}
if bs.ExpiresAt.Before(time.Now()) {
bs.ExpiresAt = time.Now().Add(time.Hour * 336) // 14 * 24
}
res, err := d.stmtBackupStreamCreate.Exec(ctx, bs.RoomID, bs.StreamName, bs.Key, bs.DefaultVendor, bs.ExpiresAt.Format("2006-01-02 15:04:05"), bs.Options)
if err != nil {
return bs, err
}
bs.ID, err = res.LastInsertId()
return bs, err
}
// GetBackupStreamByStreamName 根据流名查询备用流
func (d *Dao) GetBackupStreamByStreamName(c context.Context, sn string) (*model.BackupStream, error) {
row := d.db.QueryRow(c, _getBackupStreamByStreamName, sn)
bs := &model.BackupStream{}
err := row.Scan(&bs.RoomID, &bs.StreamName, &bs.Key,
&bs.DefaultVendor, &bs.OriginUpstream, &bs.Streaming,
&bs.LastStreamTime, &bs.ExpiresAt, &bs.Options)
if err != nil {
return nil, err
}
return bs, nil
}
const (
_setOriginUpstream = "UPDATE `backup_stream` SET `origin_upstream` = ?, `streaming` = ? WHERE `stream_name` = ? and `origin_upstream` = 0;"
_setForwardUpstream = "UPDATE `backup_stream` SET `streaming` = ? WHERE `stream_name` = ? and `streaming` = ?;"
_setOriginUpstreamOnClose = "UPDATE `backup_stream` SET `origin_upstream` = 0, `streaming` = 0, `last_stream_time` = CURRENT_TIMESTAMP WHERE `stream_name` = ? and `origin_upstream` != 0;"
_setForwardUpstreamOnClose = "UPDATE `backup_stream` SET `streaming` = ? WHERE `stream_name` = ? and `streaming` = ?;"
)
var cdnBitwiseMap = map[string]int64{
"bvc": _vendorBVC,
"ks": _vendorKS,
"js": _vendorKS, // alias
"qn": _vendorQN,
"tc": _vendorTC,
"tx": _vendorTC, // alias
"txy": _vendorTC, // alias
"ws": _vendorWS,
}
// SetBackupStreamStreamingStatus
func (d *Dao) SetBackupStreamStreamingStatus(c context.Context, p *model.StreamingNotifyParam, bs *model.BackupStream, open bool) (*model.BackupStream, error) {
bitwise, ok := cdnBitwiseMap[strings.ToLower(p.SRC)]
if !ok {
return nil, errors.New("unknown src:" + p.SRC)
}
for i := 1; i <= _maxRetryTimes; i++ {
if open { // 开播
if bs.Streaming&bitwise == bitwise {
return bs, nil
}
if p.Type.String() == "0" { // 主推
if bs.OriginUpstream == 0 { // 只有当前没有原始上行时才去尝试更新主推记录
res, err := d.db.Exec(c, _setOriginUpstream, bitwise, bitwise, bs.StreamName)
if err != nil {
log.Errorw(c, "backup_stream_update_origin_record", err)
} else {
ra, err := res.RowsAffected()
if err != nil {
log.Errorw(c, "backup_stream_update_origin_record_rows_affected", err)
}
if ra == 1 { // 成功
bs.Streaming = bitwise
bs.OriginUpstream = bitwise
return bs, nil
}
// 影响行数为 0可能是发生了错误
// 也可能是因为原始数据已经发生变更等待后面重新读取DB中的数据。
}
} else { // 目前已经有上行了。在这里处理。
if bitwise != bs.OriginUpstream {
return bs, errors.New("origin upstream already exists")
}
}
} else { // 转推
if bs.OriginUpstream == 0 {
return bs, errors.New("origin upstream not exists")
}
res, err := d.db.Exec(c, _setForwardUpstream, bs.Streaming|bitwise, bs.StreamName, bs.Streaming)
if err != nil {
log.Errorw(c, "backup_stream_update_forward_record", err)
} else {
ra, err := res.RowsAffected()
if err != nil {
log.Errorw(c, "backup_stream_update_forward_record_rows_affected", err)
}
if ra == 1 { // 成功
bs.Streaming = bs.Streaming | bitwise
return bs, nil
}
}
}
} else { // 关播
if p.Type.String() == "0" { // 主推
if bs.OriginUpstream != bitwise { // 如果不是当前主推,直接拒绝
return bs, errors.New("permission denied")
}
res, err := d.db.Exec(c, _setOriginUpstreamOnClose, bs.StreamName)
if err != nil {
log.Errorw(c, "backup_stream_update_onclose_origin_record", err)
} else {
ra, err := res.RowsAffected()
if err != nil {
log.Errorw(c, "backup_stream_update_origin_onclose_record_rows_affected", err)
}
if ra == 1 { // 成功
bs.Streaming = 0
bs.OriginUpstream = 0
return bs, nil
}
}
// 影响行数为 0可能是发生了错误
// 也可能是因为原始数据已经发生变更等待后面重新读取DB中的数据。
} else { // 转推
if bs.OriginUpstream == bitwise {
return bs, errors.New("invalid params. you are origin upstream.")
}
res, err := d.db.Exec(c, _setForwardUpstreamOnClose, bs.Streaming&^bitwise, bs.StreamName, bs.Streaming)
if err != nil {
log.Errorw(c, "backup_stream_update_onclose_forward_record", err)
} else {
ra, err := res.RowsAffected()
if err != nil {
log.Errorw(c, "backup_stream_update_forward_onclose_record_rows_affected", err)
}
if ra == 1 { // 成功
bs.Streaming = bs.Streaming &^ bitwise
return bs, nil
}
}
}
}
time.Sleep(time.Millisecond * 100)
bs, err := d.GetBackupStreamByStreamName(c, bs.StreamName)
if err != nil {
log.Errorw(c, "backup_stream_refresh_record_row", err)
return bs, errors.New("system busy")
}
}
return bs, errors.New("update backup stream failed")
}

View File

@@ -0,0 +1,84 @@
package dao
import (
"context"
"go-common/app/service/video/stream-mng/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoGetBackupStreamByRoomID(t *testing.T) {
convey.Convey("GetBackupStreamByRoomID", t, func(ctx convey.C) {
var (
ctx2 = context.Background()
rid = int64(11891462)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
infos, err := d.GetBackupStreamByRoomID(ctx2, rid)
ctx.Convey("Then err should be nil.infos should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(infos, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoCreateBackupStream(t *testing.T) {
convey.Convey("CreateBackupStream", t, func(ctx convey.C) {
var (
ctx2 = context.Background()
bs = &model.BackupStream{
RoomID: 66666,
}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1, err := d.CreateBackupStream(ctx2, bs)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoGetBackupStreamByStreamName(t *testing.T) {
convey.Convey("GetBackupStreamByStreamName", t, func(ctx convey.C) {
var (
c = context.Background()
sn = "live_1511284_bs_7317941"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1, err := d.GetBackupStreamByStreamName(c, sn)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoSetBackupStreamStreamingStatus(t *testing.T) {
convey.Convey("SetBackupStreamStreamingStatus", t, func(ctx convey.C) {
var (
c = context.Background()
p = &model.StreamingNotifyParam{
SRC: "bvc",
Type: "0",
StreamName: "live_1511284_bs_7317941",
}
bs = &model.BackupStream{
OriginUpstream: 1,
StreamName: "live_1511284_bs_7317941",
}
open bool
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1, err := d.SetBackupStreamStreamingStatus(c, p, bs, open)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}

View File

@@ -0,0 +1,143 @@
package dao
import (
"context"
"fmt"
"go-common/app/service/video/stream-mng/model"
)
//go:generate $GOPATH/src/go-common/app/tool/cache/gen
type _cache interface {
// 获取流完整信息
// cache: -singleflight=true -ignores=||id,sname -nullcache=&model.StreamFullInfo{RoomID:-1} -check_null_code=$!=nil&&$.RoomID<=0
streamFullInfo(c context.Context, id int64, sname string) (res *model.StreamFullInfo, err error)
// 获取rid
// cache: -singleflight=true -ignores=||sname -nullcache=&model.StreamFullInfo{RoomID:-1} -check_null_code=$!=nil&&$.RoomID<=0
streamRIDByName(c context.Context, sname string) (res *model.StreamFullInfo, err error)
// 批量获取接口
// cache: -nullcache=&model.StreamFullInfo{RoomID:-1} -check_null_code=$!=nil&&$.RoomID<=0
multiStreamInfo(c context.Context, rid []int64) (res map[int64]*model.StreamFullInfo, err error)
}
func (d *Dao) cacheSFstreamFullInfo(id int64, sname string) string {
if sname != "" {
return fmt.Sprintf("sf_sname_%s", sname)
}
return fmt.Sprintf("sf_rid_%d", id)
}
func (d *Dao) cacheSFstreamRIDByName(sname string) string {
return fmt.Sprintf("sf_rid_map_name_%s", sname)
}
// StreamFullInfo 传入rid或者sname 获取房间流信息
func (d *Dao) StreamFullInfo(c context.Context, rid int64, sname string) (res *model.StreamFullInfo, err error) {
info, err := d.streamFullInfo(c, rid, sname)
if err != nil {
return nil, err
}
if info == nil {
return nil, fmt.Errorf("can not find by room_id=%d", rid)
}
if len(info.List) == 1 && info.List[0].StreamName == "miss" {
return nil, fmt.Errorf("can not find any info by room_id=%d", rid)
}
return info, nil
}
// OriginUpStreamInfo 原始上行流名和src
func (d *Dao) OriginUpStreamInfo(c context.Context, rid int64) (sname string, origin int64, err error) {
info, err := d.streamFullInfo(c, rid, "")
if err != nil {
return "", 0, err
}
if info == nil {
return "", 0, fmt.Errorf("can not find by room_id=%d", rid)
}
for _, v := range info.List {
if v.Type == 1 {
// 优先级高
if v.Origin != 0 {
return v.StreamName, v.Origin, nil
}
return v.StreamName, v.DefaultUpStream, nil
}
}
return "", 0, fmt.Errorf("can not find by room_id=%d", rid)
}
func (d *Dao) DefaultUpStreamInfo(c context.Context, rid int64) (sname string, origin int64, err error) {
info, err := d.streamFullInfo(c, rid, "")
if err != nil {
return "", 0, err
}
if info == nil {
return "", 0, fmt.Errorf("can not find by room_id=%d", rid)
}
for _, v := range info.List {
if v.Type == 1 {
return v.StreamName, v.DefaultUpStream, nil
}
}
return "", 0, fmt.Errorf("can not find by room_id=%d", rid)
}
// OriginUpStreamInfoBySName 查询流的上行,正在推流上行和默认上行; 包含备用流
func (d *Dao) OriginUpStreamInfoBySName(c context.Context, sname string) (rid int64, origin int64, err error) {
info, err := d.streamFullInfo(c, 0, sname)
if err != nil {
return 0, 0, err
}
if info == nil {
return 0, 0, fmt.Errorf("can not find by sname=%s", sname)
}
for _, v := range info.List {
if v.StreamName == sname {
// 优先级高
if v.Origin != 0 {
return info.RoomID, v.Origin, nil
}
return info.RoomID, v.DefaultUpStream, nil
}
}
return 0, 0, fmt.Errorf("can not find by sname=%s", sname)
}
// StreamRIDByName 获取rid
func (d *Dao) StreamRIDByName(c context.Context, sname string) (int64, error) {
info, err := d.streamRIDByName(c, sname)
if err != nil {
return -1, err
}
if info != nil && info.RoomID > 0 {
return info.RoomID, nil
}
return -1, fmt.Errorf("can not find by sname=%s", sname)
}
// MultiStreamInfo 批量接口
func (d *Dao) MultiStreamInfo(c context.Context, rids []int64) (res map[int64]*model.StreamFullInfo, err error) {
infos, err := d.multiStreamInfo(c, rids)
if err != nil {
return res, err
}
resp := map[int64]*model.StreamFullInfo{}
for k, v := range infos {
if len(v.List) == 1 && v.List[0].StreamName == "miss" {
continue
}
resp[k] = v
}
return resp, nil
}

View File

@@ -0,0 +1,120 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaocacheSFstreamFullInfo(t *testing.T) {
convey.Convey("cacheSFstreamFullInfo", t, func(ctx convey.C) {
var (
id = int64(11891462)
sname = ""
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := d.cacheSFstreamFullInfo(id, sname)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaocacheSFstreamRIDByName(t *testing.T) {
convey.Convey("cacheSFstreamRIDByName", t, func(ctx convey.C) {
var (
sname = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := d.cacheSFstreamRIDByName(sname)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoStreamFullInfo(t *testing.T) {
convey.Convey("StreamFullInfo", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
sname = ""
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
res, err := d.StreamFullInfo(c, rid, sname)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoOriginUpStreamInfo(t *testing.T) {
convey.Convey("OriginUpStreamInfo", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
sname, origin, err := d.OriginUpStreamInfo(c, rid)
ctx.Convey("Then err should be nil.sname,origin should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(origin, convey.ShouldNotBeNil)
ctx.So(sname, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoOriginUpStreamInfoBySName(t *testing.T) {
convey.Convey("OriginUpStreamInfoBySName", t, func(ctx convey.C) {
var (
c = context.Background()
sname = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
rid, origin, err := d.OriginUpStreamInfoBySName(c, sname)
ctx.Convey("Then err should be nil.rid,origin should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(origin, convey.ShouldNotBeNil)
ctx.So(rid, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoStreamRIDByName(t *testing.T) {
convey.Convey("StreamRIDByName", t, func(ctx convey.C) {
var (
c = context.Background()
sname = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1, err := d.StreamRIDByName(c, sname)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoMultiStreamInfo(t *testing.T) {
convey.Convey("MultiStreamInfo", t, func(ctx convey.C) {
var (
c = context.Background()
rids = []int64{11891462}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
res, err := d.MultiStreamInfo(c, rids)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
})
}

View File

@@ -0,0 +1,177 @@
// Code generated by $GOPATH/src/go-common/app/tool/cache/gen. DO NOT EDIT.
/*
Package dao is a generated cache proxy package.
It is generated from:
type _cache interface {
// 获取流完整信息
// cache: -singleflight=true -ignores=||id,sname -nullcache=&model.StreamFullInfo{RoomID:-1} -check_null_code=$!=nil&&$.RoomID<=0
streamFullInfo(c context.Context, id int64, sname string) (res *model.StreamFullInfo, err error)
// 获取rid
// cache: -singleflight=true -ignores=||sname -nullcache=&model.StreamFullInfo{RoomID:-1} -check_null_code=$!=nil&&$.RoomID<=0
streamRIDByName(c context.Context, sname string) (res *model.StreamFullInfo, err error)
// 批量获取接口
// cache: -nullcache=&model.StreamFullInfo{RoomID:-1} -check_null_code=$!=nil&&$.RoomID<=0
multiStreamInfo(c context.Context, rid []int64) (res map[int64]*model.StreamFullInfo, err error)
}
*/
package dao
import (
"context"
"go-common/app/service/video/stream-mng/model"
"go-common/library/net/metadata"
"go-common/library/stat/prom"
"go-common/library/log"
"golang.org/x/sync/singleflight"
)
var _ _cache
var cacheSingleFlights = [2]*singleflight.Group{{}, {}}
// streamFullInfo 获取流完整信息
func (d *Dao) streamFullInfo(c context.Context, id int64, sname string) (res *model.StreamFullInfo, err error) {
addCache := true
res, err = d.CacheStreamFullInfo(c, id, sname)
if err != nil {
addCache = false
err = nil
}
defer func() {
if res != nil && res.RoomID <= 0 {
res = nil
}
}()
if res != nil {
log.Warn("get from redis")
prom.CacheHit.Incr("streamFullInfo")
return
}
var rr interface{}
sf := d.cacheSFstreamFullInfo(id, sname)
rr, err, _ = cacheSingleFlights[0].Do(sf, func() (r interface{}, e error) {
prom.CacheMiss.Incr("streamFullInfo")
r, e = d.RawStreamFullInfo(c, id, sname)
return
})
res = rr.(*model.StreamFullInfo)
if err != nil {
return
}
miss := res
if miss == nil {
miss = &model.StreamFullInfo{RoomID: -1}
}
if !addCache {
return
}
d.cache.Save(func() {
d.AddCacheStreamFullInfo(metadata.WithContext(c), id, miss)
})
return
}
// streamRIDByName 获取rid
func (d *Dao) streamRIDByName(c context.Context, id string) (res *model.StreamFullInfo, err error) {
addCache := true
res, err = d.CacheStreamRIDByName(c, id)
if err != nil {
addCache = false
err = nil
}
defer func() {
if res != nil && res.RoomID <= 0 {
res = nil
}
}()
if res != nil {
prom.CacheHit.Incr("streamRIDByName")
return
}
var rr interface{}
sf := d.cacheSFstreamRIDByName(id)
rr, err, _ = cacheSingleFlights[1].Do(sf, func() (r interface{}, e error) {
prom.CacheMiss.Incr("streamRIDByName")
r, e = d.RawStreamRIDByName(c, id)
return
})
res = rr.(*model.StreamFullInfo)
if err != nil {
return
}
miss := res
if miss == nil {
miss = &model.StreamFullInfo{RoomID: -1}
}
if !addCache {
return
}
d.cache.Save(func() {
d.AddCacheStreamRIDByName(metadata.WithContext(c), id, miss)
})
return
}
// multiStreamInfo 批量获取接口
func (d *Dao) multiStreamInfo(c context.Context, keys []int64) (res map[int64]*model.StreamFullInfo, err error) {
if len(keys) == 0 {
return
}
addCache := true
res, err = d.CacheMultiStreamInfo(c, keys)
if err != nil {
addCache = false
res = nil
err = nil
}
var miss []int64
for _, key := range keys {
if (res == nil) || (res[key] == nil) {
miss = append(miss, key)
}
}
prom.CacheHit.Add("multiStreamInfo", int64(len(keys)-len(miss)))
defer func() {
for k, v := range res {
if v != nil && v.RoomID <= 0 {
delete(res, k)
}
}
}()
if len(miss) == 0 {
log.Warn("get from redis")
return
}
var missData map[int64]*model.StreamFullInfo
prom.CacheMiss.Add("multiStreamInfo", int64(len(miss)))
missData, err = d.RawMultiStreamInfo(c, miss)
if res == nil {
res = make(map[int64]*model.StreamFullInfo)
}
for k, v := range missData {
res[k] = v
}
if err != nil {
return
}
for _, key := range keys {
if res[key] == nil {
if missData == nil {
missData = make(map[int64]*model.StreamFullInfo, len(keys))
}
missData[key] = &model.StreamFullInfo{RoomID: -1}
}
}
if !addCache {
return
}
d.cache.Save(func() {
d.AddCacheMultiStreamInfo(metadata.WithContext(c), missData)
})
return
}

View File

@@ -0,0 +1,57 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaostreamFullInfo(t *testing.T) {
convey.Convey("streamFullInfo", t, func(ctx convey.C) {
var (
c = context.Background()
id = int64(11891462)
sname = ""
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
res, err := d.streamFullInfo(c, id, sname)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaostreamRIDByName(t *testing.T) {
convey.Convey("streamRIDByName", t, func(ctx convey.C) {
var (
c = context.Background()
id = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
res, err := d.streamRIDByName(c, id)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaomultiStreamInfo(t *testing.T) {
convey.Convey("multiStreamInfo", t, func(ctx convey.C) {
var (
c = context.Background()
keys = []int64{11891462}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
res, err := d.multiStreamInfo(c, keys)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
})
}

View File

@@ -0,0 +1,92 @@
package dao
import (
"context"
"go-common/app/service/video/stream-mng/conf"
"go-common/library/cache"
"go-common/library/cache/memcache"
"go-common/library/cache/redis"
xsql "go-common/library/database/sql"
bm "go-common/library/net/http/blademaster"
"go-common/library/sync/pipeline/fanout"
"github.com/bluele/gcache"
)
// Dao dao
type Dao struct {
c *conf.Config
mc *memcache.Pool
redis *redis.Pool
db *xsql.DB
tidb *xsql.DB
httpClient *bm.Client
cache *cache.Cache
liveAside *fanout.Fanout
localCache gcache.Cache
// DB
// 新版本主流
stmtMainStreamCreate *xsql.Stmt
stmtMainStreamChangeDefaultVendor *xsql.Stmt
stmtMainStreamChangeOptions *xsql.Stmt
stmtMainStreamClearAllStreaming *xsql.Stmt
// 备用流
stmtBackupStreamCreate *xsql.Stmt
// 旧版本流
stmtLegacyStreamCreate *xsql.Stmt
stmtLegacyStreamEnableNewUpRank *xsql.Stmt
stmtLegacyStreamDisableUpRank *xsql.Stmt
stmtLegacyStreamClearStreamFoward *xsql.Stmt
stmtLegacyStreamNotify *xsql.Stmt
// tidb
stmtUpStreamDispatch *xsql.Stmt
}
// New init mysql db
func New(c *conf.Config) (dao *Dao) {
dao = &Dao{
c: c,
mc: memcache.NewPool(c.Memcache),
redis: redis.NewPool(c.Redis),
db: xsql.NewMySQL(c.MySQL),
tidb: xsql.NewMySQL(c.TIDB),
httpClient: bm.NewClient(c.HTTPClient),
cache: cache.New(1, 10240),
liveAside: fanout.New("stream-mng"),
localCache: gcache.New(10240).Simple().Build(),
}
// 新版本主流
dao.stmtMainStreamCreate = dao.db.Prepared(_insertMainStream)
dao.stmtMainStreamChangeDefaultVendor = dao.db.Prepared(_changeDefaultVendor)
dao.stmtMainStreamChangeOptions = dao.db.Prepared(_changeOptions)
dao.stmtMainStreamClearAllStreaming = dao.db.Prepared(_clearAllStreaming)
// 备用流
dao.stmtBackupStreamCreate = dao.db.Prepared(_insertBackupStream)
// 旧版本流
dao.stmtLegacyStreamCreate = dao.db.Prepared(_insertOfficialStream)
dao.stmtLegacyStreamEnableNewUpRank = dao.db.Prepared(_updateUpOfficialStreamStatus)
dao.stmtLegacyStreamDisableUpRank = dao.db.Prepared(_updateForwardOfficialStreamStatus)
dao.stmtLegacyStreamClearStreamFoward = dao.db.Prepared(_updateOfficalStreamUpRankStatus)
dao.stmtLegacyStreamNotify = dao.db.Prepared(_setOriginStreamingStatus)
// tidb
dao.stmtUpStreamDispatch = dao.tidb.Prepared(_insertUpStreamInfo)
return
}
// Close close the resource.
func (d *Dao) Close() {
d.mc.Close()
d.redis.Close()
d.db.Close()
d.liveAside.Close()
return
}
// Ping dao ping
func (d *Dao) Ping(c context.Context) error {
// TODO: if you need use mc,redis, please add
return d.db.Ping(c)
}

View File

@@ -0,0 +1,34 @@
package dao
import (
"flag"
"go-common/app/service/video/stream-mng/conf"
"os"
"testing"
)
var (
d *Dao
)
func TestMain(m *testing.M) {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "video.live.stream-mng")
flag.Set("conf_token", "a5640e59138e6b1845f802b6b7dcd348")
flag.Set("tree_id", "62771")
flag.Set("conf_version", "docker-1")
flag.Set("deploy_env", "uat")
flag.Set("conf_host", "config.bilibili.co")
flag.Set("conf_path", "/tmp")
flag.Set("region", "sh")
flag.Set("zone", "sh001")
} else {
flag.Set("conf", "../cmd/stream-mng.toml")
}
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
d = New(conf.Conf)
os.Exit(m.Run())
}

View File

@@ -0,0 +1,49 @@
package dao
import (
"bytes"
"context"
"fmt"
"github.com/pkg/errors"
"go-common/library/conf/env"
"net/http"
"net/url"
)
// NewRequst http 请求
func (d *Dao) NewRequst(c context.Context, method string, url string, query url.Values, body []byte, headers map[string]string, resp interface{}) error {
var req *http.Request
if body != nil && len(body) > 0 {
req, _ = http.NewRequest(method, url, bytes.NewBuffer(body))
} else {
req, _ = http.NewRequest(method, url, nil)
}
if query != nil {
req.URL.RawQuery = query.Encode()
}
if headers != nil && len(headers) > 0 {
for k, v := range headers {
req.Header.Set(k, v)
}
}
if err := d.httpClient.Do(c, req, &resp); err != nil {
err = errors.WithStack(err)
return err
}
return nil
}
// getLiveStreamUrl 对接live-stream.bilibili.co的相关业务
func (d *Dao) getLiveStreamUrl(path string) string {
url := ""
if env.DeployEnv == env.DeployEnvProd {
url = fmt.Sprintf("%s%s", "http://prod-live-stream.bilibili.co", path)
} else {
url = fmt.Sprintf("%s%s", "http://live-stream.bilibili.co", path)
}
return url
}

View File

@@ -0,0 +1,24 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoNewRequst(t *testing.T) {
convey.Convey("NewRequst", t, func(ctx convey.C) {
var (
c = context.Background()
method = ""
url = "http://live-stream.bilibili.co/"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.NewRequst(c, method, url, nil, nil, nil, nil)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}

View File

@@ -0,0 +1,162 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"go-common/app/service/video/stream-mng/model"
"go-common/library/log"
"net/http"
"time"
)
const (
_localLiveStreamList = "live_stream"
_localStreamInfo = "rid:%d"
_liveExpiredTime = 600
_localStreamExpiredTime = 1
)
type OnAirStream struct {
StreamName string `json:"stream_name, omitempty"`
}
type OnAirStreamList struct {
List []*OnAirStream `json:"list,omitempty"`
}
type AllOnAirStream struct {
M map[string]*OnAirStreamList `json:"m,omitempty"`
}
func (d *Dao) getLocalLiveStreamListKey() string {
return _localLiveStreamList
}
func (d *Dao) getLocalStreamInfoKey(rid int64) string {
return fmt.Sprintf(_localStreamInfo, rid)
}
// loadLiveStreamList 判断流是否在播
func (d *Dao) LoadLiveStreamList(c context.Context, rids []int64) map[int64]bool {
list, _ := d.localCache.Get(d.getLocalLiveStreamListKey())
isLive := map[int64]bool{}
if res, ok := list.(map[string]int); ok {
for _, v := range rids {
// rid => stream
info, err := d.streamFullInfo(c, v, "")
if err != nil || info == nil {
continue
}
var sname string
var origin int64
for _, v := range info.List {
if v.Type == 1 {
sname = v.StreamName
origin = v.Origin
break
}
}
// 不在在播列表&orgin 为0===》不在播
if _, exe := res[sname]; !exe {
if origin == 0 {
isLive[v] = false
continue
}
}
isLive[v] = true
}
} else {
// 当从缓存中获取失败 or 项目刚刚启动
for _, v := range rids {
isLive[v] = true
}
}
return isLive
}
// StoreLiveStreamList 刷新在播列表缓存
func (d *Dao) StoreLiveStreamList() {
ctx := context.Background()
type liveStream struct {
Code int `json:"code,omitempty"`
Data *AllOnAirStream `json:"data,omitempty"`
}
resp := &liveStream{}
uri := d.getLiveStreamUrl("/api/live/vendor/onairstreamlist?cdn=bvc")
err := d.NewRequst(ctx, http.MethodGet, uri, nil, nil, nil, resp)
if err != nil {
log.Errorv(ctx, log.KV("log", fmt.Sprintf("http_live_err=%v", err)))
return
}
res := map[string]int{}
if resp.Code == 0 && resp.Data != nil && resp.Data.M != nil && len(resp.Data.M["BVC"].List) > 0 {
for _, v := range resp.Data.M["BVC"].List {
res[v.StreamName] = 1
}
} else {
res, _ := json.Marshal(resp)
log.Errorv(ctx, log.KV("log", fmt.Sprintf("http_live_err=%v", string(res))))
}
// 存10分钟
d.localCache.SetWithExpire(d.getLocalLiveStreamListKey(), res, _liveExpiredTime*time.Second)
}
// storeStreamInfo 存储单个流信息
func (d *Dao) storeStreamInfo(c context.Context, info *model.StreamFullInfo) {
if info == nil || info.RoomID < 0 {
return
}
key := d.getLocalStreamInfoKey(info.RoomID)
d.localCache.SetWithExpire(key, info, _localStreamExpiredTime*time.Second)
}
// loadStreamInfo 读取单个信息
func (d *Dao) loadStreamInfo(c context.Context, rid int64) *model.StreamFullInfo {
key := d.getLocalStreamInfoKey(rid)
info, _ := d.localCache.Get(key)
if res, ok := info.(*model.StreamFullInfo); ok {
if res != nil && res.RoomID > 0 {
return res
}
}
return nil
}
// storeMultiStreamInfo 存储多路流
func (d *Dao) storeMultiStreamInfo(c context.Context, infos map[int64]*model.StreamFullInfo) {
for _, v := range infos {
key := d.getLocalStreamInfoKey(v.RoomID)
//log.Warn("key=%v", key)
d.localCache.SetWithExpire(key, v, _localStreamExpiredTime*time.Second)
}
}
// loadMultiStreamInfo 读取批量信息
func (d *Dao) loadMultiStreamInfo(c context.Context, rids []int64) (map[int64]*model.StreamFullInfo, []int64) {
infos := map[int64]*model.StreamFullInfo{}
missRids := []int64{}
for _, v := range rids {
key := d.getLocalStreamInfoKey(v)
info, _ := d.localCache.Get(key)
if res, ok := info.(*model.StreamFullInfo); ok {
if res != nil && res.RoomID > 0 {
infos[v] = res
}
} else {
missRids = append(missRids, v)
}
}
return infos, missRids
}

View File

@@ -0,0 +1,44 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaogetLocalLiveStreamListKey(t *testing.T) {
convey.Convey("getLocalLiveStreamListKey", t, func(ctx convey.C) {
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := d.getLocalLiveStreamListKey()
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoLoadLiveStreamList(t *testing.T) {
convey.Convey("LoadLiveStreamList", t, func(ctx convey.C) {
var (
c = context.Background()
rids = []int64{11891462}
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := d.LoadLiveStreamList(c, rids)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoStoreLiveStreamList(t *testing.T) {
convey.Convey("StoreLiveStreamList", t, func(ctx convey.C) {
ctx.Convey("When everything gose positive", func(ctx convey.C) {
d.StoreLiveStreamList()
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}

View File

@@ -0,0 +1,207 @@
package dao
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"go-common/app/service/video/stream-mng/common"
"go-common/app/service/video/stream-mng/model"
"go-common/library/database/sql"
"go-common/library/log"
"time"
"github.com/pkg/errors"
)
/*
全新推流结构
所需功能:
1. 创建流 (从老表搬运) done
2. 校验流/读取 done
3. 开关播回调
4. 切上行
5. 清理互推标记
*/
const (
// 创建流
_insertMainStream = "INSERT INTO `main_stream` (room_id, stream_name, `key`, default_vendor, options) VALUES (?, ?, ?, ?, ?);"
// 读取流
_getMainStreamWithoutConds = "SELECT `room_id`, `stream_name`, `key`, `default_vendor`, `origin_upstream`, `streaming`, `last_stream_time`, `options` from `main_stream` WHERE "
_getMultiMainStreamByRID = "SELECT `room_id`, `stream_name`, `key`, `default_vendor`, `origin_upstream`, `streaming`, `last_stream_time`, `options` from `main_stream` WHERE room_id = %d"
// 切上行
_changeDefaultVendor = "UPDATE `main_stream` SET `default_vendor` = ? WHERE `room_id` = ? AND status = 1"
// 切options
_changeOptions = "UPDATE `main_stream` SET `options` = ? WHERE `room_id` = ? AND status = 1 AND `options` = ?"
// 清理互推
_clearAllStreaming = "UPDATE `main_stream` SET `origin_upstream` = 0, `streaming` = 0, `options` = ? WHERE `room_id` = ? AND `options` = ? AND status = 1"
// 开关回调
_notifyMainStreamOrigin = "UPDATE `main_stream` SET `origin_upstream` = ?, `streaming` = ? WHERE `room_id` = ? and `streaming` = ? and status = 1 limit 1"
_notifyMainStreamOriginClose = "UPDATE `main_stream` SET `options` = ?,`origin_upstream` = 0, `streaming` = 0, `last_stream_time` = CURRENT_TIMESTAMP WHERE `room_id` = ? AND `options` = ? AND `status` = 1 limit 1"
_notifyMainStreamForward = "UPDATE `main_stream` SET `streaming` = ? WHERE `room_id` = ? and `streaming` = ? and status = 1 limit 1"
)
// CreateNewStream used to create new Stream record
func (d *Dao) CreateNewStream(c context.Context, stream *model.MainStream) (*model.MainStream, error) {
if stream.RoomID <= 0 {
return stream, fmt.Errorf("room id can not be empty")
}
if stream.StreamName == "" {
return stream, fmt.Errorf("stream name can not be empty")
}
if stream.Key == "" {
h := md5.New()
h.Write([]byte(fmt.Sprintf("%s%d", stream.StreamName, time.Now().Nanosecond())))
stream.Key = hex.EncodeToString(h.Sum(nil))
}
if stream.DefaultVendor == 0 {
stream.DefaultVendor = 1
}
res, err := d.stmtMainStreamCreate.Exec(c, stream.RoomID, stream.StreamName, stream.Key, stream.DefaultVendor, stream.Options)
if err != nil {
return stream, err
}
stream.ID, err = res.LastInsertId()
return stream, nil
}
// GetMainStreamFromDB 从DB中读取流信息
// roomID 和 streamName 可以只传一个,传哪个就用哪个查询,否则必须两者对应
func (d *Dao) GetMainStreamFromDB(c context.Context, roomID int64, streamName string) (*model.MainStream, error) {
if roomID <= 0 && streamName == "" {
return nil, errors.New("roomID and streamName cannot be empty at SAME time")
}
var row *sql.Row
if roomID > 0 && streamName != "" {
q := fmt.Sprintf("%s `room_id` = ? AND `stream_name` = ? AND status = 1", _getMainStreamWithoutConds)
row = d.db.QueryRow(c, q, roomID, streamName)
} else if roomID > 0 && streamName == "" {
q := fmt.Sprintf("%s `room_id` = ? AND status = 1", _getMainStreamWithoutConds)
row = d.db.QueryRow(c, q, roomID)
} else if roomID <= 0 && streamName != "" {
q := fmt.Sprintf("%s `stream_name` = ? AND status = 1", _getMainStreamWithoutConds)
row = d.db.QueryRow(c, q, streamName)
}
stream := new(model.MainStream)
err := row.Scan(&stream.RoomID, &stream.StreamName, &stream.Key,
&stream.DefaultVendor, &stream.OriginUpstream, &stream.Streaming,
&stream.LastStreamTime, &stream.Options)
if err != nil {
return nil, err
}
return stream, nil
}
// GetMultiMainStreamFromDB 批量从main-stream读取
func (d *Dao) GetMultiMainStreamFromDB(c context.Context, rids []int64) (mainStream []*model.MainStream, err error) {
len := len(rids)
muSql := ""
for i := 0; i < len; i++ {
ss := fmt.Sprintf(_getMultiMainStreamByRID, rids[i])
if i == 0 {
muSql = fmt.Sprintf("%s%s", muSql, ss)
} else {
muSql = fmt.Sprintf("%s UNION %s", muSql, ss)
}
}
var rows *sql.Rows
if rows, err = d.db.Query(c, muSql); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
stream := new(model.MainStream)
if err = rows.Scan(&stream.RoomID, &stream.StreamName, &stream.Key,
&stream.DefaultVendor, &stream.OriginUpstream, &stream.Streaming,
&stream.LastStreamTime, &stream.Options); err != nil {
if err == sql.ErrNoRows {
continue
}
err = errors.WithStack(err)
return
}
mainStream = append(mainStream, stream)
}
err = rows.Err()
return
}
// ChangeDefaultVendor 切换默认上行
func (d *Dao) ChangeDefaultVendor(c context.Context, roomID int64, newVendor int64) error {
if roomID <= 0 {
return errors.New("invalid roomID")
}
if _, ok := common.BitwiseMapName[newVendor]; !ok {
return errors.New("invalid vendor")
}
_, err := d.stmtMainStreamChangeDefaultVendor.Exec(c, newVendor, roomID)
return err
}
// ChangeMainStreamOptions 切换Options
func (d *Dao) ChangeMainStreamOptions(c context.Context, roomID int64, newOptions int64, options int64) error {
if roomID <= 0 {
return errors.New("invalid roomID")
}
_, err := d.stmtMainStreamChangeOptions.Exec(c, newOptions, roomID, options)
return err
}
// ClearMainStreaming 清理互推标记
func (d *Dao) ClearMainStreaming(c context.Context, roomID int64, newoptions int64, options int64) error {
if roomID <= 0 {
return errors.New("invalid roomID")
}
_, err := d.stmtMainStreamClearAllStreaming.Exec(c, newoptions, roomID, options)
return err
}
// MainStreamNotify 开关播回调
// @param roomID 房间号
// @param vendor 上行 CDN 位
// @param isOpen 是否是开播 true 开播 false 关播
// @param isOrigin 是否是原始上行 true 是 false 转推
func (d *Dao) MainStreamNotify(c context.Context, roomID, vendor int64, isOpen bool, isOrigin bool, options int64, newoptions int64) error {
if _, ok := common.BitwiseMapName[vendor]; !ok {
return fmt.Errorf("Unknow vendor %d", vendor)
}
log.Infov(c, log.KV("roomID", roomID), log.KV("vendor", vendor), log.KV("isOpen", isOpen), log.KV("isOrigin", isOrigin), log.KV("options", options), log.KV("newoptions", newoptions))
// "UPDATE `main_stream` SET `origin_upstream` = ?, `streaming` = ? WHERE `room_id` = ? AND `streaming` = ? AND `origin_upstream` = 0 and status = 1 limit 1"
// "UPDATE `main_stream` SET `streaming` = ? WHERE `room_id` = ? and `streaming` = ? and status = 1 limit 1"
ms, err := d.GetMainStreamFromDB(c, roomID, "")
if ms == nil || err != nil {
return fmt.Errorf("cannot found main stream by roomid (%d) with error%v", roomID, err)
}
// 开播
if isOpen {
if isOrigin { // 主推
_, err := d.db.Exec(c, _notifyMainStreamOrigin, vendor, ms.Streaming|vendor, roomID, ms.Streaming)
return err
}
// 转推
_, err := d.db.Exec(c, _notifyMainStreamForward, ms.Streaming|vendor, roomID, ms.Streaming)
return err
} else {
log.Infov(c, log.KV("----test----", fmt.Sprintf("---- %v ----- %v ---- %v ---- %v -", _notifyMainStreamOriginClose, newoptions, roomID, options)))
// 关播的时候, 必须是当前的origin=传递过来的cdn才可以关 修复开关播的时序性问题
if isOrigin && ms.OriginUpstream == vendor {
_, err := d.db.Exec(c, _notifyMainStreamOriginClose, newoptions, roomID, options)
return err
}
// 转推
_, err := d.db.Exec(c, _notifyMainStreamForward, ms.Streaming&^vendor, roomID, ms.Streaming)
return err
}
}

View File

@@ -0,0 +1,99 @@
package dao
import (
"context"
"go-common/app/service/video/stream-mng/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoCreateNewStream(t *testing.T) {
convey.Convey("CreateNewStream", t, func(ctx convey.C) {
var (
c = context.Background()
stream = &model.MainStream{
RoomID: 123456,
StreamName: "tetststest",
Key: "testtestetste",
}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1, err := d.CreateNewStream(c, stream)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoGetMainStreamFromDB(t *testing.T) {
convey.Convey("GetMainStreamFromDB", t, func(ctx convey.C) {
var (
c = context.Background()
roomID = int64(11891462)
streamName = ""
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1, err := d.GetMainStreamFromDB(c, roomID, streamName)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoChangeDefaultVendor(t *testing.T) {
convey.Convey("ChangeDefaultVendor", t, func(ctx convey.C) {
var (
c = context.Background()
roomID = int64(11891462)
newVendor = int64(2)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.ChangeDefaultVendor(c, roomID, newVendor)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoClearMainStreaming(t *testing.T) {
convey.Convey("ClearMainStreaming", t, func(ctx convey.C) {
var (
c = context.Background()
roomID = int64(11891462)
options = int64(0)
newoptions = int64(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.ClearMainStreaming(c, roomID, newoptions, options)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoMainStreamNotify(t *testing.T) {
convey.Convey("MainStreamNotify", t, func(ctx convey.C) {
var (
c = context.Background()
roomID = int64(11891462)
vendor = int64(2)
isOpen bool
isOrigin bool
options = int64(0)
newoptions = int64(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.MainStreamNotify(c, roomID, vendor, isOpen, isOrigin, options, newoptions)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}

View File

@@ -0,0 +1,383 @@
package dao
import (
"context"
"errors"
"fmt"
"go-common/app/service/video/stream-mng/common"
"go-common/app/service/video/stream-mng/model"
"go-common/library/log"
"go-common/library/net/metadata"
"go-common/library/sync/errgroup"
)
// RawStreamFullInfo 直接从数据库中查询流信息,可传入流名, 也可传入rid
func (d *Dao) RawStreamFullInfo(c context.Context, id int64, sname string) (res *model.StreamFullInfo, err error) {
var (
official []*model.OfficialStream
backup []*model.StreamBase
mainStream *model.MainStream
)
if sname != "" {
official, err = d.GetOfficialStreamByName(c, sname)
// 可以从原表中查询到
if err == nil && official != nil && len(official) > 0 {
id = official[0].RoomID
goto END
}
var backUpInfo *model.BackupStream
// 原表中查询不到
backUpInfo, err = d.GetBackupStreamByStreamName(c, sname)
if err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("sql backup_stream err = %v", err)))
return
}
if backUpInfo == nil {
err = fmt.Errorf("can not find any info by %s", sname)
return
}
id = backUpInfo.RoomID
}
END:
// todo 这里用老的errgroup 新errgroup2 暂时未有人用,bug未知
group, errCtx := errgroup.WithContext(c)
// 如果还未查sv_ls_stream则需要查询
if id > 0 && len(official) == 0 {
group.Go(func() (err error) {
log.Warn("group offical")
if official, err = d.GetOfficialStreamByRoomID(errCtx, id); err != nil {
log.Errorv(errCtx, log.KV("log", fmt.Sprintf("group offical err=%v", err)))
}
return nil
})
}
if id > 0 {
group.Go(func() (err error) {
log.Warn("group main")
if mainStream, err = d.GetMainStreamFromDB(errCtx, id, ""); err != nil {
log.Errorv(errCtx, log.KV("log", fmt.Sprintf("group main err=%v", err)))
}
return nil
})
group.Go(func() (err error) {
log.Warn("group back")
back, err := d.GetBackupStreamByRoomID(errCtx, id)
if err != nil {
log.Errorv(errCtx, log.KV("log", fmt.Sprintf("group backup err=%v", err)))
} else {
backup = d.formatBackup2BaseInfo(c, back)
}
return nil
})
}
err = group.Wait()
if err != nil {
return
}
if len(official) == 0 {
err = fmt.Errorf("can not find any info by room_id=%d", id)
return
}
return d.formatStreamFullInfo(c, official, backup, mainStream)
}
// RawStreamRIDByName 查询rid
func (d *Dao) RawStreamRIDByName(c context.Context, sname string) (res *model.StreamFullInfo, err error) {
return d.RawStreamFullInfo(c, 0, sname)
}
// RawMultiStreamInfo 批量查询流信息
func (d *Dao) RawMultiStreamInfo(c context.Context, rids []int64) (res map[int64]*model.StreamFullInfo, err error) {
var (
official []*model.OfficialStream
backup []*model.BackupStream
mainStream []*model.MainStream
)
group, errCtx := errgroup.WithContext(c)
group.Go(func() (err error) {
if official, err = d.GetMultiOfficalStreamByRID(errCtx, rids); err != nil {
log.Errorv(errCtx, log.KV("log", fmt.Sprintf("group offical err=%v", err)))
}
return nil
})
group.Go(func() (err error) {
if backup, err = d.GetMultiBackupStreamByRID(errCtx, rids); err != nil {
log.Errorv(errCtx, log.KV("log", fmt.Sprintf("group back err=%v", err)))
}
return nil
})
group.Go(func() (err error) {
if mainStream, err = d.GetMultiMainStreamFromDB(errCtx, rids); err != nil {
log.Errorv(errCtx, log.KV("log", fmt.Sprintf("group back err=%v", err)))
}
return nil
})
err = group.Wait()
if err != nil {
return
}
// 把rid相同的放为一组
ridMapOfficial := map[int64][]*model.OfficialStream{}
for _, v := range official {
ridMapOfficial[v.RoomID] = append(ridMapOfficial[v.RoomID], v)
}
ridMapBackup := map[int64][]*model.BackupStream{}
for _, v := range backup {
ridMapBackup[v.RoomID] = append(ridMapBackup[v.RoomID], v)
}
ridMapBackupBase := map[int64][]*model.StreamBase{}
for id, v := range ridMapBackup {
ridMapBackupBase[id] = d.formatBackup2BaseInfo(c, v)
}
ridMapMain := map[int64]*model.MainStream{}
for _, v := range mainStream {
ridMapMain[v.RoomID] = v
}
infos := map[int64]*model.StreamFullInfo{}
flag := false
for id, v := range ridMapOfficial {
flag = true
infos[id], _ = d.formatStreamFullInfo(c, v, ridMapBackupBase[id], ridMapMain[id])
}
if flag {
return infos, nil
}
log.Errorv(c, log.KV("log", fmt.Errorf("can not find any info by room_ids=%d", rids)))
return nil, nil
}
// formatStreamFullInfo 格式化流信息
func (d *Dao) formatStreamFullInfo(c context.Context, official []*model.OfficialStream, backup []*model.StreamBase, main *model.MainStream) (*model.StreamFullInfo, error) {
resp := &model.StreamFullInfo{}
resp.List = []*model.StreamBase{}
var roomID int64
roomID = official[0].RoomID
resp.RoomID = official[0].RoomID
base := &model.StreamBase{}
base.StreamName = official[0].Name
base.Type = 1
base.Key = official[0].Key
if main != nil {
base.Options = main.Options
if 4&base.Options == 4 {
base.Wmask = true
}
if 8&base.Options == 8 {
base.Mmask = true
}
}
for _, item := range official {
if item.UpRank == 1 {
if val, ok := common.SrcMapBitwise[item.Src]; ok {
// todo origin为main-stream取
if main != nil {
base.Origin = main.OriginUpstream
} else {
// 做个兜底逻辑, main-stream中没有这个数据但是sv_ls_stream确实在播
base.Origin = val
}
base.DefaultUpStream = val
} else {
// 如果上行不在现在的任意一家, 则重新设置上行
if err := d.UpdateOfficialStreamStatus(c, roomID, common.BVCSrc); err == nil {
if main != nil {
base.Origin = main.OriginUpstream
} else {
base.Origin = common.BitWiseBVC
}
base.DefaultUpStream = common.BitWiseBVC
go func(c context.Context, rid int64, fromOrigin int8, toOrigin int64, sname string) {
d.UpdateStreamStatusCache(c, &model.StreamStatus{
RoomID: rid,
StreamName: sname,
DefaultChange: true,
DefaultUpStream: toOrigin,
})
// 插入日志
d.InsertChangeLog(c, &model.StreamChangeLog{
RoomID: rid,
FromOrigin: int64(fromOrigin),
ToOrigin: toOrigin,
Reason: fmt.Sprintf("上行不在五家CDN,old origin=%d", fromOrigin),
OperateName: "auto_change",
Source: "background",
})
}(metadata.WithContext(c), roomID, item.Src, common.BitWiseBVC, item.Name)
}
}
} else if item.UpRank == 2 {
if val, ok := common.SrcMapBitwise[item.Src]; ok {
base.Forward = append(base.Forward, val)
}
}
}
resp.List = append(resp.List, base)
if len(backup) > 0 {
for _, v := range backup {
resp.List = append(resp.List, v)
}
}
d.liveAside.Do(c, func(ctx context.Context) {
d.diffStreamInfo(ctx, resp, main)
})
return resp, nil
}
// formatBackup2Base backup 格式化为base
func (d *Dao) formatBackup2BaseInfo(c context.Context, back []*model.BackupStream) (resp []*model.StreamBase) {
if len(back) > 0 {
for _, b := range back {
bs := &model.StreamBase{}
bs.StreamName = b.StreamName
bs.Type = 2
bs.Key = b.Key
// 原始上行
bs.Origin = b.OriginUpstream
bs.DefaultUpStream = b.DefaultVendor
bs.Options = b.Options
// 位运算:可满足9家cdn
var n int64
for n = 256; n > 0; n /= 2 {
if (b.Streaming&n) == n && n != bs.Origin {
bs.Forward = append(bs.Forward, n)
}
}
resp = append(resp, bs)
}
}
return
}
// 比较新表和老表
func (d *Dao) diffStreamInfo(c context.Context, info *model.StreamFullInfo, mainStream *model.MainStream) {
if info != nil && info.RoomID != 0 && len(info.List) > 0 {
if mainStream == nil {
d.syncMainStream(c, info.RoomID, "")
log.Infov(c, log.KV("log", fmt.Sprintf("diff_err:can find any info, room_id=%d", info.RoomID)))
return
}
offical := info.List[0]
if mainStream.StreamName != offical.StreamName {
log.Infov(c, log.KV("log", fmt.Sprintf("diff_err:stream name is differentroom_id=%d", info.RoomID)))
return
}
if mainStream.Key != offical.Key {
log.Infov(c, log.KV("log", fmt.Sprintf("diff_err:key is differentroom_id=%d", info.RoomID)))
return
}
if mainStream.DefaultVendor != offical.DefaultUpStream {
log.Infov(c, log.KV("log", fmt.Sprintf("diff_err:DefaultVendor is differentroom_id=%d,main=%d,offical=%d", info.RoomID, mainStream.DefaultVendor, offical.DefaultUpStream)))
return
}
if mainStream.OriginUpstream != 0 && (mainStream.OriginUpstream != mainStream.DefaultVendor) {
log.Infov(c, log.KV("log", fmt.Sprintf("diff_err:OriginUpstream is differentroom_id=%d, main origin=%d, main default=%d", info.RoomID, mainStream.OriginUpstream, mainStream.DefaultVendor)))
return
}
streaming := offical.DefaultUpStream
for _, v := range offical.Forward {
streaming += v
}
if mainStream.Streaming != streaming {
log.Infov(c, log.KV("log", fmt.Sprintf("diff_err:Streaming is differentroom_id=%d, main=%d, offical=%d", info.RoomID, mainStream.Streaming, streaming)))
return
}
}
}
func (d *Dao) syncMainStream(c context.Context, roomID int64, streamName string) error {
if roomID <= 0 && streamName == "" {
return errors.New("invalid params")
}
var err error
exists, err := d.GetMainStreamFromDB(c, roomID, streamName)
if err != nil && err.Error() != "sql: no rows in result set" {
log.Errorv(c, log.KV("log", fmt.Sprintf("sync_stream_data_error = %v", err)))
return err
}
if exists != nil && (exists.RoomID == roomID || exists.StreamName == streamName) {
return nil
}
var full *model.StreamFullInfo
if roomID > 0 && streamName == "" {
full, err = d.StreamFullInfo(c, roomID, "")
} else if roomID <= 0 && streamName != "" {
full, err = d.StreamFullInfo(c, 0, streamName)
}
if err != nil {
return err
}
if full == nil {
return errors.New("unknow response")
}
for _, ss := range full.List {
if ss.Type == 1 {
ms := &model.MainStream{
RoomID: full.RoomID,
StreamName: ss.StreamName,
Key: ss.Key,
DefaultVendor: ss.DefaultUpStream,
Status: 1,
}
if ms.DefaultVendor == 0 {
ms.DefaultVendor = 1
}
_, err := d.CreateNewStream(c, ms)
if err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("sync_stream_data_error = %v", err)))
}
break
}
}
return nil
}

View File

@@ -0,0 +1,73 @@
package dao
import (
"context"
"go-common/app/service/video/stream-mng/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoRawStreamFullInfo(t *testing.T) {
convey.Convey("RawStreamFullInfo", t, func(ctx convey.C) {
var (
c = context.Background()
id = int64(11891462)
sname = ""
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
res, err := d.RawStreamFullInfo(c, id, sname)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoRawStreamRIDByName(t *testing.T) {
convey.Convey("RawStreamRIDByName", t, func(ctx convey.C) {
var (
c = context.Background()
sname = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
res, err := d.RawStreamRIDByName(c, sname)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoRawMultiStreamInfo(t *testing.T) {
convey.Convey("RawMultiStreamInfo", t, func(ctx convey.C) {
var (
c = context.Background()
rids = []int64{11891462}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
res, err := d.RawMultiStreamInfo(c, rids)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaodiffStreamInfo(t *testing.T) {
convey.Convey("diffStreamInfo", t, func(ctx convey.C) {
var (
c = context.Background()
info = &model.StreamFullInfo{}
main = &model.MainStream{}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
d.diffStreamInfo(c, info, main)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}

View File

@@ -0,0 +1,28 @@
package dao
import (
"context"
"fmt"
"go-common/library/log"
)
const (
_setOriginStreamingStatus = "UPDATE `sv_ls_stream` SET `up_rank` = ? where `room_id` = ? and `src` = ? and `up_rank` = ? LIMIT 1;"
)
// SetOriginStreamingStatus 用于设置 老版本数据结构的 推流状态
func (d *Dao) SetOriginStreamingStatus(c context.Context, rid int64, src, from, to int) error {
res, err := d.stmtLegacyStreamNotify.Exec(c, to, rid, src, from)
if err != nil {
return err
}
er, err := res.RowsAffected()
if err != nil {
return err
}
if er == 0 {
log.Infow(c, "no_record_updated", fmt.Sprintf("%d_%d_%d_%d", rid, src, from, to))
return nil
}
return err
}

View File

@@ -0,0 +1,26 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoSetOriginStreamingStatus(t *testing.T) {
convey.Convey("SetOriginStreamingStatus", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
src = int(1)
from = int(1)
to = int(16)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.SetOriginStreamingStatus(c, rid, src, from, to)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}

View File

@@ -0,0 +1,189 @@
package dao
import (
"context"
"fmt"
"go-common/app/service/video/stream-mng/model"
"go-common/library/database/sql"
"go-common/library/log"
"time"
"github.com/pkg/errors"
)
const (
_getOfficialStreamByName = "SELECT id, room_id, src, `name`, `key`, up_rank, down_rank, `status` FROM `sv_ls_stream` WHERE name = ?"
_getOfficialStreamByRoomID = "SELECT id, room_id, src, `name`, `key`, up_rank, down_rank, `status` FROM `sv_ls_stream` WHERE room_id = ?"
_getMultiOfficalStreamByRID = "SELECT id, room_id, src, `name`, `key`, up_rank, down_rank, `status` FROM `sv_ls_stream` WHERE room_id = %d"
_insertOfficialStream = "INSERT INTO `sv_ls_stream` (room_id, `name`, `src`,`key`, `status`, up_rank, down_rank, last_status_updated_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)"
_updateUpOfficialStreamStatus = "UPDATE `sv_ls_stream` SET `up_rank` = 1,`last_status_updated_at` = CURRENT_TIMESTAMP WHERE `room_id` = ? and `src` = ?"
_updateForwardOfficialStreamStatus = "UPDATE `sv_ls_stream` SET `up_rank` = 0,`last_status_updated_at` = CURRENT_TIMESTAMP WHERE `room_id` = ? and `src` != ?"
_updateOfficalStreamUpRankStatus = "UPDATE `sv_ls_stream` SET `up_rank` = ?,`last_status_updated_at` = CURRENT_TIMESTAMP WHERE `room_id` = ? AND `up_rank` = ?;"
)
// GetOfficialStreamByName 根据流名查流信息, 可以获取多条记录
func (d *Dao) GetOfficialStreamByName(c context.Context, name string) (infos []*model.OfficialStream, err error) {
var rows *sql.Rows
if rows, err = d.db.Query(c, _getOfficialStreamByName, name); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
info := new(model.OfficialStream)
if err = rows.Scan(&info.ID, &info.RoomID,
&info.Src, &info.Name, &info.Key,
&info.UpRank, &info.DownRank, &info.Status); err != nil {
log.Warn("sv_ls_stream sql err = %v", err)
err = errors.WithStack(err)
infos = nil
return
}
infos = append(infos, info)
}
err = rows.Err()
return
}
// GetOfficialStreamByRoomID 根据roomid查询流信息, 可以获取多条记录
func (d *Dao) GetOfficialStreamByRoomID(c context.Context, rid int64) (infos []*model.OfficialStream, err error) {
var rows *sql.Rows
if rows, err = d.db.Query(c, _getOfficialStreamByRoomID, rid); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
info := new(model.OfficialStream)
if err = rows.Scan(&info.ID, &info.RoomID,
&info.Src, &info.Name, &info.Key,
&info.UpRank, &info.DownRank, &info.Status); err != nil {
log.Warn("sv_ls_stream sql err = %v", err)
err = errors.WithStack(err)
infos = nil
return
}
infos = append(infos, info)
}
err = rows.Err()
// 查询多个数据,不会报错, 只能判断为空
if err == nil && len(infos) == 0 && rid < 10000 {
infos = append(infos, &model.OfficialStream{
RoomID: rid,
Name: "miss",
Src: 32,
UpRank: 1,
Key: "miss",
})
}
return
}
// GetMultiOfficalStreamByRID 批量获取
func (d *Dao) GetMultiOfficalStreamByRID(c context.Context, rids []int64) (infos []*model.OfficialStream, err error) {
len := len(rids)
muSql := ""
for i := 0; i < len; i++ {
ss := fmt.Sprintf(_getMultiOfficalStreamByRID, rids[i])
if i == 0 {
muSql = fmt.Sprintf("%s%s", muSql, ss)
} else {
muSql = fmt.Sprintf("%s UNION %s", muSql, ss)
}
}
var rows *sql.Rows
if rows, err = d.db.Query(c, muSql); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
info := new(model.OfficialStream)
if err = rows.Scan(&info.ID, &info.RoomID,
&info.Src, &info.Name, &info.Key,
&info.UpRank, &info.DownRank, &info.Status); err != nil {
log.Warn("sv_ls_stream sql err = %v", err)
if err == sql.ErrNoRows {
continue
} else {
err = errors.WithStack(err)
infos = nil
return infos, err
}
}
infos = append(infos, info)
}
err = rows.Err()
return
}
// CreateOfficialStream 创建正式流
func (d *Dao) CreateOfficialStream(c context.Context, infos []*model.OfficialStream) (err error) {
tx, err := d.db.Begin(c)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
ts := time.Now().Format("2006-01-02 15:04:05")
for _, v := range infos {
if _, err = d.stmtLegacyStreamCreate.Exec(c, v.RoomID, v.Name, v.Src, v.Key, v.Status, v.UpRank, v.DownRank, ts, ts, ts); err != nil {
return err
}
}
err = tx.Commit()
return err
}
// UpdateOfficialStreamStatus 切换cdn时更新流状态
func (d *Dao) UpdateOfficialStreamStatus(c context.Context, rid int64, src int8) (err error) {
// 事务操作, 同时操作多条记录,任何一条失败均回滚
tx, err := d.db.Begin(c)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
if _, err = d.stmtLegacyStreamEnableNewUpRank.Exec(c, rid, src); err != nil {
return err
}
if _, err = d.stmtLegacyStreamDisableUpRank.Exec(c, rid, src); err != nil {
return err
}
err = tx.Commit()
return err
}
// UpdateOfficialUpRankStatus 清理互推标准更新up_rank
func (d *Dao) UpdateOfficialUpRankStatus(c context.Context, rid int64, whereSrc int8, toSrc int8) error {
res, err := d.stmtLegacyStreamClearStreamFoward.Exec(c, toSrc, rid, whereSrc)
if err != nil {
return err
}
_, err = res.RowsAffected()
return err
}

View File

@@ -0,0 +1,40 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoGetOfficialStreamByName(t *testing.T) {
convey.Convey("GetOfficialStreamByName", t, func(ctx convey.C) {
var (
c = context.Background()
name = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
infos, err := d.GetOfficialStreamByName(c, name)
ctx.Convey("Then err should be nil.infos should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(infos, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoGetOfficialStreamByRoomID(t *testing.T) {
convey.Convey("GetOfficialStreamByRoomID", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
infos, err := d.GetOfficialStreamByRoomID(c, rid)
ctx.Convey("Then err should be nil.infos should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(infos, convey.ShouldNotBeNil)
})
})
})
}

View File

@@ -0,0 +1,813 @@
package dao
import (
"context"
"fmt"
"go-common/app/service/video/stream-mng/model"
"go-common/library/cache/redis"
"go-common/library/log"
"strconv"
"strings"
)
/*
kv结构
streamName: room_id
hash结构如下
// 存储所有的流名
room_id + name + name1,name2, name3
// 默认直推
room_id + name1:default + src
// 一个流名下的直推
room_id + name1:origin + src
// 一个流名下的转推
room_id + name1:streaming + src
// 一个流名下的key
room_id + name1:key + key
。。。
*/
const (
// kv => name: rid
_streamNameKey = "mng:name:%s"
// hash key => rid: field :value
_streamRooIDKey = "mng:rid:%d"
_streamRoomFieldAllName = "mng:name:all"
_streamRoomFieldDefault = "%s:default"
_streamRoomFieldOrigin = "%s:origin"
_streamRoomFieldForward = "%s:forward"
_streamRoomFieldSecret = "%s:key"
_streamRoomFieldOption = "%s:options"
_streamRoomFieldHot = "%d:hot"
_streamLastCDN = "last:cdn:%d"
// 切流记录
_streamChangeSrc = "change:src:%d"
//房间冷热流
_streamRoomHot = "room:hot:%d"
// 存12小时
_streamExpireTime = 4 * 3600
// stream_name map room_id 流名对应房间号 这个是不变的
_nameExpireTime = 365 * 86400
// 存一年
_lastCDNExpireTime = 365 * 86400
_changeSrcExpireTime = 365 * 86400
)
func (d *Dao) getStreamNamekey(streamName string) string {
return fmt.Sprintf(_streamNameKey, streamName)
}
func (d *Dao) getRoomIDKey(rid int64) string {
return fmt.Sprintf(_streamRooIDKey, rid)
}
func (d *Dao) getRoomFieldDefaultKey(streamName string) string {
return fmt.Sprintf(_streamRoomFieldDefault, streamName)
}
func (d *Dao) getRoomFieldOriginKey(streamName string) string {
return fmt.Sprintf(_streamRoomFieldOrigin, streamName)
}
func (d *Dao) getRoomFieldForwardKey(streamName string) string {
return fmt.Sprintf(_streamRoomFieldForward, streamName)
}
func (d *Dao) getRoomFieldSecretKey(streamName string) string {
return fmt.Sprintf(_streamRoomFieldSecret, streamName)
}
func (d *Dao) getRoomFieldOption(streamName string) string {
return fmt.Sprintf(_streamRoomFieldOption, streamName)
}
func (d *Dao) getRoomFieldHotKey(rid int64) string {
return fmt.Sprintf(_streamRoomFieldHot, rid)
}
func (d *Dao) getRoomHotKey(rid int64) string {
return fmt.Sprintf(_streamRoomHot, rid)
}
func (d *Dao) getLastCDNKey(rid int64) string {
return fmt.Sprintf(_streamLastCDN, rid)
}
func (d *Dao) getChangeSrcKey(rid int64) string {
return fmt.Sprintf(_streamChangeSrc, rid)
}
// CacheStreamFullInfo 从缓存取流信息, 可传入流名, 也可以传入rid
func (d *Dao) CacheStreamFullInfo(c context.Context, rid int64, sname string) (res *model.StreamFullInfo, err error) {
if sname != "" {
infos, err := d.CacheStreamRIDByName(c, sname)
if err != nil {
return nil, err
}
if infos == nil || infos.RoomID <= 0 {
return nil, fmt.Errorf("can not find any info by sname =%s", sname)
}
rid = infos.RoomID
}
// 先从本地缓存中取
res = d.loadStreamInfo(c, rid)
if res != nil {
log.Warn("get from local cache")
return res, nil
}
conn := d.redis.Get(c)
defer conn.Close()
roomKey := d.getRoomIDKey(rid)
hotKey := d.getRoomFieldHotKey(rid)
// 先判断过期时间是否为-1 为-1则删除
//ttl, _ := redis.Int(conn.Do("TTL", roomKey))
//if ttl == -1 {
// d.DeleteStreamByRIDFromCache(c, rid)
// return nil, nil
//}
values, err := redis.StringMap(conn.Do("HGETALL", roomKey))
log.Warn("%v", values)
if err != nil {
return nil, err
}
if len(values) == 0 {
return nil, nil
}
resp := model.StreamFullInfo{}
hot, _ := strconv.ParseInt(values[hotKey], 10, 64)
resp.Hot = hot
allNames := strings.Split(values[_streamRoomFieldAllName], "|")
for _, n := range allNames {
defaultUpStream := d.getRoomFieldDefaultKey(n)
originKey := d.getRoomFieldOriginKey(n)
forwardKey := d.getRoomFieldForwardKey(n)
key := d.getRoomFieldSecretKey(n)
options := d.getRoomFieldOption(n)
if values[originKey] != "" && values[forwardKey] != "" && values[key] != "" && values[defaultUpStream] != "" {
resp.RoomID = rid
base := model.StreamBase{}
base.StreamName = n
or, _ := strconv.ParseInt(values[originKey], 10, 64)
base.Origin = or
de, _ := strconv.ParseInt(values[defaultUpStream], 10, 64)
base.DefaultUpStream = de
if de == 0 {
return nil, fmt.Errorf("default is 0")
}
forward, _ := strconv.ParseInt(values[forwardKey], 10, 64)
var num int64
for num = 256; num > 0; num /= 2 {
if ((forward & num) == num) && (num != or) {
base.Forward = append(base.Forward, num)
}
}
op, _ := strconv.ParseInt(values[options], 10, 64)
base.Options = op
//这里判断是否有wmask mmask的流
if 4&op == 4 {
base.Wmask = true
}
if 8&op == 8 {
base.Mmask = true
}
base.Key = values[key]
if strings.Contains(n, "_bs_") {
base.Type = 2
} else {
base.Type = 1
}
resp.List = append(resp.List, &base)
}
}
if len(resp.List) > 0 {
// 存储到local cache
d.storeStreamInfo(c, &resp)
return &resp, nil
}
return nil, nil
}
// AddCacheStreamFullInfo 修改缓存数据
func (d *Dao) AddCacheStreamFullInfo(c context.Context, id int64, stream *model.StreamFullInfo) error {
if stream == nil || stream.RoomID <= 0 {
return nil
}
conn := d.redis.Get(c)
defer func() {
//conn.Do("EXPIRE", d.getRoomIDKey(id), _streamExpireTime)
conn.Close()
}()
streamExpireTime := _streamExpireTime
rid := stream.RoomID
roomKey := d.getRoomIDKey(rid)
allName := ""
len := 0
for _, v := range stream.List {
len++
allName = fmt.Sprintf("%s%s|", allName, v.StreamName)
// kv 设置流名和room_id映射关系
nameKey := d.getStreamNamekey(v.StreamName)
if err := conn.Send("SET", nameKey, rid); err != nil {
return fmt.Errorf("conn.Do(set, %s, %d) error(%v)", nameKey, rid, err)
}
if err := conn.Send("EXPIRE", nameKey, _nameExpireTime); err != nil {
return fmt.Errorf("conn.Do(EXPIRE, %s,%d) error(%v)", nameKey, _nameExpireTime, err)
}
// hash 设置room_id下的field 和key
field := d.getRoomFieldDefaultKey(v.StreamName)
if v.DefaultUpStream == 0 {
return fmt.Errorf("rid= %v, default is 0", roomKey)
}
if err := conn.Send("HSET", roomKey, field, v.DefaultUpStream); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, field, v.DefaultUpStream, err)
}
if len == 1 {
if err := conn.Send("EXPIRE", roomKey, streamExpireTime); err != nil {
log.Infov(c, log.KV("conn.EXPIRE error", err.Error()))
return fmt.Errorf("conn.Do(EXPIRE, %s, %d) error(%v)", roomKey, streamExpireTime, err)
}
}
field = d.getRoomFieldOriginKey(v.StreamName)
if err := conn.Send("HSET", roomKey, field, v.Origin); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, field, v.Origin, err)
}
field = d.getRoomFieldForwardKey(v.StreamName)
var num int64
for _, f := range v.Forward {
num += f
}
if err := conn.Send("HSET", roomKey, field, num); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, field, v.Forward, err)
}
field = d.getRoomFieldSecretKey(v.StreamName)
if err := conn.Send("HSET", roomKey, field, v.Key); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, field, v.Key, err)
}
field = d.getRoomFieldOption(v.StreamName)
if err := conn.Send("HSET", roomKey, field, v.Options); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, field, v.Options, err)
}
}
// 去除最后的|
allName = strings.Trim(allName, "|")
//log.Warn("%v", allName)
if err := conn.Send("HSET", roomKey, _streamRoomFieldAllName, allName); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, _streamRoomFieldAllName, allName, err)
}
if err := conn.Flush(); err != nil {
log.Infov(c, log.KV("conn.Flush error(%v)", err.Error()))
return fmt.Errorf("conn.Flush error(%v)", err)
}
for i := 0; i < 7*len+2; i++ {
if _, err := conn.Receive(); err != nil {
log.Infov(c, log.KV("conn.Receive error(%v)", err.Error()))
return fmt.Errorf("conn.Receive error(%v)", err)
}
}
return nil
}
// CacheStreamRIDByName 根据流名查房间号
func (d *Dao) CacheStreamRIDByName(c context.Context, sname string) (res *model.StreamFullInfo, err error) {
conn := d.redis.Get(c)
defer conn.Close()
nameKey := d.getStreamNamekey(sname)
rid, err := redis.Int64(conn.Do("GET", nameKey))
if err != nil {
return nil, err
}
if rid <= 0 {
return nil, nil
}
res = &model.StreamFullInfo{
RoomID: rid,
}
return res, nil
}
// AddCacheStreamRIDByName 增加缓存
func (d *Dao) AddCacheStreamRIDByName(c context.Context, sname string, stream *model.StreamFullInfo) error {
return d.AddCacheStreamFullInfo(c, 0, stream)
}
// AddCacheMultiStreamInfo 批量增加redis
func (d *Dao) AddCacheMultiStreamInfo(c context.Context, res map[int64]*model.StreamFullInfo) error {
conn := d.redis.Get(c)
defer func() {
//for _, stream := range res {
// conn.Do("EXPIRE", d.getRoomIDKey(stream.RoomID), _streamExpireTime)
//}
conn.Close()
}()
count := 0
for _, stream := range res {
streamExpireTime := _streamExpireTime
rid := stream.RoomID
roomKey := d.getRoomIDKey(rid)
allName := ""
len := 0
for _, v := range stream.List {
len++
allName = fmt.Sprintf("%s%s|", allName, v.StreamName)
// kv 设置流名和room_id映射关系
nameKey := d.getStreamNamekey(v.StreamName)
if err := conn.Send("SET", nameKey, rid); err != nil {
return fmt.Errorf("conn.Do(set, %s, %d) error(%v)", nameKey, rid, err)
}
if err := conn.Send("EXPIRE", nameKey, _nameExpireTime); err != nil {
return fmt.Errorf("conn.Do(EXPIRE, %s,%d) error(%v)", nameKey, _nameExpireTime, err)
}
// hash 设置room_id下的field 和key
field := d.getRoomFieldDefaultKey(v.StreamName)
if err := conn.Send("HSET", roomKey, field, v.DefaultUpStream); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, field, v.DefaultUpStream, err)
}
if len == 1 {
if err := conn.Send("EXPIRE", roomKey, streamExpireTime); err != nil {
return fmt.Errorf("conn.Do(EXPIRE, %s, %d) error(%v)", roomKey, streamExpireTime, err)
}
}
field = d.getRoomFieldOriginKey(v.StreamName)
if err := conn.Send("HSET", roomKey, field, v.Origin); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, field, v.Origin, err)
}
field = d.getRoomFieldForwardKey(v.StreamName)
var num int64
for _, f := range v.Forward {
num += f
}
if err := conn.Send("HSET", roomKey, field, num); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, field, v.Forward, err)
}
field = d.getRoomFieldSecretKey(v.StreamName)
if err := conn.Send("HSET", roomKey, field, v.Key); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, field, v.Key, err)
}
field = d.getRoomFieldOption(v.StreamName)
if err := conn.Send("HSET", roomKey, field, v.Options); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, field, v.Options, err)
}
}
// 去除最后的|
allName = strings.Trim(allName, "|")
if err := conn.Send("HSET", roomKey, _streamRoomFieldAllName, allName); err != nil {
return fmt.Errorf("conn.Do(HSET, %s, %s, %v) error(%v)", roomKey, _streamRoomFieldAllName, allName, err)
}
count += (7*len + 2)
}
if err := conn.Flush(); err != nil {
return fmt.Errorf("conn.Flush error(%v)", err)
}
// 这里要len
for i := 0; i < count; i++ {
if _, err := conn.Receive(); err != nil {
return fmt.Errorf("conn.Receive error(%v)", err)
}
}
return nil
}
// CacheMultiStreamInfo 批量从redis中获取数据
func (d *Dao) CacheMultiStreamInfo(c context.Context, rids []int64) (res map[int64]*model.StreamFullInfo, err error) {
if len(rids) == 0 {
return nil, nil
}
infos := map[int64]*model.StreamFullInfo{}
// 先从local cache读取
localInfos, missRids := d.loadMultiStreamInfo(c, rids)
//log.Warn("%v=%v", localInfos, missRids)
// 若全部命中local cache,直接返回
if len(missRids) == 0 {
log.Warn("all hit local cache")
return localInfos, nil
}
if len(localInfos) != 0 {
infos = localInfos
}
conn := d.redis.Get(c)
defer conn.Close()
for _, id := range missRids {
key := d.getRoomIDKey(id)
if err := conn.Send("HGETALL", key); err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("redis: conn.Send(HGETALL, %s) error(%v)", key, err)))
return nil, fmt.Errorf("redis: conn.Send(HGETALL, %s) error(%v)", key, err)
}
}
if err := conn.Flush(); err != nil {
return nil, fmt.Errorf("redis: conn.Flush error(%v)", err)
}
for i := 0; i < len(missRids); i++ {
if values, err := redis.StringMap(conn.Receive()); err == nil {
if len(values) == 0 {
continue
}
item := model.StreamFullInfo{}
hotKey := d.getRoomFieldHotKey(missRids[i])
hot, _ := strconv.ParseInt(values[hotKey], 10, 64)
item.Hot = hot
allNames := strings.Split(values[_streamRoomFieldAllName], "|")
for _, n := range allNames {
defaultUpStream := d.getRoomFieldDefaultKey(n)
originKey := d.getRoomFieldOriginKey(n)
forwardKey := d.getRoomFieldForwardKey(n)
key := d.getRoomFieldSecretKey(n)
options := d.getRoomFieldOption(n)
if values[originKey] != "" && values[forwardKey] != "" && values[key] != "" {
item.RoomID = missRids[i]
base := model.StreamBase{}
base.StreamName = n
or, _ := strconv.ParseInt(values[originKey], 10, 64)
base.Origin = or
de, _ := strconv.ParseInt(values[defaultUpStream], 10, 64)
base.DefaultUpStream = de
forward, _ := strconv.ParseInt(values[forwardKey], 10, 64)
var num int64
for num = 256; num > 0; num /= 2 {
if ((forward & num) == num) && (num != or) {
base.Forward = append(base.Forward, num)
}
}
op, _ := strconv.ParseInt(values[options], 10, 64)
base.Options = op
//这里判断是否有wmask mmask的流
if 4&op == 4 {
base.Wmask = true
}
if 8&op == 8 {
base.Mmask = true
}
base.Key = values[key]
if strings.Contains(n, "_bs_") {
base.Type = 2
} else {
base.Type = 1
}
item.List = append(item.List, &base)
}
}
if len(item.List) > 0 {
//log.Warn("miss=%v", missRids[i])
infos[missRids[i]] = &item
}
}
}
// 更新local cache
d.storeMultiStreamInfo(c, infos)
return infos, nil
}
// UpdateLastCDNCache 设置last cdn
func (d *Dao) UpdateLastCDNCache(c context.Context, rid int64, origin int64) error {
conn := d.redis.Get(c)
defer conn.Close()
key := d.getLastCDNKey(rid)
if err := conn.Send("SET", key, origin); err != nil {
return fmt.Errorf("redis: conn.Send(SET, %s, %v) error(%v)", key, origin, err)
}
if err := conn.Send("EXPIRE", key, _lastCDNExpireTime); err != nil {
return fmt.Errorf("redis: conn.Send(EXPIRE key(%s) expire(%d)) error(%v)", key, _lastCDNExpireTime, err)
}
if err := conn.Flush(); err != nil {
return fmt.Errorf("redis: conn.Flush error(%v)", err)
}
for i := 0; i < 2; i++ {
if _, err := conn.Receive(); err != nil {
return fmt.Errorf("redis: conn.Receive error(%v)", err)
}
}
return nil
}
// UpdateChangeSrcCache 切流
func (d *Dao) UpdateChangeSrcCache(c context.Context, rid int64, origin int64) error {
conn := d.redis.Get(c)
defer conn.Close()
key := d.getChangeSrcKey(rid)
if err := conn.Send("SET", key, origin); err != nil {
return fmt.Errorf("redis: conn.Send(SET, %s, %v) error(%v)", key, origin, err)
}
if err := conn.Send("EXPIRE", key, _changeSrcExpireTime); err != nil {
return fmt.Errorf("redis: conn.Send(EXPIRE key(%s) expire(%d)) error(%v)", key, _changeSrcExpireTime, err)
}
if err := conn.Flush(); err != nil {
return fmt.Errorf("redis: conn.Flush error(%v)", err)
}
for i := 0; i < 2; i++ {
if _, err := conn.Receive(); err != nil {
return fmt.Errorf("redis: conn.Receive error(%v)", err)
}
}
return nil
}
// UpdateStreamForwardStatus 更新forward值
func (d *Dao) UpdateStreamStatusCache(c context.Context, stream *model.StreamStatus) {
var (
exist bool
err error
conn = d.redis.Get(c)
allName string
)
defer conn.Close()
// 首先判断是否存在
key := d.getRoomIDKey(stream.RoomID)
if exist, err = redis.Bool(conn.Do("EXISTS", key)); err != nil {
if err == redis.ErrNil {
return
}
log.Errorv(c, log.KV("log", fmt.Sprintf("update_status_err=%v", err)))
d.DeleteStreamByRIDFromCache(c, stream.RoomID)
return
}
if !exist {
return
}
// 不传sname 默认为主流
if stream.StreamName == "" {
if allName, err = redis.String(conn.Do("HGET", key, _streamRoomFieldAllName)); err != nil {
if err == redis.ErrNil {
return
}
log.Errorv(c, log.KV("log", fmt.Sprintf("update_status_err=%v", err)))
d.DeleteStreamByRIDFromCache(c, stream.RoomID)
return
}
names := strings.Split(allName, "|")
for _, v := range names {
if !strings.Contains(v, "_bs_") {
stream.StreamName = v
break
}
}
}
count := 0
// 是否是新增备用流
if stream.Add {
var names string
if names, err = redis.String(conn.Do("HGET", key, _streamRoomFieldAllName)); err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("update_status_err=%v", err)))
d.DeleteStreamByRIDFromCache(c, stream.RoomID)
return
}
count++
if err := conn.Send("HSET", key, _streamRoomFieldAllName, fmt.Sprintf("%s|%s", names, stream.StreamName)); err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("update_status_err=%v", err)))
d.DeleteStreamByRIDFromCache(c, stream.RoomID)
return
}
count++
secret := d.getRoomFieldSecretKey(stream.StreamName)
if err := conn.Send("HSET", key, secret, stream.Key); err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("update_status_err=%v", err)))
d.DeleteStreamByRIDFromCache(c, stream.RoomID)
return
}
}
// 如果origin改变
if stream.OriginChange {
count++
originKey := d.getRoomFieldOriginKey(stream.StreamName)
if err := conn.Send("HSET", key, originKey, stream.Origin); err != nil {
// 如果设置失败则删除key
log.Errorv(c, log.KV("log", fmt.Sprintf("update_status_err=%v", err)))
d.DeleteStreamByRIDFromCache(c, stream.RoomID)
return
}
}
// forward
if stream.ForwardChange {
count++
forwardKey := d.getRoomFieldForwardKey(stream.StreamName)
if err := conn.Send("HSET", key, forwardKey, stream.Forward); err != nil {
// 如果设置失败则删除key
log.Errorv(c, log.KV("log", fmt.Sprintf("update_status_err=%v", err)))
d.DeleteStreamByRIDFromCache(c, stream.RoomID)
return
}
}
// 切上行
if stream.DefaultChange {
count++
defaultUpKey := d.getRoomFieldDefaultKey(stream.StreamName)
if err := conn.Send("HSET", key, defaultUpKey, stream.DefaultUpStream); err != nil {
// 如果设置失败则删除key
log.Errorv(c, log.KV("log", fmt.Sprintf("update_status_err=%v", err)))
d.DeleteStreamByRIDFromCache(c, stream.RoomID)
return
}
}
//切换options
if stream.OptionsChange {
if stream.Options >= 0 {
count++
optionsKey := d.getRoomFieldOption(stream.StreamName)
if err := conn.Send("HSET", key, optionsKey, stream.Options); err != nil {
// 如果设置失败则删除key
d.DeleteStreamByRIDFromCache(c, stream.RoomID)
return
}
}
}
if err := conn.Flush(); err != nil {
log.Infov(c, log.KV("conn.Flush error(%v)", err.Error()))
return
}
for i := 0; i < count; i++ {
if _, err := conn.Receive(); err != nil {
log.Infov(c, log.KV("conn.Receive error(%v)", err.Error()))
return
}
}
}
// GetLastCDNFromCache 查询上一次cdn
func (d *Dao) GetLastCDNFromCache(c context.Context, rid int64) (int64, error) {
conn := d.redis.Get(c)
defer conn.Close()
key := d.getLastCDNKey(rid)
origin, err := redis.Int64(conn.Do("GET", key))
if err != nil {
if err != redis.ErrNil {
return 0, fmt.Errorf("redis: conn.Do(GET, %s) error(%v)", key, err)
}
}
if origin <= 0 {
return 0, nil
}
return origin, nil
}
// GetChangeSrcFromCache 查询上一次cdn
func (d *Dao) GetChangeSrcFromCache(c context.Context, rid int64) (int64, error) {
conn := d.redis.Get(c)
defer conn.Close()
key := d.getChangeSrcKey(rid)
origin, err := redis.Int64(conn.Do("GET", key))
if err != nil {
if err != redis.ErrNil {
return 0, fmt.Errorf("redis: conn.Do(GET, %s) error(%v)", key, err)
}
}
if origin <= 0 {
return 0, nil
}
return origin, nil
}
// DeleteStreamByRIDFromCache 删除一个房间的的缓存信息
func (d *Dao) DeleteStreamByRIDFromCache(c context.Context, rid int64) (err error) {
// todo 删除内存
// 删除redis
// redis删除失败进行重试确保缓存的信息是最新的
conn := d.redis.Get(c)
defer conn.Close()
roomKey := d.getRoomIDKey(rid)
for i := 0; i < 3; i++ {
_, err = conn.Do("DEL", roomKey)
if err != nil {
log.Error("conn.Do(DEL, %s) error(%v)", roomKey, err)
continue
} else {
return nil
}
}
return err
}
// DeleteLastCDNFromCache 删除上一次到cdn
func (d *Dao) DeleteLastCDNFromCache(c context.Context, rid int64) error {
conn := d.redis.Get(c)
defer conn.Close()
key := d.getLastCDNKey(rid)
_, err := conn.Do("DEL", key)
return err
}
// UpdateRoomOptionsCache 更新Options状态
func (d *Dao) UpdateRoomOptionsCache(c context.Context, rid int64, streamname string, options int64) error {
conn := d.redis.Get(c)
defer conn.Close()
roomKey := d.getRoomIDKey(rid)
optionsKey := d.getRoomFieldOption(streamname)
_, err := conn.Do("HSET", roomKey, optionsKey, options)
return err
}
// UpdateRoomHotStatusCache 更新房间冷热流状态
func (d *Dao) UpdateRoomHotStatusCache(c context.Context, rid int64, hot int64) error {
conn := d.redis.Get(c)
defer conn.Close()
roomKey := d.getRoomIDKey(rid)
hotKey := d.getRoomFieldHotKey(rid)
_, err := conn.Do("HSET", roomKey, hotKey, hot)
return err
}

View File

@@ -0,0 +1,311 @@
package dao
import (
"context"
"go-common/app/service/video/stream-mng/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaogetStreamNamekey(t *testing.T) {
convey.Convey("getStreamNamekey", t, func(ctx convey.C) {
var (
streamName = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := d.getStreamNamekey(streamName)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaogetRoomIDKey(t *testing.T) {
convey.Convey("getRoomIDKey", t, func(ctx convey.C) {
var (
rid = int64(11891462)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := d.getRoomIDKey(rid)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaogetRoomFieldDefaultKey(t *testing.T) {
convey.Convey("getRoomFieldDefaultKey", t, func(ctx convey.C) {
var (
streamName = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := d.getRoomFieldDefaultKey(streamName)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaogetRoomFieldOriginKey(t *testing.T) {
convey.Convey("getRoomFieldOriginKey", t, func(ctx convey.C) {
var (
streamName = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := d.getRoomFieldOriginKey(streamName)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaogetRoomFieldForwardKey(t *testing.T) {
convey.Convey("getRoomFieldForwardKey", t, func(ctx convey.C) {
var (
streamName = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := d.getRoomFieldForwardKey(streamName)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaogetRoomFieldSecretKey(t *testing.T) {
convey.Convey("getRoomFieldSecretKey", t, func(ctx convey.C) {
var (
streamName = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := d.getRoomFieldSecretKey(streamName)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaogetLastCDNKey(t *testing.T) {
convey.Convey("getLastCDNKey", t, func(ctx convey.C) {
var (
rid = int64(11891462)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := d.getLastCDNKey(rid)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaogetChangeSrcKey(t *testing.T) {
convey.Convey("getChangeSrcKey", t, func(ctx convey.C) {
var (
rid = int64(11891462)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := d.getChangeSrcKey(rid)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoCacheStreamFullInfo(t *testing.T) {
convey.Convey("CacheStreamFullInfo", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
sname = ""
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
res, err := d.CacheStreamFullInfo(c, rid, sname)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoAddCacheStreamFullInfo(t *testing.T) {
convey.Convey("AddCacheStreamFullInfo", t, func(ctx convey.C) {
var (
c = context.Background()
id = int64(11891462)
stream = &model.StreamFullInfo{}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.AddCacheStreamFullInfo(c, id, stream)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoCacheStreamRIDByName(t *testing.T) {
convey.Convey("CacheStreamRIDByName", t, func(ctx convey.C) {
var (
c = context.Background()
sname = "live_19148701_6447624"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
res, err := d.CacheStreamRIDByName(c, sname)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoAddCacheStreamRIDByName(t *testing.T) {
convey.Convey("AddCacheStreamRIDByName", t, func(ctx convey.C) {
var (
c = context.Background()
sname = "live_19148701_6447624"
stream = &model.StreamFullInfo{}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.AddCacheStreamRIDByName(c, sname, stream)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoAddCacheMultiStreamInfo(t *testing.T) {
convey.Convey("AddCacheMultiStreamInfo", t, func(ctx convey.C) {
var (
c = context.Background()
res map[int64]*model.StreamFullInfo
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.AddCacheMultiStreamInfo(c, res)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoCacheMultiStreamInfo(t *testing.T) {
convey.Convey("CacheMultiStreamInfo", t, func(ctx convey.C) {
var (
c = context.Background()
rids = []int64{11891462}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
res, err := d.CacheMultiStreamInfo(c, rids)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoUpdateLastCDNCache(t *testing.T) {
convey.Convey("UpdateLastCDNCache", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
origin = int64(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.UpdateLastCDNCache(c, rid, origin)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoUpdateChangeSrcCache(t *testing.T) {
convey.Convey("UpdateChangeSrcCache", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
origin = int64(0)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.UpdateChangeSrcCache(c, rid, origin)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoGetLastCDNFromCache(t *testing.T) {
convey.Convey("GetLastCDNFromCache", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1, err := d.GetLastCDNFromCache(c, rid)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoGetChangeSrcFromCache(t *testing.T) {
convey.Convey("GetChangeSrcFromCache", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1, err := d.GetChangeSrcFromCache(c, rid)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoDeleteStreamByRIDFromCache(t *testing.T) {
convey.Convey("DeleteStreamByRIDFromCache", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.DeleteStreamByRIDFromCache(c, rid)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoDeleteLastCDNFromCache(t *testing.T) {
convey.Convey("DeleteLastCDNFromCache", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.DeleteLastCDNFromCache(c, rid)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}

View File

@@ -0,0 +1,51 @@
package dao
import (
"context"
"fmt"
"go-common/app/service/video/stream-mng/model"
"go-common/library/database/sql"
"time"
"github.com/pkg/errors"
)
const (
_insertStreamChangeLog = "INSERT INTO %s (room_id, from_origin,to_origin, source,operate_name,reason) VALUES (?,?,?,?,?,?)"
_selectStreamChangelog = "SELECT room_id, from_origin, to_origin, source, operate_name,reason, ctime FROM %s where room_id = ? ORDER BY mtime DESC LIMIT ?"
)
// InsertChangeLog 插入日志
func (d *Dao) InsertChangeLog(c context.Context, change *model.StreamChangeLog) error {
// 判断当前年和月
now := time.Now().Format("200601")
tableName := fmt.Sprintf("stream_change_log_%s", now)
_, err := d.db.Exec(c, fmt.Sprintf(_insertStreamChangeLog, tableName), change.RoomID, change.FromOrigin, change.ToOrigin, change.Source, change.OperateName, change.Reason)
return err
}
// GetChangeLogByRoomID 查询日志
func (d *Dao) GetChangeLogByRoomID(c context.Context, rid int64, limit int64) (infos []*model.StreamChangeLog, err error) {
now := time.Now().Format("200601")
tableName := fmt.Sprintf("stream_change_log_%s", now)
var rows *sql.Rows
if rows, err = d.db.Query(c, fmt.Sprintf(_selectStreamChangelog, tableName), rid, limit); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
info := new(model.StreamChangeLog)
if err = rows.Scan(&info.RoomID, &info.FromOrigin, &info.ToOrigin, &info.Source, &info.OperateName, &info.Reason, &info.CTime); err != nil {
err = errors.WithStack(err)
infos = nil
return
}
infos = append(infos, info)
}
err = rows.Err()
return
}

View File

@@ -0,0 +1,48 @@
package dao
import (
"context"
"go-common/app/service/video/stream-mng/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoInsertChangeLog(t *testing.T) {
convey.Convey("InsertChangeLog", t, func(ctx convey.C) {
var (
c = context.Background()
change = &model.StreamChangeLog{
RoomID: 11891462,
FromOrigin: 1,
ToOrigin: 2,
Source: "app",
OperateName: "yy",
Reason: "auto",
}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.InsertChangeLog(c, change)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoGetChangeLogByRoomID(t *testing.T) {
convey.Convey("GetChangeLogByRoomID", t, func(ctx convey.C) {
var (
c = context.Background()
rid = int64(11891462)
limit = int64(1)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
infos, err := d.GetChangeLogByRoomID(c, rid, limit)
ctx.Convey("Then err should be nil.infos should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(infos, convey.ShouldNotBeNil)
})
})
})
}

View File

@@ -0,0 +1,146 @@
package dao
import (
"context"
"github.com/pkg/errors"
"go-common/app/service/video/stream-mng/model"
"go-common/library/database/sql"
)
// 存储上行调度信息
const (
_insertUpStreamInfo = "INSERT INTO `upstream_info` (room_id,cdn,platform,ip,country,city,isp) values(?,?,?,?,?,?,?)"
_getSummaryUpStreamRtmp = "SELECT cdn, count(id) as value FROM upstream_info where mtime >= FROM_UNIXTIME(?) AND mtime <= FROM_UNIXTIME(?) group by cdn"
_getSummaryUpStreamISP = "SELECT isp, count(id) as value FROM upstream_info where mtime >= FROM_UNIXTIME(?) AND mtime <= FROM_UNIXTIME(?) group by isp"
_getSummaryUpStreamCountry = "SELECT country, count(id) as value FROM upstream_info where mtime >= FROM_UNIXTIME(?) AND mtime <= FROM_UNIXTIME(?) group by country"
_getSummaryUpStreamPlatform = "SELECT platform, count(id) as value FROM upstream_info where mtime >= FROM_UNIXTIME(?) AND mtime <= FROM_UNIXTIME(?) group by platform"
_getSummaryUpStreamCity = "SELECT city, count(id) as value FROM upstream_info where mtime >= FROM_UNIXTIME(?) AND mtime <= FROM_UNIXTIME(?) group by city"
)
// CreateUpStreamDispatch 创建一条上行调度信息
func (d *Dao) CreateUpStreamDispatch(c context.Context, info *model.UpStreamInfo) error {
_, err := d.stmtUpStreamDispatch.Exec(c, info.RoomID, info.CDN, info.PlatForm, info.IP, info.Country, info.City, info.ISP)
return err
}
// GetSummaryUpStreamRtmp 得到统计信息
func (d *Dao) GetSummaryUpStreamRtmp(c context.Context, start int64, end int64) (infos []*model.SummaryUpStreamRtmp, err error) {
res := []*model.SummaryUpStreamRtmp{}
var rows *sql.Rows
if rows, err = d.tidb.Query(c, _getSummaryUpStreamRtmp, start, end); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
info := new(model.SummaryUpStreamRtmp)
if err = rows.Scan(&info.CDN, &info.Count); err != nil {
err = errors.WithStack(err)
infos = nil
return
}
res = append(res, info)
}
err = rows.Err()
return res, err
}
// GetSummaryUpStreamISP 得到ISP统计信息
func (d *Dao) GetSummaryUpStreamISP(c context.Context, start int64, end int64) (infos []*model.SummaryUpStreamRtmp, err error) {
res := []*model.SummaryUpStreamRtmp{}
var rows *sql.Rows
if rows, err = d.tidb.Query(c, _getSummaryUpStreamISP, start, end); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
info := new(model.SummaryUpStreamRtmp)
if err = rows.Scan(&info.ISP, &info.Count); err != nil {
err = errors.WithStack(err)
infos = nil
return
}
res = append(res, info)
}
err = rows.Err()
return res, err
}
// GetSummaryUpStreamCountry 得到Country统计信息
func (d *Dao) GetSummaryUpStreamCountry(c context.Context, start int64, end int64) (infos []*model.SummaryUpStreamRtmp, err error) {
res := []*model.SummaryUpStreamRtmp{}
var rows *sql.Rows
if rows, err = d.tidb.Query(c, _getSummaryUpStreamCountry, start, end); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
info := new(model.SummaryUpStreamRtmp)
if err = rows.Scan(&info.Country, &info.Count); err != nil {
err = errors.WithStack(err)
infos = nil
return
}
res = append(res, info)
}
err = rows.Err()
return res, err
}
// GetSummaryUpStreamPlatform 得到Platform统计信息
func (d *Dao) GetSummaryUpStreamPlatform(c context.Context, start int64, end int64) (infos []*model.SummaryUpStreamRtmp, err error) {
res := []*model.SummaryUpStreamRtmp{}
var rows *sql.Rows
if rows, err = d.tidb.Query(c, _getSummaryUpStreamPlatform, start, end); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
info := new(model.SummaryUpStreamRtmp)
if err = rows.Scan(&info.PlatForm, &info.Count); err != nil {
err = errors.WithStack(err)
infos = nil
return
}
res = append(res, info)
}
err = rows.Err()
return res, err
}
// GetSummaryUpStreamCity 得到City统计信息
func (d *Dao) GetSummaryUpStreamCity(c context.Context, start int64, end int64) (infos []*model.SummaryUpStreamRtmp, err error) {
res := []*model.SummaryUpStreamRtmp{}
var rows *sql.Rows
if rows, err = d.tidb.Query(c, _getSummaryUpStreamCity, start, end); err != nil {
err = errors.WithStack(err)
return
}
defer rows.Close()
for rows.Next() {
info := new(model.SummaryUpStreamRtmp)
if err = rows.Scan(&info.City, &info.Count); err != nil {
err = errors.WithStack(err)
infos = nil
return
}
res = append(res, info)
}
err = rows.Err()
return res, err
}

View File

@@ -0,0 +1,117 @@
package dao
import (
"context"
"go-common/app/service/video/stream-mng/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoCreateUpStreamDispatch(t *testing.T) {
convey.Convey("CreateUpStreamDispatch", t, func(ctx convey.C) {
var (
c = context.Background()
info = &model.UpStreamInfo{
RoomID: 11891462,
CDN: 1,
PlatForm: "ios",
Country: "中国",
City: "上海",
ISP: "电信",
IP: "12.12.12.12",
}
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
err := d.CreateUpStreamDispatch(c, info)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoGetSummaryUpStreamRtmp(t *testing.T) {
convey.Convey("GetSummaryUpStreamRtmp", t, func(ctx convey.C) {
var (
c = context.Background()
start = int64(1546593007)
end = int64(1546830611)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
infos, err := d.GetSummaryUpStreamRtmp(c, start, end)
ctx.Convey("Then err should be nil.infos should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(infos, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoGetSummaryUpStreamISP(t *testing.T) {
convey.Convey("GetSummaryUpStreamISP", t, func(ctx convey.C) {
var (
c = context.Background()
start = int64(1546593007)
end = int64(1546830611)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
infos, err := d.GetSummaryUpStreamISP(c, start, end)
ctx.Convey("Then err should be nil.infos should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(infos, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoGetSummaryUpStreamCountry(t *testing.T) {
convey.Convey("GetSummaryUpStreamCountry", t, func(ctx convey.C) {
var (
c = context.Background()
start = int64(1546593007)
end = int64(1546830611)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
infos, err := d.GetSummaryUpStreamCountry(c, start, end)
ctx.Convey("Then err should be nil.infos should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(infos, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoGetSummaryUpStreamPlatform(t *testing.T) {
convey.Convey("GetSummaryUpStreamPlatform", t, func(ctx convey.C) {
var (
c = context.Background()
start = int64(1546593007)
end = int64(1546830611)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
infos, err := d.GetSummaryUpStreamPlatform(c, start, end)
ctx.Convey("Then err should be nil.infos should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(infos, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoGetSummaryUpStreamCity(t *testing.T) {
convey.Convey("GetSummaryUpStreamCity", t, func(ctx convey.C) {
var (
c = context.Background()
start = int64(1546593007)
end = int64(1546830611)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
infos, err := d.GetSummaryUpStreamCity(c, start, end)
ctx.Convey("Then err should be nil.infos should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(infos, convey.ShouldNotBeNil)
})
})
})
}

View File

@@ -0,0 +1,32 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["logger.go"],
importpath = "go-common/app/service/video/stream-mng/middleware",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,26 @@
package middleware
import (
"encoding/json"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
)
func Logger() bm.HandlerFunc {
return func(c *bm.Context) {
// panic("sss")
c.Next()
i, _ := c.Get("input_params")
ji, _ := json.Marshal(i)
o, _ := c.Get("output_data")
jo, _ := json.Marshal(o)
log.Infov(c,
log.KV("path", c.Request.URL.Path),
log.KV("method", c.Request.Method),
log.KV("input_params", string(ji)),
log.KV("output_data", string(jo)),
)
}
}

View File

@@ -0,0 +1,36 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"backup-stream.go",
"main-stream.go",
"model.go",
"notify.go",
"official-stream.go",
"upstream-info.go",
],
importpath = "go-common/app/service/video/stream-mng/model",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = ["//library/time:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,20 @@
package model
import (
"time"
)
// BackupStream 备用流
type BackupStream struct {
ID int64 `json:"id,omitempty"`
RoomID int64 `json:"room_id,omitempty"`
StreamName string `json:"stream_name,omitempty"`
Key string `json:"key,omitempty"`
DefaultVendor int64 `json:"default_vendor,omitempty"`
OriginUpstream int64 `json:"origin_upstream,omitempty"`
Streaming int64 `json:"streaming,omitempty"`
LastStreamTime time.Time `json:"last_stream_time,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
Options int64 `json:"options,omitempty"`
Status int32 `json:"status,omitempty"`
}

View File

@@ -0,0 +1,18 @@
package model
import "time"
// MainStream 备用流
type MainStream struct {
ID int64 `json:"id,omitempty"`
RoomID int64 `json:"room_id,omitempty"`
StreamName string `json:"stream_name,omitempty"`
Key string `json:"key,omitempty"`
DefaultVendor int64 `json:"default_vendor,omitempty"`
OriginUpstream int64 `json:"origin_upstream,omitempty"`
Streaming int64 `json:"streaming,omitempty"`
LastStreamTime time.Time `json:"last_stream_time,omitempty"`
//第一位预留 第二位是否开启蒙版直播流 第三位wmask蒙版流开播/关播 第四位mmask蒙版流开播/关播
Options int64 `json:"options,omitempty"`
Status int32 `json:"status,omitempty"`
}

View File

@@ -0,0 +1,53 @@
package model
import "go-common/library/time"
// StreamBase
type StreamBase struct {
StreamName string `json:"stream_name,omitempty"`
DefaultUpStream int64 `json:"default_upstream,omitempty"`
Origin int64 `json:"origin,omitempty"`
Forward []int64 `json:"forward,omitempty"`
Type int `json:"type,omitempty"`
Key string `json:"-"`
Options int64 `json:"options,omitempty"`
Wmask bool `json:"wmask,omitempty"`
Mmask bool `json:"mmask,omitempty"`
}
// StreamFullInfo
type StreamFullInfo struct {
RoomID int64 `json:"room_id,omitempty"`
Hot int64 `json:"hot"`
StreamName string `json:"stream_name,omitempty"`
Origin int64 `json:"origin,omitempty"`
Forward []int64 `json:"forward,omitempty"`
List []*StreamBase `json:"list,omitempty"`
}
// StreamChangeLog 修改cdnlog
type StreamChangeLog struct {
RoomID int64 `json:"room_id,omitempty"`
FromOrigin int64 `json:"from_origin,omitempty"`
ToOrigin int64 `json:"to_origin,omitempty"`
Source string `json:"source,omitempty"`
OperateName string `json:"operate_name,omitempty"`
Reason string `json:"reason,omitempty"`
CTime time.Time `json:"ctime,omitempty"`
}
// StreamStatus 流状态
type StreamStatus struct {
RoomID int64 `json:"room_id,omitempty"`
StreamName string `json:"stream_name,omitempty"`
DefaultUpStream int64 `json:"default_upstream,omitempty"`
DefaultChange bool `json:"default_change,omitempty"`
Origin int64 `json:"origin,omitempty"`
OriginChange bool `json:"origin_change,omitempty"`
Forward int64 `json:"forward,omitempty"`
ForwardChange bool `json:"forward_change,omitempty"`
Key string `json:"key,omitempty"`
Add bool `json:"add,omitempty"`
Options int64 `json:"options,omitempty"`
OptionsChange bool `json:"options_change,omitempty"`
}

View File

@@ -0,0 +1,16 @@
package model
import (
"encoding/json"
)
// StreamingNotifyParam 开/关播回调请求参数
type StreamingNotifyParam struct {
StreamName string `json:"stream_name,omitempty"`
Key string `json:"key,omitempty"`
SRC string `json:"src,omitempty"`
Type json.Number `json:"type,omitempty"`
TS json.Number `json:"ts,omitempty"`
SUID string `json:"suid,omitempty"`
Sign string `json:"sign,omitempty"`
}

View File

@@ -0,0 +1,20 @@
package model
import (
"time"
)
// OfficialStream 正式流
type OfficialStream struct {
ID int64 `orm:"pk;column(id)" json:"id,omitempty"`
RoomID int64 `json:"room_id,omitempty"`
Src int8 `json:"src,omitempty"`
Name string `json:"name,omitempty"`
Key string `json:"key,omitempty"`
UpRank int64 `json:"up_rank,omitempty"`
DownRank int64 `json:"down_rank,omitempty"`
Status int8 `json:"status,omitempty"`
LastStatusUpdatedAt time.Time `json:"last_status_updated_at,omitempty"`
CreateAt time.Time `json:"create_at,omitempty"`
UpdateAt time.Time `json:"update_at,omitempty"`
}

View File

@@ -0,0 +1,28 @@
package model
import (
"time"
)
// UpStreamInfo 上行调度信息
type UpStreamInfo struct {
ID int64 `json:"id,omitempty"`
RoomID int64 `json:"room_id,omitempty"`
CDN int64 `json:"cdn,omitempty"`
PlatForm string `json:"platform,omitempty"`
IP string `json:"ip,omitempty"`
Country string `json:"country,omitempty"`
City string `json:"city,omitempty"`
ISP string `json:"isp,omitempty"`
Ctime time.Time `json:"ctime,omitempty"`
}
// SummaryUpStreamInfo 上行调度统计信息
type SummaryUpStreamRtmp struct {
CDN int64 `json:"cdn,omitempty"`
ISP string `json:"isp,omitempty"`
Count int64 `json:"count,omitempty"`
Country string `json:"country,omitempty"`
City string `json:"city,omitempty"`
PlatForm string `json:"platform,omitempty"`
}

View File

@@ -0,0 +1,38 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["server.go"],
importpath = "go-common/app/service/video/stream-mng/server/grpc",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/video/stream-mng/api/v1:go_default_library",
"//app/service/video/stream-mng/common:go_default_library",
"//app/service/video/stream-mng/service:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/metadata:go_default_library",
"//library/net/rpc/warden:go_default_library",
"@org_golang_google_grpc//:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,667 @@
// Package server generate by warden_gen
package grpc
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"go-common/app/service/video/stream-mng/api/v1"
"go-common/app/service/video/stream-mng/common"
"go-common/app/service/video/stream-mng/service"
"go-common/library/ecode"
"go-common/library/log"
nmd "go-common/library/net/metadata"
"go-common/library/net/rpc/warden"
"google.golang.org/grpc"
"math/rand"
"strconv"
"strings"
"time"
)
// New Stream warden rpc server
func New(c *warden.ServerConfig, svr *service.Service) *warden.Server {
//ws := warden.NewServer(c, grpc.MaxRecvMsgSize(32*1024*1024), grpc.MaxSendMsgSize(32*1024*1024)) 这里需要考虑配置问题
ws := warden.NewServer(c)
ws.Use(middleware())
v1.RegisterStreamServer(ws.Server(), &server{svr})
ws, err := ws.Start()
if err != nil {
panic(err)
}
return ws
}
type server struct {
svr *service.Service
}
var _ v1.StreamServer = &server{}
// GetSingleScreeShotByRoomID
func (s *server) GetSingleScreeShot(ctx context.Context, req *v1.GetSingleScreeShotReq) (*v1.GetSingleScreeShotReply, error) {
roomID := req.RoomId
start := req.StartTime
end := req.EndTime
channel := req.Channel
resp := &v1.GetSingleScreeShotReply{}
if roomID <= 0 || start == "" || end == "" {
st, _ := ecode.Error(ecode.RequestErr, "some fields are empty").WithDetails(resp)
return nil, st
}
startTime, err := time.ParseInLocation("2006-01-02 15:04:05", start, time.Local)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, "Start time format is incorrect").WithDetails(resp)
return nil, st
}
endTime, err := time.ParseInLocation("2006-01-02 15:04:05", end, time.Local)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, "End time format is incorrect").WithDetails(resp)
return nil, st
}
info, err := s.svr.GetSingleScreeShot(ctx, roomID, startTime.Unix(), endTime.Unix(), channel)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, err.Error()).WithDetails(resp)
return nil, st
}
return &v1.GetSingleScreeShotReply{
List: info,
}, nil
}
// GetMultiScreenShotByRommID
func (s *server) GetMultiScreenShot(ctx context.Context, req *v1.GetMultiScreenShotReq) (*v1.GetMultiScreenShotReply, error) {
rooms := req.RoomIds
ts := req.Ts
channel := req.Channel
resp := &v1.GetMultiScreenShotReply{}
if rooms == "" || ts == 0 {
st, _ := ecode.Error(ecode.RequestErr, "some fields are empty").WithDetails(resp)
return nil, st
}
// 切割room_id
roomIDs := strings.Split(rooms, ",")
if len(roomIDs) <= 0 {
st, _ := ecode.Error(ecode.RequestErr, "room_ids is not right").WithDetails(resp)
return nil, st
}
res := v1.GetMultiScreenShotReply{
List: map[int64]string{},
}
rids := []int64{}
for _, v := range roomIDs {
roomID, err := strconv.ParseInt(v, 10, 64)
if err != nil {
continue
}
rids = append(rids, roomID)
}
urls, err := s.svr.GetMultiScreenShot(ctx, rids, ts, channel)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, err.Error()).WithDetails(resp)
return nil, st
}
res.List = urls
return &res, nil
}
// GetOriginScreenShotPic
func (s *server) GetOriginScreenShotPic(ctx context.Context, req *v1.GetOriginScreenShotPicReq) (*v1.GetOriginScreenShotPicReply, error) {
rooms := req.RoomIds
ts := req.Ts
resp := &v1.GetOriginScreenShotPicReply{}
if rooms == "" || ts == 0 {
st, _ := ecode.Error(ecode.RequestErr, "some fields are empty").WithDetails(resp)
return nil, st
}
// 切割room_id
roomIDs := strings.Split(rooms, ",")
if len(roomIDs) <= 0 {
st, _ := ecode.Error(ecode.RequestErr, "room_ids is not right").WithDetails(resp)
return nil, st
}
res := v1.GetOriginScreenShotPicReply{
List: map[int64]string{},
}
rids := []int64{}
for _, v := range roomIDs {
roomID, err := strconv.ParseInt(v, 10, 64)
if err != nil {
continue
}
rids = append(rids, roomID)
}
urls, err := s.svr.GetOriginScreenShotPic(ctx, rids, ts)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, err.Error()).WithDetails(resp)
return nil, st
}
res.List = urls
return &res, nil
}
// CreateOfficeStream 创建正式流
func (s *server) CreateOfficalStream(ctx context.Context, req *v1.CreateOfficalStreamReq) (*v1.CreateOfficalStreamReply, error) {
key := req.Key
streamName := req.StreamName
if req.Uid != 0 {
key = mockStreamKey(fmt.Sprintf("%d", req.Uid))
streamName = mockStreamName(fmt.Sprintf("%d", req.Uid))
}
resp := &v1.CreateOfficalStreamReply{}
// 检查参数
if streamName == "" || key == "" || req.RoomId <= 0 {
st, _ := ecode.Error(ecode.RequestErr, "some fields are empty").WithDetails(resp)
return nil, st
}
flag := s.svr.CreateOfficalStream(ctx, streamName, key, req.RoomId)
return &v1.CreateOfficalStreamReply{
Success: flag,
}, nil
}
// GetStreamInfo 获取单个流信息
func (s *server) GetStreamInfo(ctx context.Context, req *v1.GetStreamInfoReq) (*v1.GetStreamInfoReply, error) {
rid := req.RoomId
sname := req.StreamName
resp := &v1.GetStreamInfoReply{}
if rid == 0 && sname == "" {
resp.Code = -400
resp.Message = "some fields are empty"
return resp, nil
}
info, err := s.svr.GetStreamInfo(ctx, int64(rid), sname)
if err != nil {
resp.Code = -400
resp.Message = err.Error()
return resp, nil
}
baseList := []*v1.StreamBase{}
for _, v := range info.List {
forward := []uint32{}
for _, f := range v.Forward {
forward = append(forward, uint32(f))
}
baseList = append(baseList, &v1.StreamBase{
StreamName: v.StreamName,
DefaultUpstream: uint32(v.DefaultUpStream),
Origin: uint32(v.Origin),
Forward: forward,
Type: uint32(v.Type),
Options: uint32(v.Options),
//Key: v.Key,
})
}
resp.Code = 0
resp.Data = &v1.StreamFullInfo{
RoomId: uint32(info.RoomID),
Hot: uint32(info.Hot),
List: baseList,
}
return resp, nil
}
// GetMultiStreamInfo 批量获取流信息
func (s *server) GetMultiStreamInfo(ctx context.Context, req *v1.GetMultiStreamInfoReq) (*v1.GetMultiStreamInfoReply, error) {
rids := req.RoomIds
resp := &v1.GetMultiStreamInfoReply{}
// 切割room_id
if len(rids) <= 0 {
resp.Code = 0
resp.Message = "success"
return resp, nil
}
if len(rids) > 30 {
resp.Code = -400
resp.Message = "The number of rooms must be less than 30"
return resp, nil
}
roomIDs := []int64{}
for _, v := range rids {
roomID := int64(v)
if roomID <= 0 {
continue
}
roomIDs = append(roomIDs, roomID)
}
info, err := s.svr.GetMultiStreamInfo(ctx, roomIDs)
if err != nil {
log.Infov(ctx, log.KV("log", err.Error()))
resp.Code = 0
resp.Message = "success"
return resp, nil
}
if info == nil || len(info) == 0 {
log.Infov(ctx, log.KV("log", "can find any things"))
resp.Code = 0
resp.Message = "success"
return resp, nil
}
res := map[uint32]*v1.StreamFullInfo{}
for id, v := range info {
item := &v1.StreamFullInfo{}
item.Hot = uint32(v.Hot)
item.RoomId = uint32(v.RoomID)
baseList := []*v1.StreamBase{}
for _, i := range v.List {
forward := []uint32{}
for _, f := range i.Forward {
forward = append(forward, uint32(f))
}
baseList = append(baseList, &v1.StreamBase{
StreamName: i.StreamName,
DefaultUpstream: uint32(i.DefaultUpStream),
Origin: uint32(i.Origin),
Forward: forward,
Type: uint32(i.Type),
Options: uint32(i.Options),
//Key: i.Key,
})
}
item.List = baseList
res[uint32(id)] = item
}
resp.Code = 0
resp.Data = res
return resp, nil
}
// ChangeSrc 切换cdn
func (s *server) ChangeSrc(ctx context.Context, req *v1.ChangeSrcReq) (*v1.EmptyStruct, error) {
resp := &v1.EmptyStruct{}
if req.RoomId <= 0 || req.Src == 0 || req.Source == "" || req.OperateName == "" {
st, _ := ecode.Error(ecode.RequestErr, "some fields are empty").WithDetails(resp)
return nil, st
}
// todo 后续改为新的src
src := int8(req.Src)
if _, ok := common.SrcMapBitwise[src]; !ok {
st, _ := ecode.Error(ecode.RequestErr, "src is not right").WithDetails(resp)
return nil, st
}
err := s.svr.ChangeSrc(ctx, req.RoomId, common.SrcMapBitwise[src], req.Source, req.OperateName, req.Reason)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, err.Error()).WithDetails(resp)
return nil, st
}
return resp, nil
}
// GetStreamLastTime 得到流到最后推流时间;主流的推流时间up_rank = 1
func (s *server) GetStreamLastTime(ctx context.Context, req *v1.GetStreamLastTimeReq) (*v1.GetStreamLastTimeReply, error) {
rid := req.RoomId
resp := &v1.GetStreamLastTimeReply{}
if rid <= 0 {
st, _ := ecode.Error(ecode.RequestErr, "room_id is not right").WithDetails(resp)
return nil, st
}
t, err := s.svr.GetStreamLastTime(ctx, rid)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, err.Error()).WithDetails(resp)
return nil, st
}
return &v1.GetStreamLastTimeReply{
LastTime: t,
}, nil
}
// GetStreamNameByRoomID 需要考虑备用流 + 考虑短号
func (s *server) GetStreamNameByRoomID(ctx context.Context, req *v1.GetStreamNameByRoomIDReq) (*v1.GetStreamNameByRoomIDReply, error) {
rid := req.RoomId
resp := &v1.GetStreamNameByRoomIDReply{}
if rid <= 0 {
st, _ := ecode.Error(ecode.RequestErr, "room_id is not right").WithDetails(resp)
return nil, st
}
res, err := s.svr.GetStreamNameByRoomID(ctx, rid, false)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, err.Error()).WithDetails(resp)
return nil, st
}
if len(res) == 0 {
st, _ := ecode.Error(ecode.RequestErr, fmt.Sprintf("can not find info by room_id=%d", rid)).WithDetails(resp)
return nil, st
}
return &v1.GetStreamNameByRoomIDReply{
StreamName: res[0],
}, nil
}
// GetRoomIDByStreamName 查询房间号
func (s *server) GetRoomIDByStreamName(ctx context.Context, req *v1.GetRoomIDByStreamNameReq) (*v1.GetRoomIDByStreamNameReply, error) {
resp := &v1.GetRoomIDByStreamNameReply{}
if req.StreamName == "" {
st, _ := ecode.Error(ecode.RequestErr, "stream name is empty").WithDetails(resp)
return nil, st
}
res, err := s.svr.GetRoomIDByStreamName(ctx, req.StreamName)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, err.Error()).WithDetails(resp)
return nil, st
}
if res <= 0 {
st, _ := ecode.Error(ecode.RequestErr, fmt.Sprintf("can not find any info by name = %s", req.StreamName)).WithDetails(resp)
return nil, st
}
return &v1.GetRoomIDByStreamNameReply{
RoomId: res,
}, nil
}
// GetAdapterStreamByStreamName 适配结果输出, 此处也可以输入备用流, 该结果只输出直推上行
func (s *server) GetAdapterStreamByStreamName(ctx context.Context, req *v1.GetAdapterStreamByStreamNameReq) (*v1.GetAdapterStreamByStreamNameReply, error) {
res := v1.GetAdapterStreamByStreamNameReply{
List: map[string]*v1.AdapterStream{},
}
snames := req.StreamNames
if snames == "" {
st, _ := ecode.Error(ecode.RequestErr, "stream_names is empty").WithDetails(&res)
return nil, st
}
nameSlice := strings.Split(snames, ",")
if len(nameSlice) == 0 {
st, _ := ecode.Error(ecode.RequestErr, "stream_names is empty").WithDetails(&res)
return nil, st
}
if len(nameSlice) > 500 {
st, _ := ecode.Error(ecode.RequestErr, "too many names").WithDetails(&res)
return nil, st
}
info := s.svr.GetAdapterStreamByStreamName(ctx, nameSlice)
if info != nil {
for name, v := range info {
res.List[name] = &v1.AdapterStream{
Src: v.Src,
RoomId: v.RoomID,
UpRank: v.UpRank,
SrcName: v.SrcName,
}
}
}
return &res, nil
}
// GetSrcByRoomID
func (s *server) GetSrcByRoomID(ctx context.Context, req *v1.GetSrcByRoomIDReq) (*v1.GetSrcByRoomIDReply, error) {
rid := req.RoomId
resp := &v1.GetSrcByRoomIDReply{}
if rid <= 0 {
st, _ := ecode.Error(ecode.RequestErr, "room_id is not right").WithDetails(resp)
return nil, st
}
info, err := s.svr.GetSrcByRoomID(ctx, rid)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, err.Error()).WithDetails(resp)
return nil, st
}
if info == nil || len(info) == 0 {
st, _ := ecode.Error(ecode.RequestErr, "获取线路失败").WithDetails(resp)
return nil, st
}
res := &v1.GetSrcByRoomIDReply{
List: []*v1.RoomSrcCheck{},
}
for _, v := range info {
res.List = append(res.List, &v1.RoomSrcCheck{
Src: v.Src,
Checked: int32(v.Checked),
Desc: v.Desc,
})
}
return res, nil
}
// GetLineListByRoomID
func (s *server) GetLineListByRoomID(ctx context.Context, req *v1.GetLineListByRoomIDReq) (*v1.GetLineListByRoomIDReply, error) {
resp := &v1.GetLineListByRoomIDReply{}
if req.RoomId <= 0 {
st, _ := ecode.Error(ecode.RequestErr, "room_id is not right").WithDetails(resp)
return nil, st
}
info, err := s.svr.GetLineListByRoomID(ctx, req.RoomId)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, err.Error()).WithDetails(resp)
return nil, st
}
if info == nil || len(info) == 0 {
st, _ := ecode.Error(ecode.RequestErr, "获取线路失败").WithDetails(resp)
return nil, st
}
res := &v1.GetLineListByRoomIDReply{
List: []*v1.LineList{},
}
for _, v := range info {
res.List = append(res.List, &v1.LineList{
Src: v.Src,
Use: v.Use,
Desc: v.Desc,
})
}
return res, nil
}
// GetUpStreamRtmp UpStream
func (s *server) GetUpStreamRtmp(ctx context.Context, req *v1.GetUpStreamRtmpReq) (*v1.GetUpStreamRtmpReply, error) {
resp := &v1.GetUpStreamRtmpReply{}
if req.RoomId == 0 || req.Platform == "" {
st, _ := ecode.Error(ecode.RequestErr, "some fields are empty").WithDetails(resp)
return nil, st
}
if req.Ip == "" {
if cmd, ok := nmd.FromContext(ctx); ok {
if ip, ok := cmd[nmd.RemoteIP].(string); ok {
req.Ip = ip
}
}
}
info, err := s.svr.GetUpStreamRtmp(ctx, req.RoomId, req.FreeFlow, req.Ip, req.AreaId, int(req.Attentions), 0, req.Platform)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, err.Error()).WithDetails(resp)
return nil, st
}
if info != nil {
resp.Up = &v1.UpStreamRtmp{
Addr: info.Addr,
Code: info.Code,
NewLink: info.NewLink,
}
}
return resp, nil
}
// StreamCut 切流的房间和时间, 内部调用
func (s *server) StreamCut(ctx context.Context, req *v1.StreamCutReq) (*v1.EmptyStruct, error) {
roomID := req.RoomId
cutTime := req.CutTime
resp := &v1.EmptyStruct{}
if roomID <= 0 {
st, _ := ecode.Error(ecode.RequestErr, "room_ids is not right").WithDetails(resp)
return nil, st
}
if cutTime == 0 {
cutTime = 1
}
s.svr.StreamCut(ctx, roomID, cutTime, 0)
return &v1.EmptyStruct{}, nil
}
// Ping Service
func (s *server) Ping(ctx context.Context, req *v1.PingReq) (*v1.PingReply, error) {
return &v1.PingReply{}, nil
}
// Close Service
func (s *server) Close(ctx context.Context, req *v1.CloseReq) (*v1.CloseReply, error) {
return &v1.CloseReply{}, nil
}
// ClearStreamStatus
func (s *server) ClearStreamStatus(ctx context.Context, req *v1.ClearStreamStatusReq) (*v1.EmptyStruct, error) {
rid := req.RoomId
resp := &v1.EmptyStruct{}
if rid <= 0 {
st, _ := ecode.Error(ecode.RequestErr, "room_ids is not right").WithDetails(resp)
return nil, st
}
err := s.svr.ClearStreamStatus(ctx, rid)
if err != nil {
st, _ := ecode.Error(ecode.RequestErr, err.Error()).WithDetails(resp)
return nil, st
}
return &v1.EmptyStruct{}, nil
}
// CheckLiveStreamList
func (s *server) CheckLiveStreamList(ctx context.Context, req *v1.CheckLiveStreamReq) (*v1.CheckLiveStreamReply, error) {
resp := &v1.CheckLiveStreamReply{}
rids := req.RoomId
if len(rids) == 0 {
st, _ := ecode.Error(ecode.RequestErr, "room_ids is empty").WithDetails(resp)
return nil, st
}
res := s.svr.CheckLiveStreamList(ctx, rids)
resp.List = res
return resp, nil
}
// mockStream 模拟生成的流名
func mockStreamName(uid string) string {
num := rand.Int63n(88888888)
return fmt.Sprintf("live_%s_%d", uid, num+1111111)
}
// mockStreamKey 模拟生成的key
func mockStreamKey(uid string) string {
str := fmt.Sprintf("nvijqwopW1%s%d", uid, time.Now().Unix())
h := md5.New()
h.Write([]byte(str))
cipherStr := h.Sum(nil)
md5Str := hex.EncodeToString(cipherStr)
return md5Str
}
func middleware() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
resp, err = handler(ctx, req)
out := ""
if err != nil {
out = err.Error()
} else {
jo, _ := json.Marshal(resp)
out = string(jo)
}
// 记录调用方法
log.Infov(ctx,
log.KV("path", info.FullMethod),
log.KV("caller", nmd.String(ctx, nmd.Caller)),
log.KV("input_params", fmt.Sprintf("%s", req)),
log.KV("output_data", out),
)
return
}
}

View File

@@ -0,0 +1,59 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"backup.go",
"change-log.go",
"change-src.go",
"check-live.go",
"clear-cache.go",
"clear-retweet.go",
"http.go",
"notify.go",
"official.go",
"screen-shot.go",
"stream.go",
"stream-cut.go",
"stream-info.go",
"stream-mask.go",
"stream-validate.go",
"upstream-rtmp.go",
"upstream-summary.go",
],
importpath = "go-common/app/service/video/stream-mng/server/http",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/video/stream-mng/common:go_default_library",
"//app/service/video/stream-mng/conf:go_default_library",
"//app/service/video/stream-mng/middleware:go_default_library",
"//app/service/video/stream-mng/model:go_default_library",
"//app/service/video/stream-mng/service:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/auth:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//library/net/metadata:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,74 @@
package http
import (
"encoding/json"
"go-common/app/service/video/stream-mng/model"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"io/ioutil"
"strconv"
)
// createBackupStream 创建备用流
func createBackupStream(c *bm.Context) {
var bs model.BackupStream
switch c.Request.Header.Get("Content-Type") {
case "application/x-www-form-urlencoded":
if len(c.Request.PostForm) == 0 {
c.Set("output_data", "empty params")
c.JSONMap(map[string]interface{}{"message": "empty params"}, ecode.RequestErr)
c.Abort()
return
}
bs.StreamName = c.Request.PostFormValue("stream_name")
bs.Key = c.Request.PostFormValue("key")
default_vendor := c.Request.PostFormValue("default_vendor")
vendor, _ := strconv.ParseInt(default_vendor, 10, 64)
bs.DefaultVendor = vendor
id := c.Request.PostFormValue("room_id")
rid, _ := strconv.ParseInt(id, 10, 64)
bs.RoomID = rid
//bs.ExpiresAt = c.Request.PostFormValue("expires_at")
default:
defer c.Request.Body.Close()
b, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err}, ecode.RequestErr)
c.Abort()
return
}
if len(b) == 0 {
c.Set("output_data", "参数不能为空")
c.JSONMap(map[string]interface{}{"message": "参数不能为空"}, ecode.RequestErr)
c.Abort()
return
}
err = json.Unmarshal(b, &bs)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": "请确认格式是否正常"}, ecode.RequestErr)
c.Abort()
return
}
if bs.RoomID <= 0 {
c.Set("output_data", "房间号不正确")
c.JSONMap(map[string]interface{}{"message": "房间号不正确"}, ecode.RequestErr)
c.Abort()
return
}
}
c.Set("input_params", bs)
_, err := srv.CreateBackupStream(c, &bs)
c.JSON(bs, err)
}

View File

@@ -0,0 +1,41 @@
package http
import (
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"strconv"
)
// getChangeLogByRoomID 查询cdn切换记录
func getChangeLogByRoomID(c *bm.Context) {
params := c.Request.URL.Query()
room := params.Get("room_id")
limit := params.Get("limit")
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "room_id is not right")
c.JSONMap(map[string]interface{}{"message": "room_id is not right"}, ecode.RequestErr)
c.Abort()
return
}
// 默认查询1最近一条记录
limitInt, _ := strconv.ParseInt(limit, 10, 64)
if limitInt <= 0 {
limitInt = 1
}
infos, err := srv.GetChangeLogByRoomID(c, roomID, limitInt)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.JSONMap(map[string]interface{}{"data": infos}, nil)
}

View File

@@ -0,0 +1,100 @@
package http
import (
"encoding/json"
"fmt"
"go-common/app/service/video/stream-mng/common"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"io/ioutil"
"strconv"
)
// 单独文件切换cdn
// changeSrc 切换cdn
func changeSrc(c *bm.Context) {
// 这里传递的src是新的src
type changStruct struct {
RoomID int64 `json:"room_id,omitempty"`
ToOrigin int8 `json:"src,omitempty"`
Source string `json:"source,omitempty"`
OperateName string `json:"operate_name,omitempty"`
Reason string `json:"reason,omitempty"`
}
var cs changStruct
switch c.Request.Header.Get("Content-Type") {
case "application/x-www-form-urlencoded":
if len(c.Request.PostForm) == 0 {
c.Set("output_data", "参数为空")
c.JSONMap(map[string]interface{}{"message": "参数为空"}, ecode.RequestErr)
c.Abort()
return
}
toOrigin := c.Request.PostFormValue("src")
or, _ := strconv.ParseInt(toOrigin, 10, 64)
cs.ToOrigin = int8(or)
rid, _ := strconv.ParseInt(c.Request.PostFormValue("room_id"), 10, 64)
cs.RoomID = rid
cs.Source = c.Request.PostFormValue("source")
cs.OperateName = c.Request.PostFormValue("operate_name")
default:
// 验证传参数
defer c.Request.Body.Close()
b, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.JSONMap(map[string]interface{}{"message": err}, ecode.RequestErr)
c.Abort()
return
}
if len(b) == 0 {
c.JSONMap(map[string]interface{}{"message": "参数为空"}, ecode.RequestErr)
c.Abort()
return
}
err = json.Unmarshal(b, &cs)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
}
c.Set("input_params", cs)
// 校验:房间号+src+平台来源+操作人 都是必须的, 操作理由可以不填
if cs.RoomID <= 0 || cs.ToOrigin == 0 || cs.Source == "" || cs.OperateName == "" {
c.Set("output_data", "some fields are not right")
c.JSONMap(map[string]interface{}{"message": "部分参数为空"}, ecode.RequestErr)
c.Abort()
return
}
// todo 先使用老的src, 后续改为新的src
src := int8(cs.ToOrigin)
if _, ok := common.SrcMapBitwise[src]; !ok {
c.Set("output_data", "src is not right")
c.JSONMap(map[string]interface{}{"message": "src is not right"}, ecode.RequestErr)
c.Abort()
return
}
err := srv.ChangeSrc(c, cs.RoomID, common.SrcMapBitwise[src], cs.Source, cs.OperateName, cs.Reason)
if err == nil {
c.Set("output_data", fmt.Sprintf("room_id = %d, change src success", cs.RoomID))
c.JSONMap(map[string]interface{}{"message": "ok"}, nil)
c.Abort()
return
}
c.Set("output_data", fmt.Sprintf("room_id = %d, change src faild = %v", cs.RoomID, err))
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
}

View File

@@ -0,0 +1,44 @@
package http
import (
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"strconv"
"strings"
)
func checkLiveStreamList(c *bm.Context) {
params := c.Request.URL.Query()
rooms := params.Get("room_ids")
c.Set("input_params", params)
if rooms == "" {
c.Set("output_data", "some fields are empty")
c.JSONMap(map[string]interface{}{"message": "some fields are empty"}, ecode.RequestErr)
c.Abort()
return
}
// 切割room_id
roomIDs := strings.Split(rooms, ",")
if len(roomIDs) <= 0 {
c.Set("output_data", "room_ids is not right")
c.JSONMap(map[string]interface{}{"message": "room_ids is not right"}, ecode.RequestErr)
c.Abort()
return
}
rids := []int64{}
for _, v := range roomIDs {
roomID, err := strconv.ParseInt(v, 10, 64)
if err != nil {
continue
}
rids = append(rids, roomID)
}
c.JSONMap(map[string]interface{}{"data": srv.CheckLiveStreamList(c, rids)}, nil)
}

View File

@@ -0,0 +1,32 @@
package http
import (
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"strconv"
)
// clearRoomCacheByRID删除room_id缓存的接口防止缓存问题出现的bug
func clearRoomCacheByRID(c *bm.Context) {
params := c.Request.URL.Query()
room := params.Get("room_id")
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "room_id is empty")
c.JSONMap(map[string]interface{}{"message": "room_id is empty"}, ecode.RequestErr)
c.Abort()
return
}
err = srv.ClearRoomCacheByRID(c, roomID)
if err != nil {
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.JSONMap(map[string]interface{}{"message": "ok"}, nil)
}

View File

@@ -0,0 +1,75 @@
package http
import (
"encoding/json"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"io/ioutil"
)
// clearStreamStatus 清理互推标志
func clearStreamStatus(c *bm.Context) {
type room struct {
RoomID json.Number `json:"room_id"`
}
vp := &room{}
switch c.Request.Header.Get("Content-Type") {
case "application/x-www-form-urlencoded":
if len(c.Request.PostForm) == 0 {
c.Set("output_data", "clearStreamStatus = empty post body")
c.JSONMap(map[string]interface{}{"message": "empty post body"}, ecode.RequestErr)
c.Abort()
return
}
vp.RoomID = json.Number(c.Request.PostFormValue("room_id"))
default:
defer c.Request.Body.Close()
b, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
if len(b) == 0 {
c.Set("output_data", "clearStreamStatus empty params")
c.JSONMap(map[string]interface{}{"message": "empty params"}, ecode.RequestErr)
c.Abort()
return
}
err = json.Unmarshal(b, &vp)
if err != nil {
c.Set("output_data", "room_id is not right")
c.JSONMap(map[string]interface{}{"message": "room_id is not right"}, ecode.RequestErr)
c.Abort()
return
}
}
roomID, err := vp.RoomID.Int64()
if roomID <= 0 || err != nil {
c.Set("output_data", "room_id is not right")
c.JSONMap(map[string]interface{}{"message": "room_id is not right"}, ecode.RequestErr)
c.Abort()
return
}
c.Set("input_params", map[string]int64{"room_id": roomID})
err = srv.ClearStreamStatus(c, roomID)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", "clear status success")
c.JSONMap(map[string]interface{}{"message": "ok"}, nil)
}

View File

@@ -0,0 +1,121 @@
package http
import (
"net/http"
"go-common/app/service/video/stream-mng/conf"
"go-common/app/service/video/stream-mng/middleware"
"go-common/app/service/video/stream-mng/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/auth"
"go-common/library/net/http/blademaster/middleware/verify"
)
var (
srv *service.Service
vfy *verify.Verify
authSvr *auth.Auth
)
// Init init
func Init(c *conf.Config, svc *service.Service) {
srv = svc
vfy = verify.New(c.Verify)
authSvr = auth.New(c.Auth)
engine := bm.DefaultServer(c.BM)
engine.Use(middleware.Logger())
route(engine)
if err := engine.Start(); err != nil {
log.Error("bm Start error(%v)", err)
panic(err)
}
}
func route(e *bm.Engine) {
e.Ping(ping)
e.Register(register)
g := e.Group("/video/stream-mng")
{
g.GET("/", alive)
g.POST("/stream/backup", createBackupStream)
g.POST("/stream/offical", createOfficalStream)
g.POST("/stream/validate", streamValidate)
g.GET("/stream/old/getbyroomid", getOldStreamInfoByRoomID)
g.GET("/stream/old/getbyname", getOldStreamInfoByStreamName)
g.GET("/notifymaskbyroomid", saveMaskByRoomID) //控制一个主流是否需要转蒙版直播流
g.GET("/notifymaskbystreamname", saveMaskByStreamName) //控制一个主流是否可提供蒙版给PLAYURL
g.POST("/addhotstream", addHotStream) //增加热流到redis
g.GET("/getstream", getStream)
g.GET("/getmultistreams", getMultiStreams)
g.GET("/stream/getRoomIdByStreamName", getRoomIDByStreamName)
g.GET("/stream/getStreamNameByRoomId", getStreamNameByRoomID)
g.POST("/stream/changeSrc", changeSrc)
g.GET("/stream/cut", cutStream)
g.GET("/stream/cutmobilestream", authSvr.User, cutStreamByMobile)
g.GET("/stream/getLastTime", getStreamLastTime)
g.GET("/stream/getAdapterStream", getAdapterStreamByStreamName)
g.GET("/stream/getSrcByRoomID", getSrcByRoomID)
g.GET("/stream/getLineListByRoomID", getLineListByRoomID)
g.GET("/shot/getSinglePic", getSingleScreenShot)
g.GET("/shot/getMultiPic", getMultiScreenShot)
g.GET("/shot/getOriginPic", getOriginScreenShotPic)
g.GET("/shot/getperiodpic", getTimePeriodScreenShot)
g.POST("/stream/clearstreamstatus", clearStreamStatus)
g.GET("/stream/getRoomRtmp", getRoomRtmp) // 拜年祭推流码
g.GET("/stream/getUpStreamRtmp", getUpStreamRtmp) // 后台调用,无需鉴权
g.GET("/stream/getmobilertmp", authSvr.User, getMobileRtmp) // 移动端调用
g.GET("/stream/getwebrtmp", authSvr.User, getWebRtmp) // 被web端和pc_link调用
g.GET("/stream/live", checkLiveStreamList)
// 删除room_id缓存的接口防止缓存问题出现的bug
g.GET("/stream/clearcache", clearRoomCacheByRID)
// 查询更改记录
g.GET("/change/getchangeLog", getChangeLogByRoomID)
// 查询统计上行调度信息
g.GET("/summary/upstream", getSummaryUpStreamRtmp)
g.GET("/summary/isp", getSummaryUpStreamISP)
g.GET("/summary/country", getSummaryUpStreamCountry)
g.GET("/summary/platform", getSummaryUpStreamPlatform)
g.GET("/summary/city", getSummaryUpStreamCity)
}
g2 := e.Group("/live_stream/v1/StreamThird")
{
g2.POST("/stream_validate", streamValidate)
g2.POST("/open_notify", openNotify)
g2.POST("/close_notify", closeNotify)
}
g7 := e.Group("/live_stream/v1/StreamList")
{
g7.GET("/get_stream_by_roomId", authSvr.User, getWebRtmp)
}
g8 := e.Group("/live_stream/v1/UpStreamExt")
{
g8.GET("/get_by_room", authSvr.User, getMobileRtmp)
g8.GET("/pause_by_room", authSvr.User, cutStreamByMobile)
}
}
func ping(c *bm.Context) {
if err := srv.Ping(c); err != nil {
log.Error("ping error(%v)", err)
c.AbortWithStatus(http.StatusServiceUnavailable)
}
}
func register(c *bm.Context) {
c.JSON(map[string]interface{}{}, nil)
}
func alive(c *bm.Context) {
c.String(0, "Golang 大法好 !!!")
}

View File

@@ -0,0 +1,96 @@
package http
import (
"encoding/json"
"errors"
"fmt"
"go-common/app/service/video/stream-mng/model"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"io/ioutil"
)
func openNotify(c *bm.Context) {
p, err := parseNotifyBody(c)
if err != nil {
// log.Errorv(c, log.KV("log", fmt.Sprintf("open_notify_err = %v", err)))
c.JSONMap(map[string]interface{}{"msg": err.Error()}, ecode.RequestErr)
c.Set("output_data", err.Error())
return
}
err = srv.StreamingNotify(c, p, true)
if err != nil {
// log.Errorv(c, log.KV("log", fmt.Sprintf("open_notify_err = %v", err)))
c.JSONMap(map[string]interface{}{"msg": err.Error()}, ecode.RequestErr)
c.Set("output_data", err.Error())
// c.Abort()
return
}
c.Set("output_data", "success")
c.JSONMap(map[string]interface{}{"msg": "success"}, ecode.OK)
}
func closeNotify(c *bm.Context) {
p, err := parseNotifyBody(c)
if err != nil {
c.JSONMap(map[string]interface{}{"msg": err.Error()}, ecode.RequestErr)
c.Set("output_data", err.Error())
return
}
err = srv.StreamingNotify(c, p, false)
if err != nil {
c.JSONMap(map[string]interface{}{"msg": err.Error()}, ecode.RequestErr)
c.Set("output_data", err.Error())
return
}
c.Set("output_data", "success")
c.JSONMap(map[string]interface{}{"msg": "success"}, ecode.OK)
}
func parseNotifyBody(c *bm.Context) (*model.StreamingNotifyParam, error) {
// log.Info("%v %v", c.Request.Header.Get("Content-Type"), c.Request.Form)
switch c.Request.Header.Get("Content-Type") {
case "application/x-www-form-urlencoded":
// log.Info("%+v", c.Request.PostForm)
if len(c.Request.PostForm) == 0 {
return nil, errors.New("empty post body")
}
p := &model.StreamingNotifyParam{}
p.Key = c.Request.PostFormValue("key")
p.Sign = c.Request.PostFormValue("sign")
p.SRC = c.Request.PostFormValue("src")
p.StreamName = c.Request.PostFormValue("stream_name")
p.SUID = c.Request.PostFormValue("suid")
p.TS = json.Number(c.Request.PostFormValue("ts"))
p.Type = json.Number(c.Request.PostFormValue("type"))
// log.Info("%+v", p)
// log.Infov(c, log.KV("log", fmt.Sprintf("notify_input = %+v", p)))
c.Set("input_params", *p)
return p, nil
default:
defer c.Request.Body.Close()
b, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
return nil, err
}
if len(b) == 0 {
return nil, errors.New("empty body")
}
var snp model.StreamingNotifyParam
err = json.Unmarshal(b, &snp)
if err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("notify_parse_body_error = %v", err)))
return &snp, errors.New("invalid json body")
}
// log.Infov(c, log.KV("log", fmt.Sprintf("notify_input = %+v", snp)))
c.Set("input_params", snp)
return &snp, nil
}
}

View File

@@ -0,0 +1,90 @@
package http
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"io/ioutil"
"math/rand"
"time"
)
// createOfficalStream 创建正式流
// optional string debug; 1表示线下测试
// required int uid; uid线下测试必传
func createOfficalStream(c *bm.Context) {
defer c.Request.Body.Close()
b, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.JSONMap(map[string]interface{}{"message": err}, ecode.RequestErr)
c.Abort()
return
}
if len(b) == 0 {
c.JSONMap(map[string]interface{}{"message": "empty params"}, ecode.RequestErr)
c.Abort()
return
}
type officialParams struct {
RoomID int64 `json:"room_id,omitempty"`
StreamName string `json:"stream_name,omitempty"`
Key string `json:"key,omitempty"`
Debug string `json:"debug,omitempty"`
Uid int `json:"uid,omitempty"`
}
var off officialParams
err = json.Unmarshal(b, &off)
if err != nil {
c.JSONMap(map[string]interface{}{"message": err}, ecode.RequestErr)
c.Abort()
return
}
streamName := off.StreamName
key := off.Key
uid := off.Uid
roomID := off.RoomID
// 线下测试, 1表示线下测试,uid线下测试必传
if uid != 0 {
id := fmt.Sprintf("%d", uid)
key = mockStreamKey(id)
streamName = mockStreamName(id)
}
// 检查参数
if streamName == "" || key == "" || roomID <= 0 {
c.Set("output_data", "some fields are empty")
c.JSONMap(map[string]interface{}{"message": "some fields are empty"}, ecode.RequestErr)
c.Abort()
return
}
flag := srv.CreateOfficalStream(c, streamName, key, roomID)
c.Set("output_data", fmt.Sprintf("create stream success = %v, room_id = %d", flag, roomID))
c.JSONMap(map[string]interface{}{"data": map[string]bool{"succ": flag}}, nil)
}
// mockStream 模拟生成的流名
func mockStreamName(uid string) string {
num := rand.Int63n(88888888)
return fmt.Sprintf("live_%s_%d", uid, num+1111111)
}
// mockStreamKey 模拟生成的key
func mockStreamKey(uid string) string {
str := fmt.Sprintf("nvijqwopW1%s%d", uid, time.Now().Unix())
h := md5.New()
h.Write([]byte(str))
cipherStr := h.Sum(nil)
md5Str := hex.EncodeToString(cipherStr)
return md5Str
}

View File

@@ -0,0 +1,235 @@
package http
import (
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"strconv"
"strings"
"time"
)
// 截图相关业务
// getScreeShotByRoomID 得到一个房间某个时间段的截图
func getSingleScreenShot(c *bm.Context) {
params := c.Request.URL.Query()
room := params.Get("room_id")
// 2018-10-24 14:27:07
start := params.Get("start_time")
end := params.Get("end_time")
channel := params.Get("channel")
c.Set("input_params", params)
if room == "" || start == "" || end == "" {
c.Set("output_data", "some fields are empty")
c.JSONMap(map[string]interface{}{"message": "some fields are empty"}, ecode.RequestErr)
c.Abort()
return
}
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "room_id is not right")
c.JSONMap(map[string]interface{}{"message": "room_id is not right"}, ecode.RequestErr)
c.Abort()
return
}
startTime, err := time.ParseInLocation("2006-01-02 15:04:05", start, time.Local)
if err != nil {
c.Set("output_data", "Start time format is incorrect")
c.JSONMap(map[string]interface{}{"message": "Start time format is incorrect"}, ecode.RequestErr)
c.Abort()
return
}
endTime, err := time.ParseInLocation("2006-01-02 15:04:05", end, time.Local)
if err != nil {
c.Set("output_data", "End time format is incorrect")
c.JSONMap(map[string]interface{}{"message": "End time format is incorrect"}, ecode.RequestErr)
c.Abort()
return
}
info, err := srv.GetSingleScreeShot(c, roomID, startTime.Unix(), endTime.Unix(), channel)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": map[string][]string{"list": info}}, nil)
}
// getMultiScreenShot 得到多个房间一个时间点截图
func getMultiScreenShot(c *bm.Context) {
params := c.Request.URL.Query()
rooms := params.Get("room_ids")
ts := params.Get("ts")
channel := params.Get("channel")
c.Set("input_params", params)
if rooms == "" || ts == "" {
c.Set("output_data", "some fields are empty")
c.JSONMap(map[string]interface{}{"message": "some fields are empty"}, ecode.RequestErr)
c.Abort()
return
}
// 切割room_id
roomIDs := strings.Split(rooms, ",")
if len(roomIDs) <= 0 {
c.Set("output_data", "room_ids is not right")
c.JSONMap(map[string]interface{}{"message": "room_ids is not right"}, ecode.RequestErr)
c.Abort()
return
}
tsInt, _ := strconv.ParseInt(ts, 10, 64)
rids := []int64{}
for _, v := range roomIDs {
roomID, err := strconv.ParseInt(v, 10, 64)
if err != nil {
log.Warn("room id is not right")
continue
}
rids = append(rids, roomID)
}
resp, err := srv.GetMultiScreenShot(c, rids, tsInt, channel)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", resp)
c.JSONMap(map[string]interface{}{"data": map[string]interface{}{"list": resp}}, nil)
}
// getOriginScreenShotPic 获取原始图片地址
func getOriginScreenShotPic(c *bm.Context) {
params := c.Request.URL.Query()
rooms := params.Get("room_ids")
ts := params.Get("ts")
//tp := params.Get("type")
c.Set("input_params", params)
if rooms == "" {
c.Set("output_data", "some fields are empty")
c.JSONMap(map[string]interface{}{"message": "some fields are empty"}, ecode.RequestErr)
c.Abort()
return
}
// 切割room_id
roomIDs := strings.Split(rooms, ",")
if len(roomIDs) <= 0 {
c.Set("output_data", "room_ids is not right")
c.JSONMap(map[string]interface{}{"message": "room_ids is not right"}, ecode.RequestErr)
c.Abort()
return
}
tsInt, _ := strconv.ParseInt(ts, 10, 64)
rids := []int64{}
for _, v := range roomIDs {
roomID, err := strconv.ParseInt(v, 10, 64)
if err != nil {
log.Warn("room id is not right")
continue
}
rids = append(rids, roomID)
}
resp, err := srv.GetOriginScreenShotPic(c, rids, tsInt)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", resp)
c.JSONMap(map[string]interface{}{"data": map[string]interface{}{"list": resp}}, nil)
}
// getTimePeriodScreenShot 获取多个房间一个时间段内的截图
func getTimePeriodScreenShot(c *bm.Context) {
params := c.Request.URL.Query()
room := params.Get("room_ids")
// 2018-10-24 14:27:07
start := params.Get("start_time")
end := params.Get("end_time")
channel := params.Get("channel")
c.Set("input_params", params)
if room == "" || start == "" || end == "" {
c.Set("output_data", "some fields are empty")
c.JSONMap(map[string]interface{}{"message": "some fields are empty"}, ecode.RequestErr)
c.Abort()
return
}
startTime, err := time.ParseInLocation("2006-01-02 15:04:05", start, time.Local)
if err != nil {
c.Set("output_data", "Start time format is incorrect")
c.JSONMap(map[string]interface{}{"message": "Start time format is incorrect"}, ecode.RequestErr)
c.Abort()
return
}
endTime, err := time.ParseInLocation("2006-01-02 15:04:05", end, time.Local)
if err != nil {
c.Set("output_data", "End time format is incorrect")
c.JSONMap(map[string]interface{}{"message": "End time format is incorrect"}, ecode.RequestErr)
c.Abort()
return
}
// 切割room_id
roomIDs := strings.Split(room, ",")
if len(roomIDs) <= 0 {
c.Set("output_data", "room_ids is not right")
c.JSONMap(map[string]interface{}{"message": "room_ids is not right"}, ecode.RequestErr)
c.Abort()
return
}
resp := map[int64][]string{}
for _, v := range roomIDs {
roomID, err := strconv.ParseInt(v, 10, 64)
if err != nil {
log.Warn("room id is not right")
continue
}
urls, err := srv.GetSingleScreeShot(c, roomID, startTime.Unix(), endTime.Unix(), channel)
if err != nil {
log.Warn("%v", err)
continue
}
resp[roomID] = urls
}
c.Set("output_data", resp)
c.JSONMap(map[string]interface{}{"data": resp}, nil)
}

View File

@@ -0,0 +1,86 @@
package http
import (
"fmt"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/metadata"
"strconv"
)
// cutStream 切流, 内部调用
func cutStream(c *bm.Context) {
// roomid 必须
params := c.Request.URL.Query()
room := params.Get("room_id")
cutTime := params.Get("cut_time")
c.Set("input_params", params)
// 验证传参数
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "roomid is not right")
c.JSONMap(map[string]interface{}{"message": "roomid is not right"}, ecode.RequestErr)
c.Abort()
return
}
// 默认切流时间为1s,可以传入-1
ct, err := strconv.ParseInt(cutTime, 10, 64)
if err != nil || ct == 0 {
ct = 1
}
srv.StreamCut(c, roomID, ct, 0)
c.Set("output_data", "ok")
c.JSONMap(map[string]interface{}{"data": map[string]int{}}, nil)
}
// cutStreamByExt 外部调用
func cutStreamByMobile(c *bm.Context) {
// roomid 必须
params := c.Request.URL.Query()
room := params.Get("room_id")
cutTime := params.Get("cut_time")
c.Set("input_params", params)
// 验证传参数
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "roomid is not right")
c.JSONMap(map[string]interface{}{"message": "roomid is not right"}, ecode.RequestErr)
c.Abort()
return
}
// 默认切流时间为1s,可以传入-1
ct, err := strconv.ParseInt(cutTime, 10, 64)
if err != nil || ct == 0 {
ct = 1
}
uid, ok := metadata.Value(c, metadata.Mid).(int64)
//uid = 19148701
//ok = true
if !ok {
c.Set("output_data", "未登陆")
c.JSONMap(map[string]interface{}{"message": fmt.Sprintf("未登陆")}, ecode.RequestErr)
c.Abort()
return
}
err = srv.StreamCut(c, roomID, ct, uid)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", "ok")
c.JSONMap(map[string]interface{}{"data": map[string]int{}}, nil)
}

View File

@@ -0,0 +1,196 @@
package http
import (
"encoding/json"
"fmt"
"go-common/app/service/video/stream-mng/model"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"io/ioutil"
"strconv"
"strings"
)
// addHotStream 增加房间热流标记
func addHotStream(c *bm.Context) {
req := c.Request
bs, err := ioutil.ReadAll(req.Body)
if err != nil {
c.Set("output_data", "ioutil.ReadAll() error")
c.JSONMap(map[string]interface{}{"message": "outil.ReadAll() error"}, err)
c.Abort()
return
}
req.Body.Close()
var hrbody []string
if err := json.Unmarshal(bs, &hrbody); err != nil {
c.Set("output_data", "json.Unmarshal() error")
c.JSONMap(map[string]interface{}{"message": "json.Unmarshal() error"}, err)
c.Abort()
return
}
if len(hrbody) <= 0 {
c.Set("output_data", "some fields are empty")
c.JSONMap(map[string]interface{}{"message": "some fields are empty"}, ecode.RequestErr)
c.Abort()
return
}
for _, streamName := range hrbody {
srv.AddHotStreamInfo(c, streamName)
}
c.Set("output_data", "success")
c.JSONMap(map[string]interface{}{"data": "success"}, nil)
}
// getStream 获取单个流信息
func getStream(c *bm.Context) {
params := c.Request.URL.Query()
rid := params.Get("room_id")
sname := params.Get("stream_name")
if rid == "" && sname == "" {
c.Set("output_data", "some fields are empty")
c.JSONMap(map[string]interface{}{"message": "some fields are empty"}, ecode.RequestErr)
c.Abort()
return
}
var roomID int64
var err error
var info *model.StreamFullInfo
if sname == "" {
roomID, err = strconv.ParseInt(rid, 10, 64)
// 验证传参数
if err != nil || roomID <= 0 {
c.Set("output_data", "roomid is not right")
c.JSONMap(map[string]interface{}{"message": "roomid is not right"}, ecode.RequestErr)
c.Abort()
return
}
info, err = srv.GetStreamInfo(c, roomID, "")
} else {
info, err = srv.GetStreamInfo(c, 0, sname)
}
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": info}, nil)
}
// getMulitiStreams 批量查询流接口
func getMultiStreams(c *bm.Context) {
params := c.Request.URL.Query()
roomID := params.Get("room_ids")
if roomID == "" {
c.Set("output_data", "some fields are empty")
c.JSONMap(map[string]interface{}{"message": "some fields are empty"}, ecode.RequestErr)
c.Abort()
return
}
roomIDs := []int64{}
rids := strings.Split(roomID, ",")
for _, v := range rids {
rid, err := strconv.ParseInt(v, 10, 64)
// 验证传参数
if err == nil && rid > 0 {
roomIDs = append(roomIDs, rid)
}
}
if len(roomIDs) > 30 {
c.Set("output_data", "The number of rooms must be less than 30")
c.JSONMap(map[string]interface{}{"message": "The number of rooms must be less than 30"}, ecode.RequestErr)
c.Abort()
return
}
info, err := srv.GetMultiStreamInfo(c, roomIDs)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
if info == nil || len(info) == 0 {
c.Set("output_data", fmt.Sprintf("can not find any info by room_ids=%s", roomID))
c.JSONMap(map[string]interface{}{"message": fmt.Sprintf("can not find any info by room_ids=%s", roomID)}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": info}, nil)
}
// getOldStreamInfoByRoomID map 到原始src数据
func getOldStreamInfoByRoomID(c *bm.Context) {
params := c.Request.URL.Query()
room := params.Get("roomid")
room2 := params.Get("room_id")
rid := ""
if room == "" {
rid = room2
} else {
rid = room
}
roomID, err := strconv.ParseInt(rid, 10, 64)
// 验证传参数
if err != nil || roomID <= 0 {
c.Set("output_data", "roomid is not right")
c.JSONMap(map[string]interface{}{"message": "roomid is not right"}, ecode.RequestErr)
c.Abort()
return
}
info, err := srv.GetStreamInfoByRIDMapSrcFromDB(c, roomID)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": info}, nil)
}
// getOldStreamInfoByStreamName map到原始src数据
func getOldStreamInfoByStreamName(c *bm.Context) {
params := c.Request.URL.Query()
sname := params.Get("stream_name")
sname = strings.TrimSpace(sname)
if sname == "" {
c.JSONMap(map[string]interface{}{"message": "stream name is empty"}, ecode.RequestErr)
c.Abort()
return
}
info, err := srv.GetStreamInfoBySNameMapSrcFromDB(c, sname)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": info}, nil)
}

View File

@@ -0,0 +1,103 @@
package http
import (
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"strconv"
"strings"
)
// getStreamLastTime 得到流到最后推流时间
func saveMaskByRoomID(c *bm.Context) {
params := c.Request.URL.Query()
roomid := params.Get("room_id")
mask := params.Get("mask")
c.Set("input_data", params)
roomID, err := strconv.ParseInt(roomid, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "room_id is not right")
c.JSONMap(map[string]interface{}{"message": "room_id is not right"}, ecode.RequestErr)
c.Abort()
return
}
int64mask, err := strconv.ParseInt(mask, 10, 64)
// 验证传参数
if err != nil || int64mask < 0 || int64mask > 1 {
c.Set("output_data", "mask is not right")
c.JSONMap(map[string]interface{}{"message": "mask is not right"}, ecode.RequestErr)
c.Abort()
return
}
//直接修改数据库,更新缓存
result, err := srv.ChangeMaskStreamByRoomID(c, roomID, "", int64mask)
if err != nil {
c.Set("output_data", err)
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", result)
c.JSONMap(map[string]interface{}{"data": result}, nil)
}
// getStreamLastTime 得到流到最后推流时间
func saveMaskByStreamName(c *bm.Context) {
params := c.Request.URL.Query()
sname := params.Get("stream_name")
mask := params.Get("mask")
c.Set("input_data", params)
if sname == "" {
c.Set("output_data", "stream_name is not right")
c.JSONMap(map[string]interface{}{"message": "stream_name is not right"}, ecode.RequestErr)
c.Abort()
return
}
int64mask, err := strconv.ParseInt(mask, 10, 64)
// 验证传参数
if err != nil || int64mask < 0 || int64mask > 1 {
c.Set("output_data", "mask is not right")
c.JSONMap(map[string]interface{}{"message": "mask is not right"}, ecode.RequestErr)
c.Abort()
return
}
var newmask int64
var newsname string
if len(sname) > 6 && strings.Contains(sname, "_wmask") {
//设置第三位为1
if int64mask == 0 {
newmask = 3
} else {
newmask = 2
}
newsname = sname[0 : len(sname)-6]
} else if len(sname) > 6 && strings.Contains(sname, "_mmask") {
//设置第四位为1
if int64mask == 0 {
newmask = 5
} else {
newmask = 4
}
newsname = sname[0 : len(sname)-6]
} else {
c.Set("output_data", "stream_name is not right")
c.JSONMap(map[string]interface{}{"message": "stream_name is not right"}, ecode.RequestErr)
c.Abort()
return
}
//直接修改数据库,更新缓存
result, err := srv.ChangeMaskStreamByRoomID(c, 0, newsname, newmask)
if err != nil {
c.Set("output_data", err)
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", result)
c.JSONMap(map[string]interface{}{"data": result}, nil)
}

View File

@@ -0,0 +1,69 @@
package http
import (
"encoding/json"
"go-common/app/service/video/stream-mng/service"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"io/ioutil"
)
// streamValidate 流鉴权接口
func streamValidate(c *bm.Context) {
var vp service.ValidateParams
switch c.Request.Header.Get("Content-Type") {
case "application/x-www-form-urlencoded":
if len(c.Request.PostForm) == 0 {
c.Set("output_data", "stream_valid_err = empty post body")
c.JSONMap(map[string]interface{}{"message": "empty post body"}, ecode.RequestErr)
c.Abort()
return
}
vp.Key = c.Request.PostFormValue("key")
vp.StreamName = c.Request.PostFormValue("stream_name")
vp.Src = c.Request.PostFormValue("src")
vp.Type = json.Number(c.Request.PostFormValue("type"))
default:
defer c.Request.Body.Close()
b, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
if len(b) == 0 {
c.Set("output_data", "stream_valid_err = empty params")
c.JSONMap(map[string]interface{}{"message": "empty params"}, ecode.RequestErr)
c.Abort()
return
}
err = json.Unmarshal(b, &vp)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
}
c.Set("input_params", vp)
permission, err := srv.CheckStreamValidate(c, &vp, false)
if err != nil {
c.Set("output_data", err.Error())
if err.Error() == "room is closed" {
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.LimitExceed)
} else {
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
}
c.Abort()
return
}
c.Set("output_data", permission)
c.JSONMap(map[string]interface{}{"data": map[string]int{"permission": permission}}, nil)
}

View File

@@ -0,0 +1,181 @@
package http
import (
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"strconv"
"strings"
)
// 简单的流信息的处理和返回
// getStreamLastTime 得到流到最后推流时间
func getStreamLastTime(c *bm.Context) {
// 获取url中的room_id
params := c.Request.URL.Query()
room := params.Get("room_id")
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "room_id is not right")
c.JSONMap(map[string]interface{}{"message": "room_id is not right"}, ecode.RequestErr)
c.Abort()
return
}
t, err := srv.GetStreamLastTime(c, roomID)
if err != nil {
log.Warn("%v", err)
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.JSONMap(map[string]interface{}{"data": map[string]int64{"last_time": t}}, nil)
}
// getStreamNameByRoomID 根据房间号获取流名
func getStreamNameByRoomID(c *bm.Context) {
params := c.Request.URL.Query()
room := params.Get("room_id")
back := params.Get("back")
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "room_id is not right")
c.JSONMap(map[string]interface{}{"message": "room_id is not right"}, ecode.RequestErr)
c.Abort()
return
}
flag := false
if back == "1" {
flag = true
}
info, err := srv.GetStreamNameByRoomID(c, roomID, flag)
if err != nil {
c.Set("output_data", err)
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
if !flag && len(info) > 0 {
c.JSONMap(map[string]interface{}{"data": info[0]}, nil)
return
}
c.JSONMap(map[string]interface{}{"data": info}, nil)
}
// getRoomIdByStreamName 得到房间号,传递流名,可传入备用流名
func getRoomIDByStreamName(c *bm.Context) {
params := c.Request.URL.Query()
sname := params.Get("stream_name")
sname = strings.TrimSpace(sname)
if len(sname) == 0 {
c.Set("output_data", "stream name is empty")
c.JSONMap(map[string]interface{}{"message": "stream name is empty"}, ecode.RequestErr)
c.Abort()
return
}
rid, err := srv.GetRoomIDByStreamName(c, sname)
if err != nil {
c.Set("output_data", err)
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.JSONMap(map[string]interface{}{"data": map[string]int64{"room_id": rid}}, nil)
}
// getAdapterStreamByStreamName 得到适配的流信息迁移PHP接口
func getAdapterStreamByStreamName(c *bm.Context) {
params := c.Request.URL.Query()
snames := params.Get("stream_names")
snames = strings.TrimSpace(snames)
if len(snames) == 0 {
c.Set("output_data", "stream names is empty")
c.JSONMap(map[string]interface{}{"message": "stream names is empty"}, ecode.RequestErr)
c.Abort()
return
}
// 最多查询500个数据
nameSlice := strings.Split(snames, ",")
if len(nameSlice) > 500 {
c.Set("output_data", "too many names")
c.JSONMap(map[string]interface{}{"message": "too many names"}, ecode.RequestErr)
c.Abort()
return
}
info := srv.GetAdapterStreamByStreamName(c, nameSlice)
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": info}, nil)
}
// getSrcByRoom 获取线路接口, 适配原PHP代码 线路名称+线路编码src+是否当前选择的线路
func getSrcByRoomID(c *bm.Context) {
params := c.Request.URL.Query()
room := params.Get("room_id")
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "room_id is not right")
c.JSONMap(map[string]interface{}{"message": "room_id is not right"}, ecode.RequestErr)
c.Abort()
return
}
info, err := srv.GetSrcByRoomID(c, roomID)
if err != nil {
c.Set("output_data", err)
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": info}, nil)
}
// getLineListByRoomID 得下线路信息, 和getSrcByRoomID 只有返回的格式不一样
func getLineListByRoomID(c *bm.Context) {
params := c.Request.URL.Query()
room := params.Get("room_id")
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "room_id is not right")
c.JSONMap(map[string]interface{}{"message": "room_id is not right"}, ecode.RequestErr)
c.Abort()
return
}
info, err := srv.GetLineListByRoomID(c, roomID)
if err != nil {
c.Set("output_data", err)
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": info}, nil)
}

View File

@@ -0,0 +1,279 @@
package http
import (
"fmt"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/metadata"
"net"
"strconv"
)
// 获取上行推流地址, 一共三个方法调用
// UpStream
func getUpStreamRtmp(c *bm.Context) {
params := c.Request.URL.Query()
room := params.Get("room_id")
// 来源pc表示PC端ios表示ios端android安卓端ios_link表示ios端android_link 安卓端live_mng表示live后台;vc_mng表示vc后台;
platform := params.Get("platform")
// client_ip
ip := params.Get("ip")
// 分区id
area := params.Get("area_id")
// 免流标志
freeFlow := params.Get("free_flow")
attentions := params.Get("attentions")
c.Set("input_params", params)
if room == "" || platform == "" || area == "" {
c.Set("output_data", "some fields are empty")
c.JSONMap(map[string]interface{}{"message": "some fields are empty"}, ecode.RequestErr)
c.Abort()
return
}
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "room_id is not right")
c.JSONMap(map[string]interface{}{"message": "房间号错误"}, ecode.RequestErr)
c.Abort()
return
}
var attentionsInt int
if attentions == "" {
attentionsInt = 0
} else {
att, _ := strconv.ParseInt(attentions, 10, 64)
attentionsInt = int(att)
}
// ip映射
realIP := ip
if ip == "" {
remoteAddr := c.Request.RemoteAddr
// 使用header: X-REAL-IP + X_FORWARED_FOR + reamoteadd
if add := c.Request.Header.Get("X-REAL-IP"); add != "" {
remoteAddr = add
} else if add = c.Request.Header.Get("X_FORWARED_FOR"); add != "" {
remoteAddr = add
} else {
remoteAddr, _, _ = net.SplitHostPort(remoteAddr)
}
if remoteAddr == "::1" {
remoteAddr = "127.0.0.1"
}
realIP = remoteAddr
}
areaID, _ := strconv.ParseInt(area, 10, 64)
info, err := srv.GetUpStreamRtmp(c, roomID, freeFlow, realIP, areaID, attentionsInt, 0, platform)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": "获取线路信息失败,刷新页面或稍后重试"}, ecode.RequestErr)
c.Abort()
return
}
if info == nil {
c.Set("output_data", fmt.Sprintf("can find any info by room_id=%d", roomID))
c.JSONMap(map[string]interface{}{"message": "获取线路信息失败,刷新页面或稍后重试"}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": map[string]interface{}{"up_stream": info}}, nil)
}
// getWebRtmp web端调用
func getWebRtmp(c *bm.Context) {
// 获取room_id
params := c.Request.URL.Query()
room := params.Get("room_id")
c.Set("input_params", params)
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "room_id is not right")
c.JSONMap(map[string]interface{}{"message": "房间号不正确"}, ecode.RequestErr)
c.Abort()
return
}
// 获取uid
uid, ok := metadata.Value(c, metadata.Mid).(int64)
//uid = 19148701
//ok = true
//log.Infov(c, log.KV("log", fmt.Sprintf("uid=%v", uid)))
if !ok {
log.Warn("%v=%v", uid, ok)
c.Set("output_data", "未登陆")
c.JSONMap(map[string]interface{}{"message": fmt.Sprintf("未登陆")}, ecode.RequestErr)
c.Abort()
return
}
remoteAddr := c.Request.RemoteAddr
// 使用header: X-REAL-IP + X_FORWARED_FOR + reamoteadd
if add := c.Request.Header.Get("X-REAL-IP"); add != "" {
remoteAddr = add
} else if add = c.Request.Header.Get("X_FORWARED_FOR"); add != "" {
remoteAddr = add
} else {
remoteAddr, _, _ = net.SplitHostPort(remoteAddr)
}
if remoteAddr == "::1" {
remoteAddr = "127.0.0.1"
}
realIP := remoteAddr
info, err := srv.GetWebRtmp(c, roomID, uid, realIP, "web")
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": "获取线路信息失败,刷新页面或稍后重试"}, ecode.RequestErr)
c.Abort()
return
}
if info == nil {
c.Set("output_data", fmt.Sprintf("can find any info by room_id=%d", roomID))
c.JSONMap(map[string]interface{}{"message": "获取线路信息失败,刷新页面或稍后重试"}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": info}, nil)
}
// getMobileRtmp 移动端调用
func getMobileRtmp(c *bm.Context) {
params := c.Request.URL.Query()
room := params.Get("room_id")
// 来源pc表示PC端ios表示ios端android安卓端ios_link表示ios端android_link 安卓端live_mng表示live后台;vc_mng表示vc后台;
platform := params.Get("platform")
// client_ip
ip := params.Get("ip")
// 分区id
area := params.Get("area_id")
// 免流标志
freeFlow := params.Get("free_flow")
c.Set("input_params", params)
if room == "" || platform == "" || area == "" {
c.Set("output_data", "some fields are empty")
c.JSONMap(map[string]interface{}{"message": "some fields are empty"}, ecode.RequestErr)
c.Abort()
return
}
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "房间号错误")
c.JSONMap(map[string]interface{}{"message": "房间号错误"}, ecode.RequestErr)
c.Abort()
return
}
// ip映射
realIP := ip
if ip == "" {
remoteAddr := c.Request.RemoteAddr
// 使用header: X-REAL-IP + X_FORWARED_FOR + reamoteadd
if add := c.Request.Header.Get("X-REAL-IP"); add != "" {
remoteAddr = add
} else if add = c.Request.Header.Get("X_FORWARED_FOR"); add != "" {
remoteAddr = add
} else {
remoteAddr, _, _ = net.SplitHostPort(remoteAddr)
}
if remoteAddr == "::1" {
remoteAddr = "127.0.0.1"
}
realIP = remoteAddr
}
areaID, _ := strconv.ParseInt(area, 10, 64)
// 获取uid
uid, ok := metadata.Value(c, metadata.Mid).(int64)
//uid = 19148701
//ok = true
if !ok {
c.Set("output_data", "未登陆")
c.JSONMap(map[string]interface{}{"message": fmt.Sprintf("未登陆")}, ecode.RequestErr)
c.Abort()
return
}
info, err := srv.GetUpStreamRtmp(c, roomID, freeFlow, realIP, areaID, 0, uid, platform)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": "获取线路信息失败,刷新页面或稍后重试"}, ecode.RequestErr)
c.Abort()
return
}
if info == nil {
c.Set("output_data", fmt.Sprintf("can find any info by room_id=%d", roomID))
c.JSONMap(map[string]interface{}{"message": "获取线路信息失败,刷新页面或稍后重试"}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": map[string]interface{}{"up_stream": info}}, nil)
}
// getRoomRtmp 拜年祭房间推流码接口
func getRoomRtmp(c *bm.Context) {
params := c.Request.URL.Query()
c.Set("input_params", params)
room := params.Get("room_id")
roomID, err := strconv.ParseInt(room, 10, 64)
if err != nil || roomID <= 0 {
c.Set("output_data", "房间号不正确")
c.JSONMap(map[string]interface{}{"message": "房间号不正确"}, ecode.RequestErr)
c.Abort()
return
}
info, err := srv.GetRoomRtmp(c, roomID)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": "获取房间信息失败"}, ecode.RequestErr)
c.Abort()
return
}
if info == nil {
c.Set("output_data", fmt.Sprintf("can find any info by room_id=%d", roomID))
c.JSONMap(map[string]interface{}{"message": "获取房间信息失败,请确认是否房间存在"}, ecode.RequestErr)
c.Abort()
return
}
c.Set("output_data", info)
c.JSONMap(map[string]interface{}{"data": info}, nil)
}

View File

@@ -0,0 +1,147 @@
package http
import (
"fmt"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"strconv"
"time"
)
// getSummaryUpStreamRtmp 查询统计信息
func getSummaryUpStreamRtmp(c *bm.Context) {
st, ed, err := analysisTimeParams(c)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
res, err := srv.GetSummaryUpStreamRtmp(c, st, ed)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.JSONMap(map[string]interface{}{"data": res}, nil)
}
// getSummaryUpStreamISP 获取运营商信息统计
func getSummaryUpStreamISP(c *bm.Context) {
st, ed, err := analysisTimeParams(c)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
res, err := srv.GetSummaryUpStreamISP(c, st, ed)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.JSONMap(map[string]interface{}{"data": res}, nil)
}
// getSummaryUpStreamISP 获取运营商信息统计
func getSummaryUpStreamCountry(c *bm.Context) {
st, ed, err := analysisTimeParams(c)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
res, err := srv.GetSummaryUpStreamCountry(c, st, ed)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.JSONMap(map[string]interface{}{"data": res}, nil)
}
// getSummaryUpStreamPlatform 获取Platform信息统计
func getSummaryUpStreamPlatform(c *bm.Context) {
st, ed, err := analysisTimeParams(c)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
res, err := srv.GetSummaryUpStreamPlatform(c, st, ed)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.JSONMap(map[string]interface{}{"data": res}, nil)
}
// getSummaryUpStreamCity 获取City信息统计
func getSummaryUpStreamCity(c *bm.Context) {
st, ed, err := analysisTimeParams(c)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
res, err := srv.GetSummaryUpStreamCity(c, st, ed)
if err != nil {
c.Set("output_data", err.Error())
c.JSONMap(map[string]interface{}{"message": err.Error()}, ecode.RequestErr)
c.Abort()
return
}
c.JSONMap(map[string]interface{}{"data": res}, nil)
}
// analysisTimeParams 分析传入参数
func analysisTimeParams(c *bm.Context) (int64, int64, error) {
params := c.Request.URL.Query()
start := params.Get("start")
end := params.Get("end")
var st int64
var ed int64
var err error
if start == "" {
t := time.Now()
year, month, day := t.Date()
st = time.Date(year, month, day, 0, 0, 0, 0, t.Location()).Unix()
} else {
st, err = strconv.ParseInt(start, 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("start is not right")
}
}
if end == "" {
ed = time.Now().Unix()
} else {
ed, err = strconv.ParseInt(end, 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("end is not right")
}
}
return st, ed, nil
}

View File

@@ -0,0 +1,62 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"backup.go",
"change-log.go",
"change-src.go",
"check-live.go",
"clear-cache.go",
"clear-retweet.go",
"common.go",
"main-stream.go",
"notify.go",
"official.go",
"screen-shot.go",
"service.go",
"stream.go",
"stream-cut.go",
"stream-info.go",
"stream-mask.go",
"stream-validate.go",
"upstream-rtmp.go",
"upstream-summary.go",
],
importpath = "go-common/app/service/video/stream-mng/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/location/api:go_default_library",
"//app/service/video/stream-mng/common:go_default_library",
"//app/service/video/stream-mng/conf:go_default_library",
"//app/service/video/stream-mng/dao:go_default_library",
"//app/service/video/stream-mng/model:go_default_library",
"//library/conf/env:go_default_library",
"//library/log:go_default_library",
"//library/net/metadata:go_default_library",
"//library/net/rpc/warden:go_default_library",
"//library/sync/pipeline/fanout:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/robfig/cron:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,29 @@
package service
import (
"context"
"go-common/app/service/video/stream-mng/model"
)
// CreateBackupStream 创建备用流
func (s *Service) CreateBackupStream(ctx context.Context, bs *model.BackupStream) (*model.BackupStream, error) {
res, err := s.dao.CreateBackupStream(ctx, bs)
if err == nil {
// 更新redis
s.dao.UpdateStreamStatusCache(ctx, &model.StreamStatus{
RoomID: bs.RoomID,
StreamName: bs.StreamName,
DefaultUpStream: bs.DefaultVendor,
DefaultChange: true,
Origin: bs.OriginUpstream,
OriginChange: true,
Forward: bs.Streaming,
ForwardChange: true,
Key: bs.Key,
Add: true,
})
}
return res, err
}

View File

@@ -0,0 +1,21 @@
package service
import (
"context"
"fmt"
"go-common/app/service/video/stream-mng/model"
"go-common/library/log"
)
// RecordChangeLog 记录操作日志
func (s *Service) RecordChangeLog(c context.Context, streamLog *model.StreamChangeLog) {
err := s.dao.InsertChangeLog(c, streamLog)
if err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("insert change log faild = %v", err)))
}
}
// GetChangeLogByRoomID 得到切换cdn 记录
func (s *Service) GetChangeLogByRoomID(c context.Context, rid int64, limit int64) (infos []*model.StreamChangeLog, err error) {
return s.dao.GetChangeLogByRoomID(c, rid, limit)
}

View File

@@ -0,0 +1,53 @@
package service
import (
"context"
"go-common/app/service/video/stream-mng/common"
"go-common/app/service/video/stream-mng/model"
"go-common/library/log"
"go-common/library/net/metadata"
)
// ChangeSrc 切换cdn
func (s *Service) ChangeSrc(c context.Context, rid int64, toOrigin int64, source string, operateName string, reason string) error {
_, origin, err := s.dao.OriginUpStreamInfo(c, rid)
if err != nil {
return err
}
// 更新正式流数据库
toSrc := common.BitwiseMapSrc[toOrigin]
err = s.dao.UpdateOfficialStreamStatus(c, rid, toSrc)
if err != nil {
return err
}
s.dao.UpdateStreamStatusCache(c, &model.StreamStatus{
RoomID: rid,
DefaultChange: true,
DefaultUpStream: toOrigin,
})
go func(ctx context.Context, rid int64, origin int64, toOrigin int64, reason, operateName, source string) {
// 更新main-stream
if err := s.dao.ChangeDefaultVendor(ctx, rid, toOrigin); err != nil {
log.Infov(ctx, log.KV("change_main_stream_default_err", err.Error()))
}
// 更新redis被切记录
s.dao.UpdateChangeSrcCache(ctx, rid, origin)
// 记录日志
s.RecordChangeLog(ctx, &model.StreamChangeLog{
RoomID: rid,
FromOrigin: origin,
ToOrigin: toOrigin,
Reason: reason,
OperateName: operateName,
Source: source,
})
}(metadata.WithContext(c), rid, origin, toOrigin, reason, operateName, source)
return nil
}

View File

@@ -0,0 +1,19 @@
package service
import (
"context"
"go-common/library/log"
)
// 在播列表
// refreshLiveStreamList 刷新在播列表缓存
func (s *Service) refreshLiveStreamList() {
log.Warn("refreshLiveStreamList")
s.dao.StoreLiveStreamList()
}
// checkLiveStreamList 确认流是否在
func (s *Service) CheckLiveStreamList(c context.Context, rids []int64) map[int64]bool {
return s.dao.LoadLiveStreamList(c, rids)
}

View File

@@ -0,0 +1,8 @@
package service
import "context"
// ClearRoomCacheByRID 清除一个房间缓存
func (s *Service) ClearRoomCacheByRID(c context.Context, rid int64) error {
return s.dao.DeleteStreamByRIDFromCache(c, rid)
}

View File

@@ -0,0 +1,53 @@
package service
import (
"context"
"fmt"
"go-common/app/service/video/stream-mng/model"
"go-common/library/log"
"go-common/library/net/metadata"
)
// ClearStreamStatus 清理互推标记
func (s *Service) ClearStreamStatus(c context.Context, rid int64) error {
// 主流状态更新 : 将up_rank=2的流up_rank 设置为0
err := s.dao.UpdateOfficialUpRankStatus(c, rid, 2, 0)
if err != nil {
return err
}
// 查询options
infos, err := s.dao.StreamFullInfo(c, rid, "")
//没有查到结果
if err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("select_maskbyroomid_error = %v", err)))
return err
}
var newmask int64
var options int64
if infos != nil && infos.List != nil {
for _, v := range infos.List {
if v.Type == 1 {
newmask = v.Options &^ 4
newmask = newmask &^ 8
}
}
}
// 清除缓存, 清除forward
s.dao.UpdateStreamStatusCache(c, &model.StreamStatus{
RoomID: rid,
Forward: 0,
ForwardChange: true,
Options: newmask,
OptionsChange: true,
})
// 同步数据
go func(ctx context.Context, roomID int64, newoptions int64, options int64) {
s.syncMainStream(ctx, roomID, "")
s.dao.ClearMainStreaming(ctx, roomID, newmask, options)
}(metadata.WithContext(c), rid, newmask, options)
// 备用流的状态无需更新
return nil
}

View File

@@ -0,0 +1,90 @@
package service
import (
"context"
"encoding/json"
"fmt"
location "go-common/app/service/main/location/api"
"go-common/app/service/video/stream-mng/model"
"go-common/library/conf/env"
"net/http"
)
const (
// 下面三个是老上行推流表中 up_rank 的三个状态
_originUpRankNothing = 0
//_originUpRankDefaultSrc = 1
_originUpRankForwardStreaming = 2
)
// CDNSalt cdn salt
var CDNSalt = map[string]string{
"bvc": "bvc_1701101740",
"js": "js_1703271720",
"qn": "qn_1703271730",
"txy": "txy_1610171720",
"ws": "ws_1608121700",
}
// ValidateParams 验证流合法性
type ValidateParams struct {
Key string `json:"key"`
Type json.Number `json:"type,omitempty"`
StreamName string `json:"stream_name"`
Src string `json:"src"`
}
// getLiveStreamUrl 对接live-stream.bilibili.co的相关业务
func (s *Service) getLiveStreamUrl(path string) string {
url := ""
if env.DeployEnv == env.DeployEnvProd {
url = fmt.Sprintf("%s%s", "http://prod-live-stream.bilibili.co", path)
} else {
url = fmt.Sprintf("%s%s", "http://live-stream.bilibili.co", path)
}
return url
}
// getRealRoomID 得到真正的ID
func (s *Service) getRealRoomID(rid int64) int64 {
type RespStruct struct {
Data *struct {
RoomID int64 `json:"room_id"`
} `json:"data"`
}
resp := RespStruct{}
c := context.Background()
uri := fmt.Sprintf("http://api.live.bilibili.com/room/v1/Room/room_init?id=%d", rid)
err := s.dao.NewRequst(c, http.MethodGet, uri, nil, nil, nil, &resp)
if err != nil || resp.Data == nil || resp.Data.RoomID == 0 {
return rid
}
return resp.Data.RoomID
}
// getLocation grpc 调用
func (s *Service) GetLocation(c context.Context, ip string) *model.UpStreamInfo {
res := &model.UpStreamInfo{}
res.IP = ip
if s.locationCli == nil {
res.Country = "中国"
return res
}
req := &location.InfoReq{Addr: ip}
resp, err := s.locationCli.Info(c, req)
if err != nil {
res.Country = "中国"
return res
}
res.Country = resp.Country
res.City = resp.Province
res.ISP = resp.Isp
return res
}

View File

@@ -0,0 +1,64 @@
package service
import (
"context"
"errors"
"fmt"
"go-common/app/service/video/stream-mng/model"
"go-common/library/log"
)
// 这是一个过渡接口,用于搬运现有数据到新表中
func (s *Service) syncMainStream(ctx context.Context, roomID int64, streamName string) error {
if roomID <= 0 && streamName == "" {
return errors.New("invalid params")
}
var err error
exists, err := s.dao.GetMainStreamFromDB(ctx, roomID, streamName)
if err != nil && err.Error() != "sql: no rows in result set" {
log.Errorv(ctx, log.KV("log", fmt.Sprintf("sync_stream_data_error = %v", err)))
return err
}
if exists != nil && (exists.RoomID == roomID || exists.StreamName == streamName) {
return nil
}
var full *model.StreamFullInfo
if roomID > 0 && streamName == "" {
full, err = s.GetStreamInfo(ctx, roomID, "")
} else if roomID <= 0 && streamName != "" {
full, err = s.GetStreamInfo(ctx, 0, streamName)
}
if err != nil {
return err
}
if full == nil {
return errors.New("unknow response")
}
for _, ss := range full.List {
if ss.Type == 1 {
ms := &model.MainStream{
RoomID: full.RoomID,
StreamName: ss.StreamName,
Key: ss.Key,
DefaultVendor: ss.DefaultUpStream,
Status: 1,
}
if ms.DefaultVendor == 0 {
ms.DefaultVendor = 1
}
_, err := s.dao.CreateNewStream(ctx, ms)
if err != nil {
log.Errorv(ctx, log.KV("log", fmt.Sprintf("sync_stream_data_error = %v", err)))
}
break
}
}
return nil
}

View File

@@ -0,0 +1,324 @@
package service
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"go-common/app/service/video/stream-mng/common"
"go-common/app/service/video/stream-mng/model"
"go-common/library/log"
"go-common/library/net/metadata"
"net/http"
"net/url"
"time"
)
// StreamingNotify 开关播回调
func (s *Service) StreamingNotify(ctx context.Context, p *model.StreamingNotifyParam, open bool) error {
// 流鉴权
if open {
val := &ValidateParams{
StreamName: p.StreamName,
Type: p.Type,
Src: p.SRC,
}
_, err := s.CheckStreamValidate(ctx, val, true)
if err != nil {
return err
}
}
// 校验
rid, src, bs, streamInfo, err := s.checkNotifyParams(ctx, p, open)
if err != nil {
return err
}
if bs == nil { // 如果不是备用流
// 写库
if p.Type.String() == "1" {
if open {
err = s.dao.SetOriginStreamingStatus(ctx, rid, src, _originUpRankNothing, _originUpRankForwardStreaming)
} else {
err = s.dao.SetOriginStreamingStatus(ctx, rid, src, _originUpRankForwardStreaming, _originUpRankNothing)
}
if err != nil {
return err
}
}
// 更新redis
var forwardVendor int64
var sname string
var origin int64
var postInfo *model.StreamFullInfo
var options int64
var newoptions int64
infoLen := len(streamInfo.List)
if infoLen > 1 {
postInfo = streamInfo
}
if streamInfo != nil && infoLen > 0 {
for _, v := range streamInfo.List {
if v.Type == 1 {
origin = v.Origin
sname = v.StreamName
options = v.Options
for _, k := range v.Forward {
forwardVendor += k
}
break
}
}
}
// 获取数据失败或者其他情况,直接删除缓存
if sname == "" {
s.dao.DeleteStreamByRIDFromCache(ctx, rid)
} else {
// 过渡接口 后续和main-stream保存一致
vendor := common.SrcMapBitwise[int8(src)]
if open {
//检查options第二位是否是1是的话通知AI
newoptions = options
if 2&options == 2 {
//-------------->此处通知AI<---------------
}
// 主推
if p.Type.String() == "0" {
s.dao.UpdateStreamStatusCache(ctx, &model.StreamStatus{
RoomID: rid,
StreamName: sname,
OriginChange: true,
Origin: vendor,
})
// 解决并发问题
go func(ctx context.Context, rid int64) {
time.Sleep(time.Second * 30)
s.dao.DeleteStreamByRIDFromCache(ctx, rid)
}(metadata.WithContext(ctx), rid)
if postInfo != nil {
for _, v := range postInfo.List {
if v.StreamName == sname {
v.Origin = vendor
break
}
}
}
} else {
s.dao.UpdateStreamStatusCache(ctx, &model.StreamStatus{
RoomID: rid,
StreamName: sname,
Forward: (forwardVendor | vendor),
ForwardChange: true,
})
if postInfo != nil {
for _, v := range postInfo.List {
if v.StreamName == sname {
v.Forward = append(v.Forward, vendor)
break
}
}
}
}
} else {
//关播回调 wmask mmask 第三位第四位都要清零 12 = 00001100
newoptions = options &^ 4
newoptions = newoptions &^ 8
if p.Type.String() == "0" {
// 关播需要判断是否为当前的流才可以更新
if origin == common.SrcMapBitwise[int8(src)] {
s.dao.UpdateStreamStatusCache(ctx, &model.StreamStatus{
RoomID: rid,
StreamName: sname,
Origin: 0,
OriginChange: true,
Forward: 0,
ForwardChange: true,
Options: newoptions,
OptionsChange: true,
})
if postInfo != nil {
for _, v := range postInfo.List {
if v.StreamName == sname {
v.Origin = 0
v.Forward = []int64{}
break
}
}
}
}
} else {
s.dao.UpdateStreamStatusCache(ctx, &model.StreamStatus{
RoomID: rid,
StreamName: sname,
Forward: (forwardVendor &^ vendor),
ForwardChange: true,
})
if postInfo != nil {
for _, v := range postInfo.List {
if v.StreamName == sname {
// 去除
forwards := []int64{}
for _, f := range v.Forward {
if f != vendor {
forwards = append(forwards, f)
}
}
v.Forward = forwards
break
}
}
}
}
}
}
//log.Warn("%v", postInfo)
// 同步数据
go func(ctx context.Context, roomID int64, legacySrc int8, isOpen bool, isOrigin bool, postInfo *model.StreamFullInfo, options int64, newoptions int64) {
if postInfo != nil {
s.updateLiveUpStream(ctx, postInfo)
}
s.syncMainStream(ctx, roomID, "")
if vendor, ok := common.SrcMapBitwise[legacySrc]; ok {
s.dao.MainStreamNotify(ctx, roomID, vendor, isOpen, isOrigin, options, newoptions)
}
}(metadata.WithContext(ctx), rid, int8(src), open, p.Type.String() == "0", postInfo, options, newoptions)
} else { // 如果是备用流
bs, err = s.dao.SetBackupStreamStreamingStatus(ctx, p, bs, open)
if err != nil {
return err
}
// 开播&主推
if open && p.Type.String() == "0" {
// 解决并发问题
go func(ctx context.Context, rid int64) {
time.Sleep(time.Second * 30)
s.dao.DeleteStreamByRIDFromCache(ctx, rid)
}(metadata.WithContext(ctx), rid)
}
// 已经封装好的bs
s.dao.UpdateStreamStatusCache(ctx, &model.StreamStatus{
RoomID: bs.RoomID,
StreamName: bs.StreamName,
ForwardChange: true,
Forward: bs.Streaming &^ bs.OriginUpstream,
OriginChange: true,
Origin: bs.OriginUpstream,
})
m, err := s.GetStreamInfoByRIDMapSrcFromDB(ctx, bs.RoomID)
if err == nil {
go s.updateLiveUpStream(metadata.WithContext(ctx), m)
}
}
// 广播(如果有), 无论主流还是备用流均发广播
if open && p.Type.String() == "0" {
body := []byte(fmt.Sprintf(`{"cmd":"LIVE", "roomid":"%d"}`, rid))
log.Info("%+v", string(body))
q := make(url.Values)
q.Set("ensure", "0")
q.Set("cid", fmt.Sprintf("%d", rid))
err := s.dao.NewRequst(ctx, http.MethodPost, "http://live-dm.bilibili.co/dm/1/push", q, body, nil, nil)
log.Infov(ctx, log.KV("body", string(body)), log.KV("type", "notify DM"))
if err != nil {
log.Errorv(ctx, log.KV("err", err), log.KV("type", "notify DM"))
}
}
return nil
}
func (s *Service) checkNotifyParams(ctx context.Context, p *model.StreamingNotifyParam, open bool) (int64, int, *model.BackupStream, *model.StreamFullInfo, error) {
ts, _ := p.TS.Int64()
if p == nil || p.StreamName == "" || p.SRC == "" || ts == 0 {
return 0, 0, nil, nil, errors.New("invalid params")
}
t, err := p.Type.Int64()
if err != nil {
return 0, 0, nil, nil, errors.New("invalid typeof type ")
}
if time.Now().Sub(time.Unix(ts, 0)) > time.Minute*30 {
return 0, 0, nil, nil, errors.New("ts expired")
}
// sign
if salt, ok := CDNSalt[p.SRC]; ok {
uri := "/live_stream/v1/StreamThird/close_notify"
if open {
uri = "/live_stream/v1/StreamThird/open_notify"
}
h := md5.New()
h.Write([]byte(fmt.Sprintf("%s%s%s", salt, uri, p.TS.String())))
log.Warn("sign = %v", hex.EncodeToString(h.Sum(nil)))
if p.Sign != hex.EncodeToString(h.Sum(nil)) {
return 0, 0, nil, nil, errors.New("invalid sign")
}
} else {
return 0, 0, nil, nil, errors.New("invalid src")
}
if bitwise, ok := common.CdnBitwiseMap[p.SRC]; ok {
log.Infov(ctx, log.KV("bitwise", bitwise), log.KV("t", t))
info, _ := s.dao.StreamFullInfo(ctx, 0, p.StreamName)
if info != nil && info.List != nil {
for _, row := range info.List {
if row.StreamName != p.StreamName {
continue
}
// 只有主流需要校验推流逻辑,
if row.Type == 1 && ((t == 0 && row.DefaultUpStream == bitwise) || (t == 1 && row.DefaultUpStream != bitwise)) {
return info.RoomID, int(common.BitwiseMapSrc[bitwise]), nil, info, nil
}
if row.Type == 2 {
bs, err := s.dao.GetBackupStreamByStreamName(ctx, p.StreamName)
if err != nil {
log.Infov(ctx, log.KV("query_backup_stream_with_err", err))
return 0, 0, nil, nil, errors.New("invalid stream name")
}
if bs != nil && bs.RoomID != 0 {
return bs.RoomID, 0, bs, nil, nil
}
}
}
}
}
return 0, 0, nil, nil, errors.New("invalid type")
}
// updateLiveUpStream 更新playurl缓存
func (s *Service) updateLiveUpStream(ctx context.Context, m *model.StreamFullInfo) {
b, err := json.Marshal(m)
if err == nil {
h := map[string]string{"Content-Type": "application/json"}
log.Infov(ctx, log.KV("body", string(b)), log.KV("type", "notify playurl"))
err := s.dao.NewRequst(ctx, http.MethodPost, "http://live-upstream.bilibili.co/live_stream/v1/Dispatch/set_streaminfo", nil, b, h, nil)
if err != nil {
log.Errorv(ctx, log.KV("err", err), log.KV("type", "notify playurl"))
}
}
}

View File

@@ -0,0 +1,55 @@
package service
import (
"context"
"fmt"
"go-common/app/service/video/stream-mng/common"
"go-common/app/service/video/stream-mng/model"
"go-common/library/log"
"go-common/library/net/metadata"
"time"
)
// CreateOfficalStream 创建正式流
func (s *Service) CreateOfficalStream(c context.Context, streamName string, key string, rid int64) bool {
succ := false
infos := []*model.OfficialStream{}
// 创建所有cdn的数据 ,为防止其中一条插入失败, 使用事务操作
for k, v := range common.CdnMapSrc {
var upRank int64
if k == common.QNName {
upRank = 1
}
ts := time.Now()
stream := model.OfficialStream{
RoomID: rid,
Name: streamName,
Key: key,
Src: v,
UpRank: upRank,
DownRank: 0,
Status: 0,
LastStatusUpdatedAt: ts,
UpdateAt: ts,
CreateAt: ts,
}
infos = append(infos, &stream)
}
err := s.dao.CreateOfficialStream(c, infos)
if err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("stream create faild err= %v, room_id= %d", err, rid)))
} else {
succ = true
go func(ctx context.Context, roomID int64) {
s.syncMainStream(ctx, roomID, "")
}(metadata.WithContext(c), rid)
}
return succ
}

View File

@@ -0,0 +1,171 @@
package service
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"go-common/app/service/video/stream-mng/common"
url2 "net/url"
"time"
)
// GetSingleScreeShot 得到一个房间一个时间段内截图
func (s *Service) GetSingleScreeShot(c context.Context, rid int64, startTime int64, endTime int64, channel string) ([]string, error) {
resp := []string{}
// todo 测试房间映射
streamName, origin, err := s.dao.OriginUpStreamInfo(c, rid)
if err != nil {
return nil, err
}
for i := startTime; i <= endTime; i += 15 {
resp = append(resp, s.getCapture(streamName, i, common.BitwiseMapSrc[origin], channel))
}
return resp, nil
}
// GetMultiScreenShot 多个房间一个时间戳截图
func (s *Service) GetMultiScreenShot(c context.Context, rids []int64, ts int64, channel string) (map[int64]string, error) {
infos, err := s.dao.MultiStreamInfo(c, rids)
res := map[int64]string{}
if err != nil {
return res, err
}
if infos == nil {
return res, fmt.Errorf("查询不到数据")
}
for id, item := range infos {
for _, v := range item.List {
if v.Type == 1 {
var or int64
if v.Origin != 0 {
or = v.Origin
} else {
or = v.DefaultUpStream
}
res[id] = s.getCapture(v.StreamName, ts, common.BitwiseMapSrc[or], channel)
break
}
}
}
return res, nil
}
// GetOriginScreenShotPic 多个房间一个时间戳原始截图
func (s *Service) GetOriginScreenShotPic(c context.Context, rids []int64, ts int64) (map[int64]string, error) {
infos, err := s.dao.MultiStreamInfo(c, rids)
res := map[int64]string{}
if err != nil {
return res, err
}
if infos == nil {
return res, fmt.Errorf("查询不到数据")
}
for id, item := range infos {
for _, v := range item.List {
if v.Type == 1 {
var or int64
if v.Origin != 0 {
or = v.Origin
} else {
or = v.DefaultUpStream
}
res[id] = s.getOriginCapture(v.StreamName, ts, common.BitwiseMapSrc[or])
break
}
}
}
return res, nil
}
// getOriginCapture 原始图片地址
func (s *Service) getOriginCapture(streamName string, t int64, src int8) string {
tu := time.Unix(t, 0)
dataStr := tu.Format("200601021504")
secondStr := fmt.Sprintf("%02d", (tu.Second()/15)*15)
ts := fmt.Sprintf("%s%s", dataStr, secondStr)
url := ""
switch src {
case common.WSSrc:
url = "http://live-jk-img.hdslb.net/original_live-%s--%s.jpg"
url = fmt.Sprintf(url, streamName, ts)
case common.TXYSrc:
tsd := time.Unix(t, 0).Format("2006-01-02")
url = fmt.Sprintf("http://bilibilitest-1252693259.cosgz.myqcloud.com/%s-%s/%s.png", tsd, streamName, ts)
case common.JSSrc:
url = fmt.Sprintf("https://ks3-cn-beijing.ksyun.com/live-image/record/live-js/%s/%s.png", streamName, ts)
case common.QNSrc:
url = fmt.Sprintf("http://qn.static.acgvideo.com/%s/%s.jpg", streamName, ts)
case common.BVCSrc:
path := fmt.Sprintf("/liveshotraw/%s_%s.png", streamName, ts)
params := make(url2.Values)
params.Set("deadline", fmt.Sprintf("%d", time.Now().Unix()+5184000))
url = fmt.Sprintf("http://upos-sz.acgvideo.com%s", s.getBVCSign(path, params))
default:
}
return url
}
// getCapture
func (s *Service) getCapture(streamName string, t int64, src int8, channel string) string {
// ts date('YmdHi', $iTS) . sprintf('%02d', (int)(date('s', $iTS) / 15) * 15);
tu := time.Unix(t, 0)
dataStr := tu.Format("200601021504")
secondStr := fmt.Sprintf("%02d", (tu.Second()/15)*15)
ts := fmt.Sprintf("%s%s", dataStr, secondStr)
tsd := time.Unix(t, 0).Format("2006-01-02")
url := ""
switch src {
case common.WSSrc:
url = "http://live-jk-img.hdslb.net/live-%s--%s.jpg"
if channel == "checkup_yellow" {
url = "http://live.pic.bilibili.8686c.com/live-%s--%s.jpg"
}
url = fmt.Sprintf(url, streamName, ts)
case common.TXYSrc:
url = fmt.Sprintf("http://bilibilitest-1252693259.cosgz.myqcloud.com/%s-%s/%s.jpg", tsd, streamName, ts)
case common.JSSrc:
url = fmt.Sprintf("https://ks3-cn-beijing.ksyun.com/live-image/record/live-js/%s/%s.jpg", streamName, ts)
case common.QNSrc:
url = fmt.Sprintf("http://qn.static.acgvideo.com/%s/%s.jpg?imageView2/0/h/293/format/jpg", streamName, ts)
case common.BVCSrc:
path := fmt.Sprintf("/liveshot/%s_%s.jpg", streamName, ts)
params := make(url2.Values)
params.Set("deadline", fmt.Sprintf("%d", time.Now().Unix()+5184000))
url = fmt.Sprintf("http://upos-sz-office.acgvideo.com%s", s.getBVCSign(path, params))
default:
}
return url
}
// getBVCSign bvc签名
func (s *Service) getBVCSign(path string, params url2.Values) string {
upsigSecret := "20170607920cbd5211831ce2a97066a8b544fa7b"
toSign := fmt.Sprintf("%s?%s", path, params.Encode())
h := md5.New()
h.Write([]byte(fmt.Sprintf("%s%s", toSign, upsigSecret)))
cipherStr := h.Sum(nil)
md5Str := hex.EncodeToString(cipherStr)
return fmt.Sprintf("%s&upsig=%s", toSign, md5Str)
}

View File

@@ -0,0 +1,61 @@
package service
import (
"context"
"github.com/robfig/cron"
location "go-common/app/service/main/location/api"
"go-common/app/service/video/stream-mng/conf"
"go-common/app/service/video/stream-mng/dao"
"go-common/library/net/rpc/warden"
"go-common/library/sync/pipeline/fanout"
xtime "go-common/library/time"
"time"
)
// Service struct
type Service struct {
c *conf.Config
dao *dao.Dao
locationCli location.LocationClient
cron *cron.Cron
liveAside *fanout.Fanout
}
// New init
func New(c *conf.Config) (s *Service) {
cfg := &warden.ClientConfig{
Dial: xtime.Duration(time.Second * 1),
Timeout: xtime.Duration(time.Second * 3),
}
locConn, err := warden.NewClient(cfg).Dial(context.Background(), "discovery://default/location.service")
if err != nil {
panic(err)
}
s = &Service{
c: c,
dao: dao.New(c),
locationCli: location.NewLocationClient(locConn),
cron: cron.New(),
liveAside: fanout.New("stream-service", fanout.Worker(2), fanout.Buffer(1024)),
}
//if err := s.cron.AddFunc("0 */1 * * * *", s.refreshLiveStreamList); err != nil {
// panic(err)
//}
//
//s.cron.Start()
return s
}
// Ping Service
func (s *Service) Ping(c context.Context) (err error) {
return s.dao.Ping(c)
}
// Close Service
func (s *Service) Close() {
s.cron.Stop()
s.dao.Close()
}

View File

@@ -0,0 +1,52 @@
package service
import (
"context"
"fmt"
"go-common/app/service/video/stream-mng/common"
"go-common/library/log"
"net/http"
)
// StreamCut 切流的房间和时间, 内部调用
func (s *Service) StreamCut(c context.Context, rid int64, ct int64, uid int64) error {
if uid != 0 {
if uid != s.getRoomUserID(c, rid) {
return fmt.Errorf("无权限,不是该房间用户")
}
}
// 根据房间号查询流名和直推&转推的cdn
info, err := s.dao.StreamFullInfo(c, rid, "")
if err != nil || info == nil || len(info.List) == 0 {
log.Errorv(c, log.KV("log", fmt.Sprintf("stream cut err = %v", err)))
return err
}
for _, v := range info.List {
if v.Type == 1 {
// 发送切流请求
uri := s.getLiveStreamUrl("/api/live/vendor/cutstream")
uri = fmt.Sprintf("%s?stream_name=%s&cut_time=%d", uri, v.StreamName, ct)
if v.Origin != 0 {
uri = fmt.Sprintf("%s&src=%s", uri, common.BitwiseMapName[v.Origin])
}
for _, i := range v.Forward {
if i != 0 {
uri = fmt.Sprintf("%s&src=%s", uri, common.BitwiseMapName[i])
}
}
log.Infov(c, log.KV("log", fmt.Sprintf("url=%s", uri)))
err = s.dao.NewRequst(c, http.MethodGet, uri, nil, nil, nil, nil)
if err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("stream cut err = %v", err)))
}
break
}
}
return nil
}

View File

@@ -0,0 +1,94 @@
package service
import (
"context"
"fmt"
"go-common/app/service/video/stream-mng/common"
"go-common/app/service/video/stream-mng/model"
)
// GetStreamInfo 获取单个流信息
func (s *Service) GetStreamInfo(c context.Context, roomID int64, sname string) (*model.StreamFullInfo, error) {
result, err := s.dao.StreamFullInfo(c, roomID, sname)
return result, err
}
// AddHotStreamInfo 增加热流信息到REDIS
func (s *Service) AddHotStreamInfo(c context.Context, streamName string) (string, error) {
roomid, _, err := s.dao.OriginUpStreamInfoBySName(c, streamName)
if err != nil {
return "error", err
} else {
if roomid <= 0 {
return "error", fmt.Errorf("room id < 0")
}
err := s.dao.UpdateRoomHotStatusCache(c, roomid, 1)
//err := s.dao.AddRoomHotCache(c, roomid)
if err == nil {
return "success", nil
}
return "error", err
}
}
// GetMultiStreamInfo 批量获取流信息
func (s *Service) GetMultiStreamInfo(c context.Context, rids []int64) (map[int64]*model.StreamFullInfo, error) {
result, err := s.dao.MultiStreamInfo(c, rids)
return result, err
}
// GetStreamInfoByRIDMapSrcFromDB 传入房间号,返回房间流信息,包含备用流+使用原始src
func (s *Service) GetStreamInfoByRIDMapSrcFromDB(c context.Context, roomID int64) (*model.StreamFullInfo, error) {
info, err := s.GetStreamInfo(c, roomID, "")
if err != nil {
return nil, err
}
return s.translateInfoBit2Src(info), nil
}
// GetStreamInfoBySNameMapSrcFromDB 传入流名,返回房间流信息,包含备用流+使用原始src
func (s *Service) GetStreamInfoBySNameMapSrcFromDB(c context.Context, sname string) (*model.StreamFullInfo, error) {
info, err := s.GetStreamInfo(c, 0, sname)
if err != nil {
return nil, err
}
return s.translateInfoBit2Src(info), nil
}
// translatebit2Src 将cdn对应的bit 转为原始的src
func (s *Service) translatebit2Src(b int64) int64 {
for k, v := range common.CdnBitwiseMap {
if v == b {
return int64(common.CdnMapSrc[k])
}
}
return 0
}
// translateInfoBit2Src 将原始结构转为src对应的list
func (s *Service) translateInfoBit2Src(info *model.StreamFullInfo) *model.StreamFullInfo {
resp := &model.StreamFullInfo{}
// 适配src
if info != nil {
resp.RoomID = info.RoomID
for _, v := range info.List {
fw := []int64{}
for _, v2 := range v.Forward {
fw = append(fw, s.translatebit2Src(v2))
}
resp.List = append(resp.List, &model.StreamBase{
StreamName: v.StreamName,
DefaultUpStream: s.translatebit2Src(v.DefaultUpStream),
Origin: s.translatebit2Src(v.Origin),
Forward: fw,
Type: v.Type,
})
}
}
return resp
}

View File

@@ -0,0 +1,78 @@
package service
import (
"context"
"errors"
"fmt"
"go-common/library/log"
)
func (s *Service) ChangeMaskStreamByRoomID(ctx context.Context, realRoomID int64, streamname string, mask int64) (t string, err error) {
//记录一下
log.Errorv(ctx, log.KV("log", fmt.Sprintf("mask is run = rid->%v sname->%v mask->%v", realRoomID, streamname, mask)))
retrylimit := 2
retry:
infos, err := s.dao.StreamFullInfo(ctx, realRoomID, streamname)
//没有查到结果
if err != nil {
log.Errorv(ctx, log.KV("log", fmt.Sprintf("select_maskbyroomid_error = %v", err)))
return "", err
}
var newmask int64
if infos != nil && infos.List != nil {
//当rid = infos.RoomID
realRoomID = infos.RoomID
for _, v := range infos.List {
if v.Type == 1 {
//做位运算
if mask == 0 {
//mask=0关上蒙版
newmask = v.Options &^ 2
newmask = newmask &^ 4
newmask = newmask &^ 8
} else if mask == 1 {
//mask==1打开蒙版
newmask = v.Options | 2
} else if mask == 2 {
//wmask蒙版可调度playurl
newmask = v.Options | 4
} else if mask == 3 {
//wmask蒙版停止调度playurl
newmask = v.Options &^ 4
} else if mask == 4 {
//mmask蒙版可调度playurl
newmask = v.Options | 8
} else if mask == 5 {
//mmask蒙版停止调度playurl
newmask = v.Options &^ 8
} else {
return "", errors.New("mask value is err")
}
//修改数据库 options字段
err := s.dao.ChangeMainStreamOptions(ctx, realRoomID, newmask, v.Options)
retrylimit--
if err != nil {
if retrylimit <= 0 {
//更新数据库重试2次
goto retry
}
return "", err
} else {
//更新缓存
err := s.dao.UpdateRoomOptionsCache(ctx, realRoomID, v.StreamName, newmask)
//重试一次,如果缓存更新失败会导致缓存存在期间数据库插入失败
if err != nil {
err := s.dao.UpdateRoomOptionsCache(ctx, realRoomID, v.StreamName, newmask)
if err != nil {
//再失败则删除缓存
s.dao.DeleteStreamByRIDFromCache(ctx, realRoomID)
}
}
return "success", nil
}
}
}
}
return "", errors.New("select mask options by roomid result is nil")
}

View File

@@ -0,0 +1,149 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"go-common/app/service/video/stream-mng/common"
"go-common/library/net/metadata"
"net/http"
"strings"
)
// CheckStreamValidate 根据流名查询正式流
func (s *Service) CheckStreamValidate(c context.Context, vp *ValidateParams, notify bool) (int, error) {
// 首先验证参数不能为空
vp.StreamName = strings.TrimSpace(vp.StreamName)
vp.Key = strings.TrimSpace(vp.Key)
vp.Src = strings.TrimSpace(vp.Src)
if vp.StreamName == "" || vp.Src == "" {
return 0, errors.New("some fields are empty")
}
checked := false
rid, _ := s.dao.StreamRIDByName(c, vp.StreamName)
if rid > 0 {
if !strings.Contains(vp.StreamName, "_bs_") {
checked = true
// 检验房间是否开播状态
bd := map[string]interface{}{
"ids": []int64{rid},
"fields": []string{"live_time"},
}
bdj, _ := json.Marshal(bd)
url := "http://api.live.bilibili.co/room/v2/Room/get_by_ids"
type liveTimeStruct struct {
Data map[string]map[string]interface{} `json:"data"`
}
liveResp := &liveTimeStruct{}
head := map[string]string{
"Content-Type": "application/json",
}
err := s.dao.NewRequst(c, http.MethodPost, url, nil, bdj, head, liveResp)
if err != nil {
return 0, err
}
liveTime := liveResp.Data[fmt.Sprintf("%d", rid)]["live_time"]
if liveTime == "0000-00-00 00:00:00" {
return 0, errors.New("room is closed")
}
}
}
// 验证src是否合法
iSc := common.CdnMapSrc[vp.Src]
if iSc == 0 {
return 0, errors.New("src is not right")
}
// 转换type
vpType, err := vp.Type.Int64()
if err != nil {
return 0, errors.New("type is not right")
}
// 返回信息
roomInfo, err := s.dao.StreamFullInfo(c, 0, vp.StreamName)
// 可以查询到
if err == nil && roomInfo != nil && len(roomInfo.List) > 0 {
realKey := ""
var origin int64
var rid int64
isBack := false
for _, v := range roomInfo.List {
if v.StreamName == vp.StreamName {
if v.Type == 2 {
isBack = true
}
realKey = v.Key
origin = v.DefaultUpStream
rid = roomInfo.RoomID
}
}
if !notify {
// 验证key是否匹配
if !strings.Contains(vp.Key, realKey) {
return 0, errors.New("key is not right")
}
}
// 主流需要验证,备用流不需要验证
if !isBack {
// 直/互推参数检验type 1:互推; 0直推
// src相等但是type为互推1 或者 src不相等 type为直推0
upSrc := common.BitwiseMapSrc[origin]
if ((upSrc == iSc) && vpType == 1) || (upSrc != iSc && vpType == 0) {
return 0, errors.New("key type is not right")
}
if !checked {
// 检验房间是否开播状态
bd := map[string]interface{}{
"ids": []int64{rid},
"fields": []string{"live_time"},
}
bdj, _ := json.Marshal(bd)
url := "http://api.live.bilibili.co/room/v2/Room/get_by_ids"
type liveTimeStruct struct {
Data map[string]map[string]interface{} `json:"data"`
}
liveResp := &liveTimeStruct{}
head := map[string]string{
"Content-Type": "application/json",
}
err = s.dao.NewRequst(c, http.MethodPost, url, nil, bdj, head, liveResp)
if err != nil {
return 0, err
}
liveTime := liveResp.Data[fmt.Sprintf("%d", rid)]["live_time"]
if liveTime == "0000-00-00 00:00:00" {
return 0, errors.New("room is closed")
}
}
}
// 同步数据
go func(ctx context.Context, roomID int64) {
s.syncMainStream(ctx, roomID, "")
}(metadata.WithContext(c), rid)
return 1, nil
}
return 0, nil
}

View File

@@ -0,0 +1,227 @@
package service
import (
"context"
"go-common/app/service/video/stream-mng/common"
"go-common/library/log"
"net/http"
"net/url"
)
type adpterStream struct {
Src int64 `json:"src"`
RoomID int64 `json:"room_id"`
UpRank int64 `json:"up_rank"`
SrcName string `json:"src_name"`
}
type roomSrc struct {
Src int64 `json:"src"`
Checked int `json:"checked"`
Desc string `json:"desc"`
}
type lineList struct {
Src int64 `json:"src"`
Use bool `json:"use"`
Desc string `json:"desc"`
}
// GetStreamLastTime 得到流到最后推流时间;主流的推流时间up_rank = 1
func (s *Service) GetStreamLastTime(c context.Context, rid int64) (t int64, err error) {
// 读取上行
streamName, origin, err := s.dao.OriginUpStreamInfo(c, rid)
if err != nil {
return 0, err
}
cdn := ""
for k, v := range common.BitwiseMapName {
if k == origin {
cdn = v
break
}
}
// 发送请求,三次重试机制
uri := s.getLiveStreamUrl("/api/live/vendor/checkstream")
params := make(url.Values)
params.Set("cdn", cdn)
params.Set("stream_name", streamName)
type RespStruct struct {
Code int `json:"code"`
Data []*map[string]int64 `json:"data"`
}
// 重试机制
for i := 0; i < 3; i++ {
resp := RespStruct{}
err := s.dao.NewRequst(c, http.MethodGet, uri, params, nil, nil, &resp)
if err != nil {
log.Warn("http request err = %v", err)
continue
}
if resp.Code == 0 && len(resp.Data) > 0 {
d := *resp.Data[0]
return d[streamName], nil
}
}
return 0, nil
}
// GetStreamNameByRoomID 需要考虑备用流 + 考虑短号
func (s *Service) GetStreamNameByRoomID(c context.Context, rid int64, back bool) ([]string, error) {
resp := []string{}
// 考虑短号
realRoomID := s.getRealRoomID(rid)
if !back {
name, _, err := s.dao.OriginUpStreamInfo(c, realRoomID)
if err != nil {
return resp, err
}
resp = append(resp, name)
return resp, nil
}
infos, err := s.dao.StreamFullInfo(c, realRoomID, "")
if err != nil {
return resp, err
}
if infos != nil && infos.List != nil {
for _, v := range infos.List {
resp = append(resp, v.StreamName)
}
}
return resp, nil
}
// GetRoomIDByStreamName 查询房间号
func (s *Service) GetRoomIDByStreamName(c context.Context, sname string) (int64, error) {
return s.dao.StreamRIDByName(c, sname)
}
// GetAdapterStreamByStreamName 适配结果输出, 此处也可以输入备用流, 该结果只输出直推上行
func (s *Service) GetAdapterStreamByStreamName(c context.Context, names []string) map[string]*adpterStream {
resp := map[string]*adpterStream{}
for _, n := range names {
if n != "" {
rid, origin, err := s.dao.OriginUpStreamInfoBySName(c, n)
if err != nil {
return resp
}
item := &adpterStream{}
item.RoomID = rid
item.UpRank = 1
// todo changesrc 完全上线后才使用
//item.Src = origin
item.Src = int64(common.BitwiseMapSrc[origin])
item.SrcName = common.NameMapBitwise[origin]
resp[n] = item
}
}
return resp
}
// GetSrcByRoomID 得到src
func (s *Service) GetSrcByRoomID(c context.Context, rid int64) ([]*roomSrc, error) {
_, origin, err := s.dao.DefaultUpStreamInfo(c, rid)
if err != nil {
return nil, err
}
resp := []*roomSrc{}
for k, v := range common.ChinaNameMapBitwise {
checked := 0
if v == origin {
checked = 1
}
// todo 等changesrc上线
resp = append(resp, &roomSrc{
//Src: v,
Src: int64(common.BitwiseMapSrc[v]),
Checked: checked,
Desc: k,
})
}
return resp, nil
}
// GetLineListByRoomID 获取线路信息
func (s *Service) GetLineListByRoomID(c context.Context, rid int64) ([]*lineList, error) {
_, origin, err := s.dao.DefaultUpStreamInfo(c, rid)
if err != nil {
return nil, err
}
resp := []*lineList{}
//aUse := lineList{}
//empty := false
for k, v := range common.ChinaNameMapBitwise {
// 网宿暂不开发到后台
if v == common.BitWiseWS {
continue
}
use := false
if v == origin {
use = true
}
//if use {
// //aUse.Src = v
// aUse.Src = int64(common.BitwiseMapSrc[v])
// aUse.Use = use
// aUse.Desc = k
//} else {
// resp = append(resp, lineList{
// Src: int64(common.BitwiseMapSrc[v]),
// Use: use,
// Desc: k,
// })
// //empty = true
//}
resp = append(resp, &lineList{
Src: int64(common.BitwiseMapSrc[v]),
Use: use,
Desc: k,
})
}
// 十九大cdn情况兼容
//setEmpty := &lineList{
// Src: int64(999),
// Use: empty,
// Desc: "空",
//}
//
//if aUse.Src != 0 {
// resp = append(resp, &aUse, setEmpty)
//} else {
// // 上行在网宿,就会执行
// resp = append(resp, empty)
//}
return resp, nil
}

View File

@@ -0,0 +1,556 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"go-common/app/service/video/stream-mng/common"
"go-common/app/service/video/stream-mng/conf"
"go-common/app/service/video/stream-mng/model"
"go-common/library/conf/env"
"go-common/library/log"
"go-common/library/net/metadata"
"net/http"
"net/url"
"strings"
)
// 此处写具体的实现方式
// UpStreamRtmp 上行推流地址格式
type UpStreamRtmp struct {
Addr string `json:"addr,omitempty"`
Code string `json:"code,omitempty"`
Name string `json:"name,omitempty"`
NewLink string `json:"new_link,omitempty"`
}
// JudgeChangeRtmp 判断是否需要切换cdn参数
type JudgeChangeRtmp struct {
RId int64 `json:"rid,omitempty"`
FreeFlow string `json:"freeflow,omitempty"`
IP string `json:"ip,omitempty"`
AreaID int64 `json:"area_id,omitempty"`
Attentions int `json:"attentions,omitempty"`
Uid int64 `json:"uid,omitempty"`
Platform string `json:"platform,omitempty"`
}
// GetUpStreamRtmp UpStream
func (s *Service) GetUpStreamRtmp(c context.Context, rid int64, freeFlow string, ip string, areaID int64, attentions int, uid int64, platform string) (up *UpStreamRtmp, err error) {
// 判断是否是该房间用户
if uid != 0 {
if uid != s.getRoomUserID(c, rid) {
return nil, fmt.Errorf("无权限,不是该房间用户")
}
}
judge := &JudgeChangeRtmp{
RId: rid,
FreeFlow: freeFlow,
IP: ip,
AreaID: areaID,
Attentions: attentions,
Platform: platform,
}
log.Warn("%v", judge)
sname, key, endUpStream, err := s.getEndUpStream(c, judge, false)
if err != nil {
return nil, err
}
// 获取url
addr, code := s.generateUpStreamUrl(c, sname, endUpStream, key)
newLink := s.formatNewLink(c, endUpStream, addr, code, false)
if freeFlow == "unicom" {
newLink = fmt.Sprintf("%s&unicom_free=1", newLink)
}
up = &UpStreamRtmp{
Addr: addr,
Code: code,
NewLink: newLink,
}
return up, nil
}
// GetWebRtmp web端调用
func (s *Service) GetWebRtmp(c context.Context, rid int64, uid int64, ip string, platform string) (interface{}, error) {
// 判断是否是该房间用户
if uid != s.getRoomUserID(c, rid) {
return nil, fmt.Errorf("无权限,不是该房间用户")
}
judge := &JudgeChangeRtmp{
RId: rid,
FreeFlow: "",
IP: ip,
AreaID: 0,
Attentions: 0,
Platform: platform,
}
sname, key, endUpStream, err := s.getEndUpStream(c, judge, true)
if err != nil {
return nil, err
}
resp := map[string]interface{}{}
// 获取url
addr, code := s.generateUpStreamUrl(c, sname, endUpStream, key)
resp["rtmp"] = map[string]string{
"addr": addr,
"code": code,
}
streamLine := []map[string]interface{}{}
streamLine = append(streamLine, map[string]interface{}{
"name": common.LineName[0],
"src": common.BitwiseMapSrc[endUpStream],
"cdn_name": common.BitwiseMapName[endUpStream],
"checked": 1,
})
// 获取是否是签约主播
sign := s.isSignRoom(c, uid)
// 测试房间
testRoom := map[int64]int64{
537499: 537499,
11891462: 11891462,
}
ok := false
_, ok = testRoom[rid]
if sign || ok {
for or, n := range common.BitwiseMapName {
if or != endUpStream && or != common.BitWiseWS {
streamLine = append(streamLine, map[string]interface{}{
"name": common.LineName[or],
"src": common.BitwiseMapSrc[or],
"cdn_name": n,
"checked": 0,
})
}
}
}
resp["stream_line"] = streamLine
return resp, nil
}
// GetRoomRtmp 拜年祭推流接口
func (s *Service) GetRoomRtmp(c context.Context, rid int64) (interface{}, error) {
info, err := s.dao.StreamFullInfo(c, rid, "")
if err != nil || info.RoomID <= 0 {
return nil, err
}
type rtmp struct {
Type int `json:"type"`
StreamName string `json:"stream_name"`
Key string `json:"key,omitempty"`
List []*UpStreamRtmp `json:"list"`
}
resp := []rtmp{}
// 只返回主流的一个推流码, 备用流的所有cdn推流码
for _, v := range info.List {
// 主流
if v.Type == 1 {
var or int64
if v.Origin != 0 {
or = v.Origin
} else {
or = v.DefaultUpStream
}
if or == 0 {
return nil, fmt.Errorf("can not find upstream by room_id=%d", rid)
}
addr, code := s.generateUpStreamUrl(c, v.StreamName, or, v.Key)
item := rtmp{
Type: 1,
StreamName: v.StreamName,
Key: v.Key,
}
item.List = append(item.List, &UpStreamRtmp{
Addr: addr,
Code: code,
Name: common.NameMapBitwise[or],
})
resp = append(resp, item)
} else {
item := rtmp{
Type: 2,
StreamName: v.StreamName,
Key: v.Key,
}
for k, b := range common.ChinaNameMapBitwise {
if b == common.BitWiseWS {
continue
}
addr, code := s.generateUpStreamUrl(c, v.StreamName, b, v.Key)
item.List = append(item.List, &UpStreamRtmp{
Name: k,
Addr: addr,
Code: code,
})
}
resp = append(resp, item)
}
}
if len(resp) == 0 {
return nil, nil
}
return resp, nil
}
func (s *Service) getEndUpStream(c context.Context, judge *JudgeChangeRtmp, web bool) (sname string, key string, end int64, err error) {
info := &model.StreamFullInfo{}
info, err = s.dao.StreamFullInfo(c, judge.RId, "")
if err != nil {
return
}
if info == nil || info.RoomID <= 0 {
err = fmt.Errorf("获取房间信息失败")
return
}
// 线路给默认上行的数据而不是origin
sname = ""
var origin int64
var endUpStream int64
key = ""
for _, v := range info.List {
if v.Type == 1 {
sname = v.StreamName
if v.DefaultUpStream != 0 {
origin = v.DefaultUpStream
endUpStream = v.DefaultUpStream
} else {
origin = v.Origin
endUpStream = v.Origin
}
key = v.Key
break
}
}
if sname == "" || key == "" || origin == 0 {
errStr, _ := json.Marshal(info)
log.Errorv(c, log.KV("log", string(errStr)))
err = fmt.Errorf("获取房间信息失败, room_id=%d", judge.RId)
return
}
// web 端调用, 不需要考虑免流和ismust
fromSrc, toSrc, isMust, isChange, reason, dispatch := s.getChangeSrcRule(c, origin, judge.RId, judge.FreeFlow, judge.IP, judge.AreaID, judge.Attentions, web)
if isChange {
// 重新设置上行, 免流的必须切成功,其他无所谓
err := s.dao.UpdateOfficialStreamStatus(c, judge.RId, common.BitwiseMapSrc[toSrc])
if err == nil {
endUpStream = toSrc
go func(ctx context.Context, rid, toSrc, fromSrc int64, reason string, sname string) {
s.dao.UpdateStreamStatusCache(ctx, &model.StreamStatus{
RoomID: rid,
StreamName: sname,
DefaultUpStream: toSrc,
DefaultChange: true,
})
// 更新main-stream
if err := s.dao.ChangeDefaultVendor(ctx, rid, toSrc); err != nil {
log.Infov(ctx, log.KV("change_main_stream_default_err", err.Error()))
}
s.RecordChangeLog(ctx, &model.StreamChangeLog{
RoomID: rid,
FromOrigin: fromSrc,
ToOrigin: toSrc,
Reason: reason,
OperateName: "auto_change",
Source: "background",
})
log.Infov(ctx, log.KV("log", fmt.Sprintf("auto_change_room:room_id=%d,from src=%d, tosrc=%d", rid, fromSrc, toSrc)))
}(metadata.WithContext(c), judge.RId, toSrc, fromSrc, reason, sname)
}
} else {
// 非web端必须切
if isMust && !web {
err = fmt.Errorf("auto_change_room failed:room_id=%d", judge.RId)
return
}
}
dispatch.PlatForm = judge.Platform
dispatch.RoomID = judge.RId
dispatch.CDN = endUpStream
log.Warn("%v", dispatch)
s.liveAside.Do(c, func(ctx context.Context) {
err := s.dao.CreateUpStreamDispatch(ctx, dispatch)
if err != nil {
log.Errorv(ctx, log.KV("log", fmt.Sprintf("tidb_err:=%v", err)))
}
})
return sname, key, endUpStream, nil
}
// getChangeSrcRule
func (s *Service) getChangeSrcRule(c context.Context, iLastCDN int64, rid int64, freeFlow string, ip string, areaID int64, attentions int, web bool) (fromOrigin int64, toOrigin int64, isMust bool, isChange bool, reason string, local *model.UpStreamInfo) {
// 1.免流卡切至视频云(优先),下次如果不免流则切回上一次的CDN 免流必须切成功
// 2.签约主播不自动切 ----》 废弃
// 3.是否被手动切过,是则不再做以下逻辑
// 4.国内放映厅切至视频云
// 5.海外切至腾讯
// 6.按尾号切 --->废弃
// 7.吃鸡分区,且关注数大于
fromOrigin = iLastCDN
// 判断用户所在区域
local = s.GetLocation(c, ip)
country := local.Country
log.Infov(c, log.KV("log", fmt.Sprintf("ip=%s;country=%s", ip, country)))
// 免流切到bvc
if freeFlow == "unicom" {
if iLastCDN != common.BitWiseBVC {
// 设置上次的redis
err := s.dao.UpdateLastCDNCache(c, rid, iLastCDN)
if err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("set_last_cdn_error:%v", err)))
}
toOrigin = common.BitWiseBVC
isMust = true
isChange = true
}
return fromOrigin, toOrigin, isMust, isChange, "bilibili unicom card", local
}
thisconf := *conf.Conf
if drop, ok := thisconf.DropCDN["dropCDN"].([]interface{}); ok {
for _, v := range drop {
if v.(int64) == fromOrigin {
toOrigin = common.BitWiseBVC
isChange = true
isMust = true
s.dao.DeleteLastCDNFromCache(c, rid)
return fromOrigin, toOrigin, isMust, isChange, "drop cdn", local
}
}
}
// 切空某家上行到另一家
// 此次是bvc, 上次不是bvc,切回原来的并删除上次切cdn的记录
if iLastCDN == common.BitWiseBVC {
origin, err := s.dao.GetLastCDNFromCache(c, rid)
if err != nil {
return 0, 0, isMust, isChange, "", local
}
if origin > 0 {
toOrigin = origin
isChange = true
s.dao.DeleteLastCDNFromCache(c, rid)
return fromOrigin, toOrigin, isMust, isChange, "cdn is bvc", local
}
}
// 被手动切过,是则不再做以下逻辑
or, err := s.dao.GetChangeSrcFromCache(c, rid)
if or != 0 && err == nil {
s.dao.DeleteLastCDNFromCache(c, rid)
return fromOrigin, toOrigin, isMust, isChange, "", local
}
// 国内吃鸡数大于1000给bvc
preAttention := common.ChickenAttention
if country == "中国" && areaID == common.AREAIDCHICKEN && attentions >= preAttention {
if fromOrigin != common.BitWiseBVC {
toOrigin = common.BitWiseBVC
isChange = true
s.dao.DeleteLastCDNFromCache(c, rid)
return fromOrigin, toOrigin, isMust, isChange, "country is china and area is chicken", local
}
}
// 海外节点给腾讯无论web端还是移动端均切换
if country != "中国" && country != "局域网" && country != "本机地址" {
if fromOrigin != common.BitWiseTC {
toOrigin = common.BitWiseTC
isChange = true
s.dao.DeleteLastCDNFromCache(c, rid)
return fromOrigin, toOrigin, isMust, isChange, "country is foreign country", local
}
}
// bvc 移动端 切给qn把尾号为2和5的的切给qn,先切20%
if !web {
if fromOrigin == common.BitWiseBVC && (rid%10 == 2 || rid%10 == 5) {
toOrigin = common.BitWiseQN
isChange = true
s.dao.DeleteLastCDNFromCache(c, rid)
return fromOrigin, toOrigin, isMust, isChange, "change bvc to qn", local
}
}
// 全量切网宿, 3:2=tc:bvc
if fromOrigin == common.BitWiseWS {
if rid%10 < 5 {
toOrigin = common.BitWiseTC
} else {
toOrigin = common.BitWiseBVC
}
isChange = true
isMust = true
s.dao.DeleteLastCDNFromCache(c, rid)
return fromOrigin, toOrigin, isMust, isChange, fmt.Sprintf("change ws to %s", common.BitwiseMapName[toOrigin]), local
}
return 0, 0, isMust, isChange, "", local
}
// generateUpStreamUrl 生成推流url
func (s *Service) generateUpStreamUrl(c context.Context, sname string, origin int64, key string) (rtmp string, code string) {
srcKw := common.BitwiseMapName[origin]
if srcKw == common.WSName {
rtmp = "rtmp://live-send.acg.tv/live"
code = fmt.Sprintf("%s?streamname=%s&key=%s", sname, sname, key)
} else {
rtmp = fmt.Sprintf("rtmp://%s.live-send.acg.tv/live-%s/", srcKw, srcKw)
code = fmt.Sprintf("?streamname=%s&key=%s", sname, key)
}
return
}
// formatNewLink
func (s *Service) formatNewLink(c context.Context, origin int64, addr string, code string, isHttps bool) string {
upRtmp := ""
if common.BitwiseMapName[origin] == common.WSName {
upRtmp = fmt.Sprintf("%s/%s", addr, code)
} else {
upRtmp = fmt.Sprintf("%s%s", addr, code)
}
val := ""
if isHttps {
val = common.NewLinkMap[origin]["newLinkHttps"]
} else {
val = common.NewLinkMap[origin]["newLink"]
}
if val == "" {
return val
}
// val = http://tcdns.myqcloud.com:8086/bilibili_redirect?up_rtmp=
// upRtmp = rtmp://live-send.acg.tv/live/live_19148701_6447624?streamname=live_19148701_6447624&key=f2b23e1a938b6f979fa080f2bddc9225
upRtmpEncode := url.QueryEscape(strings.Replace(upRtmp, "rtmp://", "", -1))
newlink := fmt.Sprintf("%s%s", val, upRtmpEncode)
return newlink
}
// getRoomUserID 获取一个房间的uid
func (s *Service) getRoomUserID(c context.Context, rid int64) int64 {
bd := map[string]interface{}{
"ids": []int64{rid},
}
bdj, _ := json.Marshal(bd)
url := ""
if env.DeployEnv == env.DeployEnvProd {
url = "http://api.live.bilibili.co/room/v2/Room/get_by_ids"
} else {
url = "http://api.live.bilibili.com/room/v2/Room/get_by_ids"
}
type liveTimeStruct struct {
Data map[string]map[string]interface{} `json:"data"`
}
liveResp := &liveTimeStruct{}
head := map[string]string{
"Content-Type": "application/json",
}
err := s.dao.NewRequst(c, http.MethodPost, url, nil, bdj, head, liveResp)
if err != nil {
log.Warn("%v", err)
return 0
}
log.Infov(c, log.KV("log", fmt.Sprintf("url=%s;resp=%v", url, liveResp)))
if id, ok := liveResp.Data[fmt.Sprintf("%d", rid)]["uid"].(float64); ok {
return int64(id)
}
return 0
}
// 判断是否是签约的房间
func (s *Service) isSignRoom(c context.Context, rid int64) bool {
if rid <= 0 {
return false
}
url := "http://api.live.bilibili.co/live_user/v1/Anchor/get_badge"
type signStatus struct {
Data map[string]map[string]bool `json:"data,omitempty"`
}
var bd bytes.Buffer
fmt.Fprint(&bd, "uids[]=", strings.Join([]string{fmt.Sprint(rid)}, ","))
log.Warn("%s", bd.String())
//bdj, _ := json.Marshal(bd)
resp := &signStatus{}
head := map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
}
err := s.dao.NewRequst(c, http.MethodPost, url, nil, bd.Bytes(), head, resp)
if err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("%v=%v", err, resp)))
return false
}
log.Infov(c, log.KV("log", fmt.Sprintf("%v", resp)))
return resp.Data[fmt.Sprintf("%d", rid)]["is_signed_anchor"]
}

View File

@@ -0,0 +1,31 @@
package service
import (
"context"
"go-common/app/service/video/stream-mng/model"
)
// GetSummaryUpStreamRtmp 统计上行调度信息
func (s *Service) GetSummaryUpStreamRtmp(c context.Context, start int64, end int64) ([]*model.SummaryUpStreamRtmp, error) {
return s.dao.GetSummaryUpStreamRtmp(c, start, end)
}
// GetSummaryUpStreamISP 统计ISP调度信息
func (s *Service) GetSummaryUpStreamISP(c context.Context, start int64, end int64) ([]*model.SummaryUpStreamRtmp, error) {
return s.dao.GetSummaryUpStreamISP(c, start, end)
}
// GetSummaryUpStreamCountry 统计Country调度信息
func (s *Service) GetSummaryUpStreamCountry(c context.Context, start int64, end int64) ([]*model.SummaryUpStreamRtmp, error) {
return s.dao.GetSummaryUpStreamCountry(c, start, end)
}
// GetSummaryUpStreamCountry 统计Platform调度信息
func (s *Service) GetSummaryUpStreamPlatform(c context.Context, start int64, end int64) ([]*model.SummaryUpStreamRtmp, error) {
return s.dao.GetSummaryUpStreamPlatform(c, start, end)
}
// GetSummaryUpStreamCity 统计City调度信息
func (s *Service) GetSummaryUpStreamCity(c context.Context, start int64, end int64) ([]*model.SummaryUpStreamRtmp, error) {
return s.dao.GetSummaryUpStreamCity(c, start, end)
}