A signaling server for GB28181
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,3 +21,6 @@
|
|||||||
|
|
||||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
hs_err_pid*
|
hs_err_pid*
|
||||||
|
bin/
|
||||||
|
objs
|
||||||
|
.idea
|
||||||
|
|||||||
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
// 使用 IntelliSense 了解相关属性。
|
||||||
|
// 悬停以查看现有属性的描述。
|
||||||
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Package",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/main",
|
||||||
|
"env": {},
|
||||||
|
"args": ["-sip-port", "5080", "-media-addr", "127.0.0.1:1985"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2022 ossrs
|
Copyright (c) 2024 Haibo Chen
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
22
Makefile
Normal file
22
Makefile
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
GOCMD=go
|
||||||
|
GOBUILD=$(GOCMD) build
|
||||||
|
BINARY_NAME=bin/srs-sip
|
||||||
|
MAIN_PATH=main/main.go
|
||||||
|
|
||||||
|
default: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
$(GOBUILD) -o $(BINARY_NAME) $(MAIN_PATH)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BINARY_NAME)
|
||||||
|
|
||||||
|
run:
|
||||||
|
$(GOBUILD) -o $(BINARY_NAME) $(MAIN_PATH)
|
||||||
|
./$(BINARY_NAME)
|
||||||
|
|
||||||
|
install:
|
||||||
|
$(GOBUILD) -o $(BINARY_NAME) $(MAIN_PATH)
|
||||||
|
mv $(BINARY_NAME) /usr/local/bin
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
74
README.md
Normal file
74
README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# SRS-SIP
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Pre-requisites:
|
||||||
|
- Go 1.20+ is installed
|
||||||
|
- GOPATH/bin is in your PATH
|
||||||
|
|
||||||
|
Then run
|
||||||
|
```
|
||||||
|
git clone https://github.com/ossrs/srs-sip
|
||||||
|
cd srs-sip
|
||||||
|
./bootstrap.sh
|
||||||
|
mage
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are on a Unix-like system, you can also run the following command.
|
||||||
|
```
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the program:
|
||||||
|
|
||||||
|
```
|
||||||
|
./bin/srs-sip -sip-port 5060 -media-addr 127.0.0.1:1985 -api-port 2020 -http-server-port 8888
|
||||||
|
```
|
||||||
|
|
||||||
|
- `sip-port` : the SIP port, this program listen on, for device register with gb28181
|
||||||
|
- `media-addr` : the API address for SRS, typically on port 1985, used to send HTTP requests to "/gb/v1/publish"
|
||||||
|
- `api-port`: The API server port, used to send HTTP requests, for example "/srs-sip/v1/channels"
|
||||||
|
- `http-server-port`: The demo web server.
|
||||||
|
|
||||||
|
Access http://localhost:8888 in web browser.
|
||||||
|
|
||||||
|
## Sequence
|
||||||
|
|
||||||
|
1. 注册流程
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
Device ->> SRS-SIP : 1. Register
|
||||||
|
SRS-SIP ->> Device : 2. 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
暂时没有实现鉴权功能,敬请期待。
|
||||||
|
|
||||||
|
2. 播放视频流程
|
||||||
|
Player、SRS-SIP、SRS Server和GB28181 Device的交互图如下:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
Player ->> SRS-SIP : 1. Play Request(with id)
|
||||||
|
SRS-SIP ->> SRS : 2. Publish Request(with ssrc and id)
|
||||||
|
SRS ->> SRS-SIP : 3. Response(with port)
|
||||||
|
SRS-SIP ->> Device : 4. Invite(with port)
|
||||||
|
Device ->> SRS-SIP : 5. 200 OK
|
||||||
|
SRS-SIP ->> Player : 6. 200 OK(with url)
|
||||||
|
Device -->> SRS : Media Stream
|
||||||
|
Player ->> SRS : 7. Play
|
||||||
|
SRS -->> Player : Media Stream
|
||||||
|
Player ->> SRS-SIP : 8. Stop Request
|
||||||
|
SRS-SIP ->> SRS : 9. Unpublish Request
|
||||||
|
SRS-SIP ->> Device : 10. Bye
|
||||||
|
```
|
||||||
|
|
||||||
|
1. 通过SRS-SIP提供的API接口`/srs-sip/v1/invite`,Player主动发起播放请求,携带设备的通道ID
|
||||||
|
2. SRS-SIP向SRS发起推流请求,携带SSRC和ID,SSRC是设备推流时RTP里的字段
|
||||||
|
3. SRS响应推流请求,并返回收流端口。目前SRS仅支持TCP单端口模式,在配置文件`stream_caster.listen`中配置
|
||||||
|
4. SRS-SIP通过GB28181协议向设备发起`Invite`请求,携带SRS的收流端口及SSRC
|
||||||
|
5. 设备响应成功
|
||||||
|
6. SRS-SIP响应成功,携带URL,用于播放
|
||||||
|
7. Player通过返回的URL进行拉流播放
|
||||||
|
8. Player停止播放
|
||||||
|
9. SRS-SIP通知SRS停止收流
|
||||||
|
10. SRS-SIP通过设备停止推流
|
||||||
49
bootstrap.sh
Executable file
49
bootstrap.sh
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
add_gopath_to_path() {
|
||||||
|
GOPATH=$(go env GOPATH)
|
||||||
|
# Check if GOPATH is set
|
||||||
|
if [ -z "$GOPATH" ]; then
|
||||||
|
echo "GOPATH is not set."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if $GOPATH/bin is already in ~/.bashrc
|
||||||
|
if grep -q "$GOPATH/bin" ~/.bashrc; then
|
||||||
|
echo "$GOPATH/bin is already in PATH."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add $GOPATH/bin to PATH
|
||||||
|
echo "export PATH=\$PATH:$GOPATH/bin" >> ~/.bashrc
|
||||||
|
source ~/.bashrc
|
||||||
|
|
||||||
|
echo "$GOPATH/bin has been added to PATH."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if ! command -v mage &> /dev/null
|
||||||
|
then
|
||||||
|
pushd /tmp
|
||||||
|
|
||||||
|
OS_IS_LINUX=$(uname -s |grep -q Linux && echo YES)
|
||||||
|
if [ "$OS_IS_LINUX" == "YES" ]; then
|
||||||
|
add_gopath_to_path
|
||||||
|
if [ $? -eq 1 ]; then
|
||||||
|
echo "error: Failed to add $GOPATH/bin to PATH."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
git clone https://github.com/magefile/mage
|
||||||
|
cd mage
|
||||||
|
go run bootstrap.go
|
||||||
|
rm -rf /tmp/mage
|
||||||
|
popd
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v mage &> /dev/null
|
||||||
|
then
|
||||||
|
echo "error: Ensure `go env GOPATH`/bin is in your \$PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
go mod download
|
||||||
41
go.mod
Normal file
41
go.mod
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
module github.com/ossrs/srs-sip
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/handlers v1.5.2
|
||||||
|
github.com/gorilla/mux v1.8.1
|
||||||
|
github.com/magefile/mage v1.15.0
|
||||||
|
github.com/ossrs/go-oryx-lib v0.0.9
|
||||||
|
github.com/ossrs/srs-bench v0.0.0-20230906232735-aa029b492d0f
|
||||||
|
github.com/rs/zerolog v1.32.0
|
||||||
|
golang.org/x/net v0.10.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/emiago/sipgo v0.22.1 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||||
|
github.com/ghettovoice/gosip v0.0.0-20220929080231-de8ba881be83 // indirect
|
||||||
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
|
github.com/gobwas/ws v1.3.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/icholy/digest v0.1.22 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||||
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
|
github.com/pion/rtp v1.7.13 // indirect
|
||||||
|
github.com/pion/webrtc/v3 v3.2.9 // indirect
|
||||||
|
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 // indirect
|
||||||
|
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
|
||||||
|
github.com/yapingcat/gomedia/codec v0.0.0-20220617074658-94762898dc25 // indirect
|
||||||
|
github.com/yapingcat/gomedia/mpeg2 v0.0.0-20220617074658-94762898dc25 // indirect
|
||||||
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
|
golang.org/x/term v0.8.0 // indirect
|
||||||
|
golang.org/x/text v0.9.0 // indirect
|
||||||
|
)
|
||||||
271
go.sum
Normal file
271
go.sum
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca/go.mod h1:W+3LQaEkN8qAwwcw0KC546sUEnX86GIT8CcMLZC4mG0=
|
||||||
|
github.com/emiago/sipgo v0.22.0 h1:GaQ51m26M9QnVBVY2aDJ/mXqq/BDfZ1A+nW7XgU/4Ts=
|
||||||
|
github.com/emiago/sipgo v0.22.0/go.mod h1:a77FgPEEjJvfYWYfP3p53u+dNhWEMb/VGVS6guvBzx0=
|
||||||
|
github.com/emiago/sipgo v0.22.1 h1:imidktnLwl+fUKPAUUhxQJ4E3sDDaMBvoEvUOMJaSOI=
|
||||||
|
github.com/emiago/sipgo v0.22.1/go.mod h1:a77FgPEEjJvfYWYfP3p53u+dNhWEMb/VGVS6guvBzx0=
|
||||||
|
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||||
|
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/ghettovoice/gosip v0.0.0-20220929080231-de8ba881be83 h1:4v14bwSGZH2usyuG9XWZgMbGkVU33ayg0cb68nvKfj0=
|
||||||
|
github.com/ghettovoice/gosip v0.0.0-20220929080231-de8ba881be83/go.mod h1:yTr3BEYSFe9As6XM7ldyrVgqsPwlnw8Ahc4N28VFM2g=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.1.0-rc.1/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
|
||||||
|
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
|
||||||
|
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||||
|
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/icholy/digest v0.1.22 h1:dRIwCjtAcXch57ei+F0HSb5hmprL873+q7PoVojdMzM=
|
||||||
|
github.com/icholy/digest v0.1.22/go.mod h1:uLAeDdWKIWNFMH0wqbwchbTQOmJWhzSnL7zmqSPqEEc=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
|
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/nxadm/tail v1.4.5/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||||
|
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||||
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
|
||||||
|
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
|
||||||
|
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||||
|
github.com/ossrs/go-oryx-lib v0.0.9 h1:piZkzit/1hqAcXP31/mvDEDpHVjCmBMmvzF3hN8hUuQ=
|
||||||
|
github.com/ossrs/go-oryx-lib v0.0.9/go.mod h1:i2tH4TZBzAw5h+HwGrNOKvP/nmZgSQz0OEnLLdzcT/8=
|
||||||
|
github.com/ossrs/srs-bench v0.0.0-20230906232735-aa029b492d0f h1:qvibrAolgLiEgbwtWbUy4Ts48sfURc7+7UaGxi2euyo=
|
||||||
|
github.com/ossrs/srs-bench v0.0.0-20230906232735-aa029b492d0f/go.mod h1:aba1nViJ8Cd37kvuyhUrZ3kY1ASxFldaA8o1pLlZO6Y=
|
||||||
|
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||||
|
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||||
|
github.com/pion/ice/v2 v2.3.6/go.mod h1:9/TzKDRwBVAPsC+YOrKH/e3xDrubeTRACU9/sHQarsU=
|
||||||
|
github.com/pion/interceptor v0.1.17/go.mod h1:SY8kpmfVBvrbUzvj2bsXz7OJt5JvmVNZ+4Kjq7FcwrI=
|
||||||
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
|
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
|
||||||
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
|
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||||
|
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
||||||
|
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||||
|
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||||
|
github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU=
|
||||||
|
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||||
|
github.com/pion/srtp/v2 v2.0.15/go.mod h1:b/pQOlDrbB0HEH5EUAQXzSYxikFbNcNuKmF8tM0hCtw=
|
||||||
|
github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
|
||||||
|
github.com/pion/stun v0.6.0/go.mod h1:HPqcfoeqQn9cuaet7AOmB5e5xkObu9DwBdurwLKO9oA=
|
||||||
|
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||||
|
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
||||||
|
github.com/pion/transport/v2 v2.1.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
|
||||||
|
github.com/pion/transport/v2 v2.2.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
|
||||||
|
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||||
|
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
|
||||||
|
github.com/pion/webrtc/v3 v3.2.9 h1:U8NSjQDlZZ+Iy/hg42Q/u6mhEVSXYvKrOIZiZwYTfLc=
|
||||||
|
github.com/pion/webrtc/v3 v3.2.9/go.mod h1:gjQLMZeyN3jXBGdxGmUYCyKjOuYX/c99BDjGqmadq0A=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||||
|
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
|
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM=
|
||||||
|
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 h1:hNna6Fi0eP1f2sMBe/rJicDmaHmoXGe1Ta84FPYHLuE=
|
||||||
|
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0=
|
||||||
|
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||||
|
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||||
|
github.com/yapingcat/gomedia/codec v0.0.0-20220609081842-9e0c0e8a19a0/go.mod h1:obSECV6X3NPUsLL0olA7DurvQHKMq7J3iBTNQ4bL/vQ=
|
||||||
|
github.com/yapingcat/gomedia/codec v0.0.0-20220617074658-94762898dc25 h1:1mq/skGEQGCqxHJPKfontELt/a052Gu236H0bge0Qr0=
|
||||||
|
github.com/yapingcat/gomedia/codec v0.0.0-20220617074658-94762898dc25/go.mod h1:obSECV6X3NPUsLL0olA7DurvQHKMq7J3iBTNQ4bL/vQ=
|
||||||
|
github.com/yapingcat/gomedia/mpeg2 v0.0.0-20220617074658-94762898dc25 h1:51qjqT2jsOESm/jDi0k0AdQX33Sg4vhw8X6eooj7c8A=
|
||||||
|
github.com/yapingcat/gomedia/mpeg2 v0.0.0-20220617074658-94762898dc25/go.mod h1:bvxj2Oi5Rwj7eHm2OjqgOIs8x2T0j+V068eS/SAyZLA=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
|
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||||
|
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
|
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201214095126-aec9a390925b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
|
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||||
|
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
|
||||||
50
magefile.go
Normal file
50
magefile.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//go:build mage
|
||||||
|
// +build mage
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/magefile/mage/sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Default = Build
|
||||||
|
|
||||||
|
func Build() error {
|
||||||
|
path := "bin"
|
||||||
|
if err := os.MkdirAll(path, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := "srs-sip"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
name += ".exe"
|
||||||
|
}
|
||||||
|
name = filepath.Join(path, name)
|
||||||
|
|
||||||
|
if err := sh.Run("go", "build", "-o", name, "main/main.go"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
name = "srs-sip-tools"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
name += ".exe"
|
||||||
|
}
|
||||||
|
name = filepath.Join(path, name)
|
||||||
|
|
||||||
|
if err := sh.Run("go", "build", "-o", name, "tools/main.go"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("build done")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Clean() {
|
||||||
|
os.RemoveAll("bin")
|
||||||
|
fmt.Println("clean done")
|
||||||
|
}
|
||||||
85
main/main.go
Normal file
85
main/main.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ossrs/go-oryx-lib/logger"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/api"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/config"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/service"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WaitTerminationSignal(cancel context.CancelFunc) {
|
||||||
|
sigc := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer signal.Stop(sigc)
|
||||||
|
<-sigc
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
conf := utils.Parse(ctx)
|
||||||
|
sipSvr, err := service.NewService(ctx, conf)
|
||||||
|
if err != nil {
|
||||||
|
logger.Ef("create service failed. err is %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sipSvr.Start(); err != nil {
|
||||||
|
logger.Ef("start sip service failed. err is %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiSvr, err := api.NewHttpApiServer(conf, sipSvr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Ef("create http service failed. err is %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apiSvr.Start()
|
||||||
|
|
||||||
|
var targetDir string
|
||||||
|
targetDirs := []string{"./web/html", "../web/html"}
|
||||||
|
for _, dir := range targetDirs {
|
||||||
|
if _, err := os.Stat(path.Join(dir, "index.html")); err == nil {
|
||||||
|
targetDir = dir
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if targetDir == "" {
|
||||||
|
logger.Ef(ctx, "index.html not found in %v", targetDirs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
c := conf.(*config.MainConfig)
|
||||||
|
httpPort := strconv.Itoa(c.HttpServerPort)
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: ":" + httpPort,
|
||||||
|
Handler: http.FileServer(http.Dir(targetDir)),
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
IdleTimeout: 30 * time.Second,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
logger.Tf(ctx, "http server listen on %s, home is %v", httpPort, targetDir)
|
||||||
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Ef(ctx, "listen on %s failed", httpPort)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger.Tf(ctx, "media server address is %v", conf.(*config.MainConfig).MediaAddr)
|
||||||
|
|
||||||
|
WaitTerminationSignal(cancel)
|
||||||
|
|
||||||
|
sipSvr.Stop()
|
||||||
|
}
|
||||||
137
pkg/api/api-controller.go
Normal file
137
pkg/api/api-controller.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *HttpApiServer) RegisterRoutes(router *mux.Router) {
|
||||||
|
|
||||||
|
apiV1Router := router.PathPrefix("/srs-sip/v1").Subrouter()
|
||||||
|
|
||||||
|
// Add Auth middleware
|
||||||
|
//apiV1Router.Use(authMiddleware)
|
||||||
|
|
||||||
|
apiV1Router.HandleFunc("/devices", h.ApiListDevices).Methods(http.MethodGet)
|
||||||
|
apiV1Router.HandleFunc("/devices/{id}/channels", h.ApiGetChannelByDeviceId).Methods(http.MethodGet)
|
||||||
|
apiV1Router.HandleFunc("/channels", h.ApiGetAllChannels).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
apiV1Router.HandleFunc("/invite", h.ApiInvite).Methods(http.MethodPost)
|
||||||
|
apiV1Router.HandleFunc("/bye", h.ApiBye).Methods(http.MethodPost)
|
||||||
|
apiV1Router.HandleFunc("/ptz", h.ApiPTZControl).Methods(http.MethodPost)
|
||||||
|
|
||||||
|
apiV1Router.HandleFunc("", h.GetAPIRoutes(apiV1Router)).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
router.HandleFunc("/srs-sip", h.ApiGetAPIVersion).Methods(http.MethodGet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpApiServer) RespondWithJSON(w http.ResponseWriter, code int, data interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
wrapper := map[string]interface{}{
|
||||||
|
"code": code,
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(wrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpApiServer) RespondWithJSONSimple(w http.ResponseWriter, jsonStr string) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(jsonStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpApiServer) GetAPIRoutes(router *mux.Router) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var routes []map[string]string
|
||||||
|
|
||||||
|
router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
||||||
|
path, err := route.GetPathTemplate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
methods, err := route.GetMethods()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, method := range methods {
|
||||||
|
routes = append(routes, map[string]string{
|
||||||
|
"method": method,
|
||||||
|
"path": path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
h.RespondWithJSON(w, 0, routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpApiServer) ApiGetAPIVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.RespondWithJSONSimple(w, `{"version": "v1"}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpApiServer) ApiListDevices(w http.ResponseWriter, r *http.Request) {
|
||||||
|
list := service.DM.GetDevices()
|
||||||
|
h.RespondWithJSON(w, 0, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpApiServer) ApiGetChannelByDeviceId(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
channels := service.DM.ApiGetChannelByDeviceId(id)
|
||||||
|
h.RespondWithJSON(w, 0, channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpApiServer) ApiGetAllChannels(w http.ResponseWriter, r *http.Request) {
|
||||||
|
channels := service.DM.GetAllVideoChannels()
|
||||||
|
h.RespondWithJSON(w, 0, channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
// request: {"device_id": "1", "channel_id": "1", "sub_stream": 0}
|
||||||
|
// response: {"code": 0, "data": {"channel_id": "1", "url": "webrtc://"}}
|
||||||
|
func (h *HttpApiServer) ApiInvite(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse request
|
||||||
|
var req map[string]string
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get device and channel
|
||||||
|
deviceID := req["device_id"]
|
||||||
|
channelID := req["channel_id"]
|
||||||
|
//subStream := req["sub_stream"]
|
||||||
|
|
||||||
|
code := 0
|
||||||
|
url := ""
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
data := map[string]string{
|
||||||
|
"channel_id": channelID,
|
||||||
|
"url": url,
|
||||||
|
}
|
||||||
|
h.RespondWithJSON(w, code, data)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := h.sipSvr.Uas.Invite(deviceID, channelID); err != nil {
|
||||||
|
code = http.StatusInternalServerError
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c, ok := h.sipSvr.Uas.GetVideoChannelStatue(channelID)
|
||||||
|
if !ok {
|
||||||
|
code = http.StatusInternalServerError
|
||||||
|
return
|
||||||
|
}
|
||||||
|
url = "webrtc://" + h.conf.MediaAddr + "/live/" + c.Ssrc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpApiServer) ApiBye(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.RespondWithJSONSimple(w, `{"msg":"Not implemented"}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpApiServer) ApiPTZControl(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.RespondWithJSONSimple(w, `{"msg":"Not implemented"}`)
|
||||||
|
}
|
||||||
45
pkg/api/api.go
Normal file
45
pkg/api/api.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ossrs/go-oryx-lib/logger"
|
||||||
|
|
||||||
|
"github.com/gorilla/handlers"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/config"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpApiServer struct {
|
||||||
|
conf *config.MainConfig
|
||||||
|
sipSvr *service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpApiServer(r0 interface{}, svr *service.Service) (*HttpApiServer, error) {
|
||||||
|
return &HttpApiServer{
|
||||||
|
conf: r0.(*config.MainConfig),
|
||||||
|
sipSvr: svr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpApiServer) Start() {
|
||||||
|
router := mux.NewRouter().StrictSlash(true)
|
||||||
|
h.RegisterRoutes(router)
|
||||||
|
|
||||||
|
headers := handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"})
|
||||||
|
methods := handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"})
|
||||||
|
origins := handlers.AllowedOrigins([]string{"*"})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
addr := fmt.Sprintf(":%v", h.conf.APIPort)
|
||||||
|
logger.Tf(ctx, "http api listen on %s", addr)
|
||||||
|
err := http.ListenAndServe(addr, handlers.CORS(headers, methods, origins)(router))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
56
pkg/config/config.go
Normal file
56
pkg/config/config.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MainConfig struct {
|
||||||
|
Serial string `ymal:"serial"`
|
||||||
|
Realm string `ymal:"realm"`
|
||||||
|
SipHost string `ymal:"sip-host"`
|
||||||
|
SipPort int `ymal:"sip-port"`
|
||||||
|
MediaAddr string `ymal:"media-addr"`
|
||||||
|
HttpServerPort int `ymal:"http-server-port"`
|
||||||
|
APIPort int `ymal:"api-port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLocalIP() (string, error) {
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
type Iface struct {
|
||||||
|
Name string
|
||||||
|
Addr net.IP
|
||||||
|
}
|
||||||
|
var candidates []Iface
|
||||||
|
for _, ifc := range ifaces {
|
||||||
|
if ifc.Flags&net.FlagUp == 0 || ifc.Flags&net.FlagUp == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ifc.Flags&(net.FlagPointToPoint|net.FlagLoopback) != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addrs, err := ifc.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ipnet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ip4 := ipnet.IP.To4(); ip4 != nil {
|
||||||
|
candidates = append(candidates, Iface{
|
||||||
|
Name: ifc.Name, Addr: ip4,
|
||||||
|
})
|
||||||
|
//logger.Tf("considering interface", "iface", ifc.Name, "ip", ip4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return "", fmt.Errorf("No local IP found")
|
||||||
|
}
|
||||||
|
return candidates[0].Addr.String(), nil
|
||||||
|
}
|
||||||
17
pkg/service/cascade.go
Normal file
17
pkg/service/cascade.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/emiago/sipgo"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cascade struct {
|
||||||
|
ua *sipgo.UserAgent
|
||||||
|
sipCli *sipgo.Client
|
||||||
|
sipSvr *sipgo.Server
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
conf *config.MainConfig
|
||||||
|
}
|
||||||
166
pkg/service/device.go
Normal file
166
pkg/service/device.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ossrs/srs-sip/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// <Item>
|
||||||
|
// <DeviceID>34020000001320000002</DeviceID>
|
||||||
|
// <Name>209</Name>
|
||||||
|
// <Manufacturer>UNIVIEW</Manufacturer>
|
||||||
|
// <Model>HIC6622-IR@X33-VF</Model>
|
||||||
|
// <Owner>IPC-B2202.7.11.230222</Owner>
|
||||||
|
// <CivilCode>CivilCode</CivilCode>
|
||||||
|
// <Address>Address</Address>
|
||||||
|
// <Parental>1</Parental>
|
||||||
|
// <ParentID>75015310072008100002</ParentID>
|
||||||
|
// <SafetyWay>0</SafetyWay>
|
||||||
|
// <RegisterWay>1</RegisterWay>
|
||||||
|
// <Secrecy>0</Secrecy>
|
||||||
|
// <Status>ON</Status>
|
||||||
|
// <Longitude>0.0000000</Longitude>
|
||||||
|
// <Latitude>0.0000000</Latitude>
|
||||||
|
// <Info>
|
||||||
|
// <PTZType>1</PTZType>
|
||||||
|
// <Resolution>6/4/2</Resolution>
|
||||||
|
// <DownloadSpeed>0</DownloadSpeed>
|
||||||
|
// </Info>
|
||||||
|
// </Item>
|
||||||
|
|
||||||
|
type ChannelInfo struct {
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
ParentID string `json:"parent_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Manufacturer string `json:"manufacturer"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
CivilCode string `json:"civil_code"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Parental int `json:"parental"`
|
||||||
|
SafetyWay int `json:"safety_way"`
|
||||||
|
RegisterWay int `json:"register_way"`
|
||||||
|
Secrecy int `json:"secrecy"`
|
||||||
|
IPAddress string `json:"ip_address"`
|
||||||
|
Status ChannelStatus `json:"status"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Info struct {
|
||||||
|
PTZType int `json:"ptz_type"`
|
||||||
|
Resolution string `json:"resolution"`
|
||||||
|
DownloadSpeed string `json:"download_speed"` // 1/2/4/8
|
||||||
|
} `json:"info"`
|
||||||
|
|
||||||
|
// custom fields
|
||||||
|
Ssrc string `json:"ssrc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelStatus string
|
||||||
|
|
||||||
|
type DeviceInfo struct {
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
SourceAddr string `json:"source_addr"`
|
||||||
|
NetworkType string `json:"network_type"`
|
||||||
|
ChannelMap sync.Map `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type deviceManager struct {
|
||||||
|
devices sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
var instance *deviceManager
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
func GetDeviceManager() *deviceManager {
|
||||||
|
once.Do(func() {
|
||||||
|
instance = &deviceManager{
|
||||||
|
devices: sync.Map{},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *deviceManager) AddDevice(id string, info *DeviceInfo) {
|
||||||
|
dm.devices.Store(id, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *deviceManager) RemoveDevice(id string) {
|
||||||
|
dm.devices.Delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *deviceManager) GetDevices() []*DeviceInfo {
|
||||||
|
list := make([]*DeviceInfo, 0)
|
||||||
|
dm.devices.Range(func(key, value interface{}) bool {
|
||||||
|
list = append(list, value.(*DeviceInfo))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *deviceManager) GetDevice(id string) (*DeviceInfo, bool) {
|
||||||
|
v, ok := dm.devices.Load(id)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return v.(*DeviceInfo), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *deviceManager) UpdateChannels(deviceID string, list ...ChannelInfo) {
|
||||||
|
device, ok := dm.GetDevice(deviceID)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, channel := range list {
|
||||||
|
device.ChannelMap.Store(channel.DeviceID, channel)
|
||||||
|
}
|
||||||
|
dm.devices.Store(deviceID, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *deviceManager) ApiGetChannelByDeviceId(deviceID string) []ChannelInfo {
|
||||||
|
device, ok := dm.GetDevice(deviceID)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
channels := make([]ChannelInfo, 0)
|
||||||
|
device.ChannelMap.Range(func(key, value interface{}) bool {
|
||||||
|
channels = append(channels, value.(ChannelInfo))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *deviceManager) GetAllVideoChannels() []ChannelInfo {
|
||||||
|
channels := make([]ChannelInfo, 0)
|
||||||
|
dm.devices.Range(func(key, value interface{}) bool {
|
||||||
|
device := value.(*DeviceInfo)
|
||||||
|
device.ChannelMap.Range(func(key, value interface{}) bool {
|
||||||
|
if utils.IsVideoChannel(value.(ChannelInfo).DeviceID) {
|
||||||
|
channels = append(channels, value.(ChannelInfo))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *deviceManager) GetDeviceInfoByChannel(channelID string) (*DeviceInfo, bool) {
|
||||||
|
var device *DeviceInfo
|
||||||
|
found := false
|
||||||
|
dm.devices.Range(func(key, value interface{}) bool {
|
||||||
|
d := value.(*DeviceInfo)
|
||||||
|
_, ok := d.ChannelMap.Load(channelID)
|
||||||
|
if ok {
|
||||||
|
device = d
|
||||||
|
found = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return device, found
|
||||||
|
}
|
||||||
153
pkg/service/inbound.go
Normal file
153
pkg/service/inbound.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/emiago/sipgo/sip"
|
||||||
|
"github.com/ossrs/go-oryx-lib/logger"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/service/stack"
|
||||||
|
"golang.org/x/net/html/charset"
|
||||||
|
)
|
||||||
|
|
||||||
|
const GB28181_ID_LENGTH = 20
|
||||||
|
|
||||||
|
type VideoChannelStatus struct {
|
||||||
|
ID string
|
||||||
|
ParentID string
|
||||||
|
MediaHost string
|
||||||
|
MediaPort int
|
||||||
|
Ssrc string
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) onRegister(req *sip.Request, tx sip.ServerTransaction) {
|
||||||
|
id := req.From().Address.User
|
||||||
|
if len(id) != GB28181_ID_LENGTH {
|
||||||
|
logger.E(s.ctx, "invalid device ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isUnregister := false
|
||||||
|
if exps := req.GetHeaders("Expires"); len(exps) > 0 {
|
||||||
|
exp := exps[0]
|
||||||
|
expSec, err := strconv.ParseInt(exp.Value(), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
logger.Ef(s.ctx, "parse expires header error: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if expSec == 0 {
|
||||||
|
isUnregister = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.E(s.ctx, "empty expires header")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isUnregister {
|
||||||
|
DM.RemoveDevice(id)
|
||||||
|
logger.Wf(s.ctx, "Device %s unregistered", id)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if d, ok := DM.GetDevice(id); !ok {
|
||||||
|
DM.AddDevice(id, &DeviceInfo{
|
||||||
|
DeviceID: id,
|
||||||
|
SourceAddr: req.Source(),
|
||||||
|
NetworkType: req.Transport(),
|
||||||
|
})
|
||||||
|
s.respondRegister(req, http.StatusOK, "OK", tx)
|
||||||
|
logger.Tf(s.ctx, "%s Register success, source:%s, req: %s", id, req.Source(), req.String())
|
||||||
|
|
||||||
|
go s.Catalog(id)
|
||||||
|
} else {
|
||||||
|
if d.SourceAddr != req.Source() {
|
||||||
|
logger.Ef(s.ctx, "Device %s[%s] already registered, %s is NOT allowed.", id, d.SourceAddr, req.Source())
|
||||||
|
// TODO: 国标没有明确定义重复ID注册的处理方式,这里暂时返回冲突
|
||||||
|
s.respondRegister(req, http.StatusConflict, "Conflict Device ID", tx)
|
||||||
|
} else {
|
||||||
|
// TODO: 刷新DM里面的设备信息
|
||||||
|
s.respondRegister(req, http.StatusOK, "OK", tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) respondRegister(req *sip.Request, code sip.StatusCode, reason string, tx sip.ServerTransaction) {
|
||||||
|
res := stack.NewRegisterResponse(req, code, reason)
|
||||||
|
_ = tx.Respond(res)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) onMessage(req *sip.Request, tx sip.ServerTransaction) {
|
||||||
|
id := req.From().Address.User
|
||||||
|
if len(id) != 20 {
|
||||||
|
logger.Ef(s.ctx, "invalid device ID %s", req.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
//logger.Tf(s.ctx, "Received MESSAGE: %s", req.String())
|
||||||
|
|
||||||
|
temp := &struct {
|
||||||
|
XMLName xml.Name
|
||||||
|
CmdType string
|
||||||
|
SN int // 请求序列号,一般用于对应 request 和 response
|
||||||
|
DeviceID string
|
||||||
|
DeviceName string
|
||||||
|
Manufacturer string
|
||||||
|
Model string
|
||||||
|
Channel string
|
||||||
|
DeviceList []ChannelInfo `xml:"DeviceList>Item"`
|
||||||
|
// RecordList []*Record `xml:"RecordList>Item"`
|
||||||
|
// SumNum int
|
||||||
|
}{}
|
||||||
|
decoder := xml.NewDecoder(bytes.NewReader([]byte(req.Body())))
|
||||||
|
decoder.CharsetReader = charset.NewReaderLabel
|
||||||
|
if err := decoder.Decode(temp); err != nil {
|
||||||
|
logger.Ef(s.ctx, "decode message error: %s\n message:%s", err.Error(), req.Body())
|
||||||
|
}
|
||||||
|
var body string
|
||||||
|
switch temp.CmdType {
|
||||||
|
case "Keepalive":
|
||||||
|
logger.T(s.ctx, "Keepalive")
|
||||||
|
if _, ok := DM.GetDevice(temp.DeviceID); !ok {
|
||||||
|
// unregister device
|
||||||
|
tx.Respond(sip.NewResponseFromRequest(req, http.StatusBadRequest, "", nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "SensorCatalog": // 兼容宇视,非国标
|
||||||
|
case "Catalog":
|
||||||
|
logger.T(s.ctx, "Catalog")
|
||||||
|
DM.UpdateChannels(temp.DeviceID, temp.DeviceList...)
|
||||||
|
//go s.AutoInvite(temp.DeviceID, temp.DeviceList...)
|
||||||
|
case "Alarm":
|
||||||
|
logger.T(s.ctx, "Alarm")
|
||||||
|
default:
|
||||||
|
logger.Wf(s.ctx, "Not supported CmdType: %s", temp.CmdType)
|
||||||
|
response := sip.NewResponseFromRequest(req, http.StatusBadRequest, "", nil)
|
||||||
|
tx.Respond(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", []byte(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) onNotify(req *sip.Request, tx sip.ServerTransaction) {
|
||||||
|
logger.T(s.ctx, "Received NOTIFY request")
|
||||||
|
tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) AddVideoChannelStatue(channelID string, status VideoChannelStatus) {
|
||||||
|
s.channelsStatue.Store(channelID, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) GetVideoChannelStatue(channelID string) (VideoChannelStatus, bool) {
|
||||||
|
v, ok := s.channelsStatue.Load(channelID)
|
||||||
|
if !ok {
|
||||||
|
return VideoChannelStatus{}, false
|
||||||
|
}
|
||||||
|
return v.(VideoChannelStatus), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) RemoveVideoChannelStatue(channelID string) {
|
||||||
|
s.channelsStatue.Delete(channelID)
|
||||||
|
}
|
||||||
169
pkg/service/outbound.go
Normal file
169
pkg/service/outbound.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emiago/sipgo/sip"
|
||||||
|
"github.com/ossrs/go-oryx-lib/errors"
|
||||||
|
"github.com/ossrs/go-oryx-lib/logger"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/service/stack"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *UAS) AutoInvite(deviceID string, list ...ChannelInfo) {
|
||||||
|
for _, c := range list {
|
||||||
|
if c.Status == "ON" && utils.IsVideoChannel(c.DeviceID) {
|
||||||
|
if err := s.Invite(deviceID, c.DeviceID); err != nil {
|
||||||
|
logger.Ef(s.ctx, "invite error: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) Invite(deviceID, channelID string) error {
|
||||||
|
if s.isPublishing(channelID) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ssrc := utils.CreateSSRC(true)
|
||||||
|
|
||||||
|
mediaPort, err := s.signal.Publish(ssrc, ssrc)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "api gb publish request error")
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaHost := strings.Split(s.conf.MediaAddr, ":")[0]
|
||||||
|
if mediaHost == "" {
|
||||||
|
return errors.Errorf("media host is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
sdpInfo := []string{
|
||||||
|
"v=0",
|
||||||
|
fmt.Sprintf("o=%s 0 0 IN IP4 %s", channelID, mediaHost),
|
||||||
|
"s=" + "Play",
|
||||||
|
"u=" + channelID + ":0",
|
||||||
|
"c=IN IP4 " + mediaHost,
|
||||||
|
"t=0 0", // start time and end time
|
||||||
|
fmt.Sprintf("m=video %d TCP/RTP/AVP 96", mediaPort),
|
||||||
|
"a=recvonly",
|
||||||
|
"a=rtpmap:96 PS/90000",
|
||||||
|
"y=" + ssrc,
|
||||||
|
"\r\n",
|
||||||
|
}
|
||||||
|
if true { // support tcp only
|
||||||
|
sdpInfo = append(sdpInfo, "a=setup:passive", "a=connection:new")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 需要考虑不同设备,通道ID相同的情况
|
||||||
|
d, ok := DM.GetDeviceInfoByChannel(channelID)
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("device not found by %s", channelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := fmt.Sprintf("%s:%s,%s:0", channelID, ssrc, s.conf.Serial)
|
||||||
|
|
||||||
|
req, err := stack.NewInviteRequest([]byte(strings.Join(sdpInfo, "\r\n")), subject, stack.OutboundConfig{
|
||||||
|
Via: d.SourceAddr,
|
||||||
|
To: d.DeviceID,
|
||||||
|
From: s.conf.Serial,
|
||||||
|
Transport: d.NetworkType,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "build invite request error")
|
||||||
|
}
|
||||||
|
tx, err := s.sipCli.TransactionRequest(s.ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "transaction request error")
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.waitAnswer(tx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "wait answer error")
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return errors.Errorf("invite response error: %s", res.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
ack := sip.NewAckRequest(req, res, nil)
|
||||||
|
s.sipCli.WriteRequest(ack)
|
||||||
|
|
||||||
|
s.AddVideoChannelStatue(channelID, VideoChannelStatus{
|
||||||
|
ID: channelID,
|
||||||
|
ParentID: deviceID,
|
||||||
|
MediaHost: mediaHost,
|
||||||
|
MediaPort: mediaPort,
|
||||||
|
Ssrc: ssrc,
|
||||||
|
Status: "ON",
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) isPublishing(channelID string) bool {
|
||||||
|
c, err := s.GetVideoChannelStatue(channelID)
|
||||||
|
if !err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if p, err := s.signal.GetStreamStatus(c.Ssrc); err != nil || !p {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) Bye() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) Catalog(deviceID string) error {
|
||||||
|
var CatalogXML = `<?xml version="1.0"?><Query>
|
||||||
|
<CmdType>Catalog</CmdType>
|
||||||
|
<SN>%d</SN>
|
||||||
|
<DeviceID>%s</DeviceID>
|
||||||
|
</Query>
|
||||||
|
`
|
||||||
|
|
||||||
|
d, ok := DM.GetDevice(deviceID)
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("device %s not found", deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := fmt.Sprintf(CatalogXML, s.getSN(), deviceID)
|
||||||
|
|
||||||
|
req, err := stack.NewCatelogRequest([]byte(body), stack.OutboundConfig{
|
||||||
|
Via: d.SourceAddr,
|
||||||
|
To: d.DeviceID,
|
||||||
|
From: s.conf.Serial,
|
||||||
|
Transport: d.NetworkType,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "build catalog request error")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.sipCli.TransactionRequest(s.ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "transaction request error")
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.waitAnswer(tx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "wait answer error")
|
||||||
|
}
|
||||||
|
logger.Tf(s.ctx, "catalog response: %s", res.String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) waitAnswer(tx sip.ClientTransaction) (*sip.Response, error) {
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return nil, errors.Errorf("context done")
|
||||||
|
case res := <-tx.Responses():
|
||||||
|
if res.StatusCode == 100 || res.StatusCode == 101 || res.StatusCode == 180 || res.StatusCode == 183 {
|
||||||
|
return s.waitAnswer(tx)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
52
pkg/service/service.go
Normal file
52
pkg/service/service.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/emiago/sipgo"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/config"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
ctx context.Context
|
||||||
|
conf *config.MainConfig
|
||||||
|
Uac *UAC
|
||||||
|
Uas *UAS
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(ctx context.Context, r0 interface{}) (*Service, error) {
|
||||||
|
s := &Service{
|
||||||
|
ctx: ctx,
|
||||||
|
conf: r0.(*config.MainConfig),
|
||||||
|
}
|
||||||
|
s.Uac = NewUac()
|
||||||
|
s.Uas = NewUas()
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Start() error {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||||
|
|
||||||
|
ua, err := sipgo.NewUA(
|
||||||
|
sipgo.WithUserAgent(UserAgent),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Uas.Start(ua, s.conf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if err := s.Uac.Start(ua, s.conf); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Stop() {
|
||||||
|
s.Uac.Stop()
|
||||||
|
s.Uas.Stop()
|
||||||
|
}
|
||||||
68
pkg/service/stack/request.go
Normal file
68
pkg/service/stack/request.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package stack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emiago/sipgo/sip"
|
||||||
|
"github.com/ossrs/go-oryx-lib/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OutboundConfig struct {
|
||||||
|
Transport string
|
||||||
|
Via string
|
||||||
|
From string
|
||||||
|
To string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequest(method sip.RequestMethod, body []byte, conf OutboundConfig) (*sip.Request, error) {
|
||||||
|
if len(conf.From) != 20 || len(conf.To) != 20 {
|
||||||
|
return nil, errors.Errorf("From or To length is not 20")
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := conf.Via
|
||||||
|
to := sip.Uri{User: conf.To, Host: conf.To[:10]}
|
||||||
|
from := &sip.Uri{User: conf.From, Host: conf.From[:10]}
|
||||||
|
|
||||||
|
fromHeader := &sip.FromHeader{Address: *from, Params: sip.NewParams()}
|
||||||
|
fromHeader.Params.Add("tag", sip.GenerateTagN(16))
|
||||||
|
|
||||||
|
req := sip.NewRequest(method, to)
|
||||||
|
req.AppendHeader(fromHeader)
|
||||||
|
req.AppendHeader(&sip.ToHeader{Address: to})
|
||||||
|
req.AppendHeader(&sip.ContactHeader{Address: *from})
|
||||||
|
req.AppendHeader(sip.NewHeader("Max-Forwards", "70"))
|
||||||
|
req.SetBody(body)
|
||||||
|
req.SetDestination(dest)
|
||||||
|
req.SetTransport(conf.Transport)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegisterRequest(conf OutboundConfig) (*sip.Request, error) {
|
||||||
|
req, err := newRequest(sip.REGISTER, nil, conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.AppendHeader(sip.NewHeader("Expires", "3600"))
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInviteRequest(body []byte, subject string, conf OutboundConfig) (*sip.Request, error) {
|
||||||
|
req, err := newRequest(sip.INVITE, body, conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.AppendHeader(sip.NewHeader("Content-Type", "application/sdp"))
|
||||||
|
req.AppendHeader(sip.NewHeader("Subject", subject))
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCatelogRequest(body []byte, conf OutboundConfig) (*sip.Request, error) {
|
||||||
|
req, err := newRequest(sip.MESSAGE, body, conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.AppendHeader(sip.NewHeader("Content-Type", "Application/MANSCDP+xml"))
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
25
pkg/service/stack/response.go
Normal file
25
pkg/service/stack/response.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package stack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emiago/sipgo/sip"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TIME_LAYOUT = "2024-01-01T00:00:00"
|
||||||
|
const EXPIRES_TIME = 3600
|
||||||
|
|
||||||
|
func NewRegisterResponse(req *sip.Request, code sip.StatusCode, reason string) *sip.Response {
|
||||||
|
resp := sip.NewResponseFromRequest(req, code, reason, nil)
|
||||||
|
|
||||||
|
newTo := &sip.ToHeader{Address: resp.To().Address, Params: sip.NewParams()}
|
||||||
|
newTo.Params.Add("tag", sip.GenerateTagN(10))
|
||||||
|
|
||||||
|
resp.ReplaceHeader(newTo)
|
||||||
|
resp.RemoveHeader("Allow")
|
||||||
|
expires := sip.ExpiresHeader(EXPIRES_TIME)
|
||||||
|
resp.AppendHeader(&expires)
|
||||||
|
resp.AppendHeader(sip.NewHeader("Date", time.Now().Format(TIME_LAYOUT)))
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
122
pkg/service/uac.go
Normal file
122
pkg/service/uac.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emiago/sipgo"
|
||||||
|
"github.com/emiago/sipgo/sip"
|
||||||
|
"github.com/ossrs/go-oryx-lib/errors"
|
||||||
|
"github.com/ossrs/go-oryx-lib/logger"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/config"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/service/stack"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserAgent = "SRS-SIP/1.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UAC struct {
|
||||||
|
*Cascade
|
||||||
|
|
||||||
|
SN uint32
|
||||||
|
LocalIP string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUac() *UAC {
|
||||||
|
ip, err := config.GetLocalIP()
|
||||||
|
if err != nil {
|
||||||
|
logger.E("get local ip failed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &UAC{
|
||||||
|
Cascade: &Cascade{},
|
||||||
|
LocalIP: ip,
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UAC) Start(agent *sipgo.UserAgent, r0 interface{}) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
c.ctx = context.Background()
|
||||||
|
c.conf = r0.(*config.MainConfig)
|
||||||
|
|
||||||
|
if agent == nil {
|
||||||
|
ua, err := sipgo.NewUA(sipgo.WithUserAgent(UserAgent))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
agent = ua
|
||||||
|
}
|
||||||
|
|
||||||
|
c.sipCli, err = sipgo.NewClient(agent, sipgo.WithClientHostname(c.LocalIP))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.sipSvr, err = sipgo.NewServer(agent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.sipSvr.OnInvite(c.onInvite)
|
||||||
|
c.sipSvr.OnBye(c.onBye)
|
||||||
|
c.sipSvr.OnMessage(c.onMessage)
|
||||||
|
|
||||||
|
go c.doRegister()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UAC) Stop() {
|
||||||
|
// TODO: 断开所有当前连接
|
||||||
|
c.sipCli.Close()
|
||||||
|
c.sipSvr.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UAC) doRegister() error {
|
||||||
|
r, _ := stack.NewRegisterRequest(stack.OutboundConfig{
|
||||||
|
From: "34020000001110000001",
|
||||||
|
To: "34020000002000000001",
|
||||||
|
Transport: "UDP",
|
||||||
|
Via: fmt.Sprintf("%s:%d", c.LocalIP, c.conf.SipPort),
|
||||||
|
})
|
||||||
|
tx, err := c.sipCli.TransactionRequest(c.ctx, r)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "transaction request error")
|
||||||
|
}
|
||||||
|
|
||||||
|
rs, _ := c.getResponse(tx)
|
||||||
|
logger.Tf(c.ctx, "register response: %s", rs.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UAC) OnRequest(req *sip.Request, tx sip.ServerTransaction) {
|
||||||
|
switch req.Method {
|
||||||
|
case "INVITE":
|
||||||
|
c.onInvite(req, tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UAC) onInvite(req *sip.Request, tx sip.ServerTransaction) {
|
||||||
|
logger.T(c.ctx, "onInvite")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UAC) onBye(req *sip.Request, tx sip.ServerTransaction) {
|
||||||
|
logger.T(c.ctx, "onBye")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UAC) onMessage(req *sip.Request, tx sip.ServerTransaction) {
|
||||||
|
logger.Tf(c.ctx, "onMessage %s", req.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UAC) getResponse(tx sip.ClientTransaction) (*sip.Response, error) {
|
||||||
|
select {
|
||||||
|
case <-tx.Done():
|
||||||
|
return nil, fmt.Errorf("transaction died")
|
||||||
|
case res := <-tx.Responses():
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
137
pkg/service/uas.go
Normal file
137
pkg/service/uas.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/emiago/sipgo"
|
||||||
|
"github.com/emiago/sipgo/sip"
|
||||||
|
"github.com/ossrs/go-oryx-lib/logger"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/config"
|
||||||
|
"github.com/ossrs/srs-sip/pkg/signaling"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UAS struct {
|
||||||
|
*Cascade
|
||||||
|
|
||||||
|
SN uint32
|
||||||
|
channelsStatue sync.Map
|
||||||
|
signal signaling.ISignaling
|
||||||
|
|
||||||
|
sipConnUDP *net.UDPConn
|
||||||
|
sipConnTCP *net.TCPListener
|
||||||
|
}
|
||||||
|
|
||||||
|
var DM = GetDeviceManager()
|
||||||
|
|
||||||
|
func NewUas() *UAS {
|
||||||
|
return &UAS{
|
||||||
|
Cascade: &Cascade{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) Start(agent *sipgo.UserAgent, r0 interface{}) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
conf := r0.(*config.MainConfig)
|
||||||
|
sig := &signaling.Srs{
|
||||||
|
Ctx: ctx,
|
||||||
|
Addr: "http://" + conf.MediaAddr,
|
||||||
|
}
|
||||||
|
s.signal = sig
|
||||||
|
s.startSipServer(agent, ctx, r0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) Stop() {
|
||||||
|
s.sipCli.Close()
|
||||||
|
s.sipSvr.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) startSipServer(agent *sipgo.UserAgent, ctx context.Context, r0 interface{}) error {
|
||||||
|
conf := r0.(*config.MainConfig)
|
||||||
|
s.ctx = ctx
|
||||||
|
s.conf = conf
|
||||||
|
|
||||||
|
if agent == nil {
|
||||||
|
ua, err := sipgo.NewUA(sipgo.WithUserAgent(UserAgent))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
agent = ua
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := sipgo.NewClient(agent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.sipCli = cli
|
||||||
|
|
||||||
|
svr, err := sipgo.NewServer(agent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.sipSvr = svr
|
||||||
|
|
||||||
|
s.sipSvr.OnRegister(s.onRegister)
|
||||||
|
s.sipSvr.OnMessage(s.onMessage)
|
||||||
|
s.sipSvr.OnNotify(s.onNotify)
|
||||||
|
|
||||||
|
if err := s.startUDP(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.startTCP(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) startUDP() error {
|
||||||
|
lis, err := net.ListenUDP("udp", &net.UDPAddr{
|
||||||
|
IP: net.IPv4(0, 0, 0, 0),
|
||||||
|
Port: s.conf.SipPort,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot listen on the UDP signaling port %d: %w", s.conf.SipPort, err)
|
||||||
|
}
|
||||||
|
s.sipConnUDP = lis
|
||||||
|
logger.Tf(s.ctx, "sip signaling listening on UDP %s:%d", lis.LocalAddr().String(), s.conf.SipPort)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := s.sipSvr.ServeUDP(lis); err != nil {
|
||||||
|
panic(fmt.Errorf("SIP listen UDP error: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) startTCP() error {
|
||||||
|
lis, err := net.ListenTCP("tcp", &net.TCPAddr{
|
||||||
|
IP: net.IPv4(0, 0, 0, 0),
|
||||||
|
Port: s.conf.SipPort,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot listen on the TCP signaling port %d: %w", s.conf.SipPort, err)
|
||||||
|
}
|
||||||
|
s.sipConnTCP = lis
|
||||||
|
logger.Tf(s.ctx, "sip signaling listening on TCP %s:%d", lis.Addr().String(), s.conf.SipPort)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := s.sipSvr.ServeTCP(lis); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||||
|
panic(fmt.Errorf("SIP listen TCP error: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sipErrorResponse(tx sip.ServerTransaction, req *sip.Request) {
|
||||||
|
_ = tx.Respond(sip.NewResponseFromRequest(req, 400, "", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UAS) getSN() uint32 {
|
||||||
|
s.SN++
|
||||||
|
return s.SN
|
||||||
|
}
|
||||||
75
pkg/signaling/signaling.go
Normal file
75
pkg/signaling/signaling.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package signaling
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ossrs/go-oryx-lib/errors"
|
||||||
|
"github.com/ossrs/go-oryx-lib/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ISignaling interface {
|
||||||
|
Publish(id, ssrc string) (int, error)
|
||||||
|
Unpublish(id string) error
|
||||||
|
GetStreamStatus(id string) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The r is HTTP API to request, like "http://localhost:1985/gb/v1/publish".
|
||||||
|
// The req is the HTTP request body, will be marshal to JSON object. nil is no body
|
||||||
|
// The res is the HTTP response body, already unmarshal to JSON object.
|
||||||
|
func apiRequest(ctx context.Context, r string, req interface{}, res interface{}) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if req != nil {
|
||||||
|
if err := json.NewEncoder(&buf).Encode(req); err != nil {
|
||||||
|
return errors.Wrapf(err, "Marshal body %v", req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Tf(ctx, "Request url api=%v with %v bytes", r, buf.Len())
|
||||||
|
|
||||||
|
method := "POST"
|
||||||
|
if req == nil {
|
||||||
|
method = "GET"
|
||||||
|
}
|
||||||
|
reqObj, err := http.NewRequest(method, r, &buf)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "HTTP request %v", buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resObj, err := client.Do(reqObj.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Do HTTP request %v", buf.String())
|
||||||
|
}
|
||||||
|
defer resObj.Body.Close()
|
||||||
|
|
||||||
|
if resObj.StatusCode != http.StatusOK {
|
||||||
|
return errors.Errorf("Server returned status code=%v", resObj.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
b2, err := io.ReadAll(resObj.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Read response for %v", buf.String())
|
||||||
|
}
|
||||||
|
logger.Tf(ctx, "Response from %v is %v bytes", r, len(b2))
|
||||||
|
|
||||||
|
errorCode := struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
}{}
|
||||||
|
if err := json.Unmarshal(b2, &errorCode); err != nil {
|
||||||
|
return errors.Wrapf(err, "Unmarshal %v", string(b2))
|
||||||
|
}
|
||||||
|
if errorCode.Code != 0 {
|
||||||
|
return errors.Errorf("Server fail code=%v %v", errorCode.Code, string(b2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b2, res); err != nil {
|
||||||
|
return errors.Wrapf(err, "Unmarshal %v", string(b2))
|
||||||
|
}
|
||||||
|
logger.Tf(ctx, "Parse response to code=%v ok, %v", errorCode.Code, res)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
95
pkg/signaling/srs.go
Normal file
95
pkg/signaling/srs.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package signaling
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/ossrs/go-oryx-lib/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Srs struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Addr string // The address of SRS, eg: http://localhost:1985
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Srs) Publish(id, ssrc string) (int, error) {
|
||||||
|
req := struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
SSRC string `json:"ssrc"`
|
||||||
|
}{
|
||||||
|
id, ssrc,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := apiRequest(s.Ctx, s.Addr+"/gb/v1/publish/", req, &res); err != nil {
|
||||||
|
return 0, errors.Wrapf(err, "gb/v1/publish")
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Port, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Srs) Unpublish(id string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// {
|
||||||
|
// "code": 0,
|
||||||
|
// "server": "vid-y19n6nm",
|
||||||
|
// "service": "382k456r",
|
||||||
|
// "pid": "9495",
|
||||||
|
// "streams": [{
|
||||||
|
// "id": "vid-9y0ozy0",
|
||||||
|
// "name": "0551954854",
|
||||||
|
// "vhost": "vid-v2ws53u",
|
||||||
|
// "app": "live",
|
||||||
|
// "tcUrl": "webrtc://127.0.0.1:1985/live",
|
||||||
|
// "url": "/live/0551954854",
|
||||||
|
// "live_ms": 1720428680003,
|
||||||
|
// "clients": 1,
|
||||||
|
// "frames": 8431,
|
||||||
|
// "send_bytes": 66463941,
|
||||||
|
// "recv_bytes": 89323998,
|
||||||
|
// "kbps": {
|
||||||
|
// "recv_30s": 0,
|
||||||
|
// "send_30s": 0
|
||||||
|
// },
|
||||||
|
// "publish": {
|
||||||
|
// "active": false,
|
||||||
|
// "cid": "b3op069g"
|
||||||
|
// },
|
||||||
|
// "video": null,
|
||||||
|
// "audio": null
|
||||||
|
// }]
|
||||||
|
// }
|
||||||
|
func (s *Srs) GetStreamStatus(id string) (bool, error) {
|
||||||
|
type Stream struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Publish struct {
|
||||||
|
Active bool `json:"active"`
|
||||||
|
Cid string `json:"cid"`
|
||||||
|
} `json:"publish"`
|
||||||
|
}
|
||||||
|
res := struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Streams []Stream `json:"streams"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := apiRequest(s.Ctx, s.Addr+"/api/v1/streams?count=99", nil, &res); err != nil {
|
||||||
|
return false, errors.Wrapf(err, "api/v1/stream")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.Streams) == 0 {
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
for _, v := range res.Streams {
|
||||||
|
if v.Name == id {
|
||||||
|
return v.Publish.Active, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
66
pkg/utils/utils.go
Normal file
66
pkg/utils/utils.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"flag"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/ossrs/srs-sip/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Parse(ctx context.Context) interface{} {
|
||||||
|
fl := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||||
|
|
||||||
|
var conf config.MainConfig
|
||||||
|
fl.StringVar(&conf.Serial, "serial", "34020000002000000001", "The serial number")
|
||||||
|
fl.StringVar(&conf.Realm, "realm", "3402000000", "The realm")
|
||||||
|
fl.StringVar(&conf.SipHost, "sip-host", "0.0.0.0", "The SIP host")
|
||||||
|
fl.IntVar(&conf.SipPort, "sip-port", 5060, "The SIP port")
|
||||||
|
fl.StringVar(&conf.MediaAddr, "media-addr", "127.0.0.1:1985", "The api address of media server. like: 127.0.0.1:1985")
|
||||||
|
fl.IntVar(&conf.HttpServerPort, "http-server-port", 8888, "The port of http server")
|
||||||
|
fl.IntVar(&conf.APIPort, "api-port", 2020, "The port of http api server")
|
||||||
|
|
||||||
|
fl.Usage = func() {
|
||||||
|
fl.PrintDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fl.Parse(os.Args[1:]); err == flag.ErrHelp {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
showHelp := conf.MediaAddr == ""
|
||||||
|
if showHelp {
|
||||||
|
fl.Usage()
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &conf
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenRandomNumber(n int) string {
|
||||||
|
var result string
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
randomDigit, _ := rand.Int(rand.Reader, big.NewInt(10))
|
||||||
|
result += randomDigit.String()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateSSRC(isLive bool) string {
|
||||||
|
ssrc := make([]byte, 10)
|
||||||
|
if isLive {
|
||||||
|
ssrc[0] = '0'
|
||||||
|
} else {
|
||||||
|
ssrc[0] = '1'
|
||||||
|
}
|
||||||
|
copy(ssrc[1:], GenRandomNumber(9))
|
||||||
|
return string(ssrc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see GB/T28181—2016 附录D 统一编码规则
|
||||||
|
func IsVideoChannel(channelID string) bool {
|
||||||
|
deviceType := channelID[10:13]
|
||||||
|
return deviceType == "131" || deviceType == "132"
|
||||||
|
}
|
||||||
30
tools/main.go
Normal file
30
tools/main.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/ossrs/go-oryx-lib/logger"
|
||||||
|
"github.com/ossrs/srs-bench/gb28181"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var conf interface{}
|
||||||
|
conf = gb28181.Parse(ctx)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
go func() {
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
for sig := range sigs {
|
||||||
|
logger.Wf(ctx, "Quit for signal %v", sig)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
gb28181.Run(ctx, conf)
|
||||||
|
}
|
||||||
9
web/html/css/bootstrap.min.css
vendored
Normal file
9
web/html/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
web/html/img/tooltip.png
Normal file
BIN
web/html/img/tooltip.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 783 B |
865
web/html/index.html
Normal file
865
web/html/index.html
Normal file
@ -0,0 +1,865 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>DEMO</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css"/>
|
||||||
|
<style>
|
||||||
|
body{
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
#my_modal_footer {
|
||||||
|
margin-top: -20px;
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
#main_modal {
|
||||||
|
margin-top: -60px;
|
||||||
|
}
|
||||||
|
#rtc_player_modal {
|
||||||
|
margin-top: -60px;
|
||||||
|
}
|
||||||
|
.div_play_time {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
#pb_buffer_bg {
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img src=' '/>
|
||||||
|
<div class="navbar navbar-fixed-top">
|
||||||
|
<div class="navbar-inner">
|
||||||
|
<div class="container">
|
||||||
|
<a id="srs_index" class="brand" href="#">DEMO</a>
|
||||||
|
<div class="nav-collapse collapse">
|
||||||
|
<ul class="nav">
|
||||||
|
<li class="active" ><a id="nav_gb28181" href="srs_gb28181.html">GB28181</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/ossrs/srs">
|
||||||
|
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/ossrs/srs?style=social">
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div name="detect_flash">
|
||||||
|
<div id="main_flash_alert" class="alert alert-danger fade in hide">
|
||||||
|
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||||
|
<strong><p>Usage:</p></strong>
|
||||||
|
<p>
|
||||||
|
请点击下面的图标,启用Flash
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
若没有见到这个图标,Chrome浏览器请打开
|
||||||
|
<span class="text-info">chrome://settings/content/flash</span> 并修改为"Ask first"。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div id="main_flash_hdr" class="hide">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-inline">
|
||||||
|
API地址与端口
|
||||||
|
<input type="text" id="txt_api_url" class="input-xxlarge">
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="span4" id="divSipSessionList">
|
||||||
|
<span>设备列表</span><label id="lab_sip_session"></label>
|
||||||
|
<div style="overflow:scroll; height:330px; width:310px">
|
||||||
|
<ul id="channelList"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="span8">
|
||||||
|
<button class="btn btn-primary" id="btn_query_channel">获取设备</button>
|
||||||
|
<button class="btn btn-primary" id="btn_sip_bye">bye</button>
|
||||||
|
<button class="btn btn-primary" id="btn_sip_querycatalog" style="display:none;">querycatalog</button>
|
||||||
|
<div id="context2">
|
||||||
|
<div>
|
||||||
|
<pre id="video" style="overflow:scroll; height:280px"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="span4" id="divMediaChannelList">
|
||||||
|
</div>
|
||||||
|
<div class="span8">
|
||||||
|
<div id="context2">
|
||||||
|
URL:<a id="gb28181ChannelId"></a>
|
||||||
|
<div>
|
||||||
|
<textarea class="span6" id="txt_rtc_url" rows="2"></textarea>
|
||||||
|
<button class="btn btn-primary" id="btn_rtc_play">RTC播放</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Message</div>
|
||||||
|
<pre id="apiMessage" style="overflow:scroll; height:210px"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main_content" class="hide">
|
||||||
|
<div id="main_modal" class="modal hide fade">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h3><a href="https://github.com/ossrs/srs">SrsFlvPlayer</a></h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div>
|
||||||
|
<video id="video_player" width="98%" autoplay controls></video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer" id="my_modal_footer">
|
||||||
|
<div>
|
||||||
|
<div class="btn-group dropup">
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_up"> 上↑ </button>
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_down"> 下↓ </button>
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_left"> ←左 </button>
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_right"> 右→ </button>
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_zoomin"> 放大+ </button>
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_zoomout"> 缩小- </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="rtc_player_modal" class="modal hide fade">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h3><a href="https://github.com/ossrs/srs">RtcPlayer</a></h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<video id="rtc_media_player" width="100%" controls autoplay ></video>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer" id="my_modal_footer">
|
||||||
|
<div>
|
||||||
|
<div class="btn-group dropup">
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_up_rtc"> 上↑ </button>
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_down_rtc"> 下↓ </button>
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_left_rtc"> ←左 </button>
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_right_rtc"> 右→ </button>
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_zoomin_rtc"> 放大+ </button>
|
||||||
|
<button class="btn btn-primary" id="btn_ptz_zoomout_rtc"> 缩小- </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p></p>
|
||||||
|
<p><a href="https://github.com/ossrs/srs">SRS Team © 2013</a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script type="text/javascript" src="js/jquery-1.12.2.min.js"></script>
|
||||||
|
<script type="text/javascript" src="js/bootstrap.min.js"></script>
|
||||||
|
<script type="text/javascript" src="js/json2.js"></script>
|
||||||
|
<script type="text/javascript" src="js/srs.page.js"></script>
|
||||||
|
<script type="text/javascript" src="js/srs.log.js"></script>
|
||||||
|
<script type="text/javascript" src="js/srs.utility.js"></script>
|
||||||
|
<script type="text/javascript" src="js/winlin.utility.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function(){
|
||||||
|
$('#main_content').show();
|
||||||
|
autoLoadPage();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var srs_player = null;
|
||||||
|
var url = null;
|
||||||
|
|
||||||
|
var query = parse_query_string();
|
||||||
|
var query_host = query.host.split(':');
|
||||||
|
if (query_host && query_host.length == 2) {
|
||||||
|
$("#txt_api_url").val("http://" + query_host[0] + ":2020");
|
||||||
|
} else {
|
||||||
|
$("#txt_api_url").val("http://" + query.host + ":2020");
|
||||||
|
}
|
||||||
|
var __active_dar = null;
|
||||||
|
function select_dar(dar_id, num, den) {
|
||||||
|
srs_player.set_dar(num, den);
|
||||||
|
|
||||||
|
if (__active_dar) {
|
||||||
|
__active_dar.removeClass("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
__active_dar = $(dar_id).parent();
|
||||||
|
__active_dar.addClass("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
var __active_size = null;
|
||||||
|
function select_fs_size(size_id, refer, percent) {
|
||||||
|
srs_player.set_fs(refer, percent);
|
||||||
|
|
||||||
|
if (__active_size) {
|
||||||
|
__active_size.removeClass("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
__active_size = $(size_id).parent();
|
||||||
|
__active_size.addClass("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
function select_buffer(buffer_time) {
|
||||||
|
var bt = buffer_time;
|
||||||
|
var bt_id = "#btn_bt_" + bt.toFixed(1).replace(".", "_");
|
||||||
|
select_buffer_time(bt_id, bt);
|
||||||
|
}
|
||||||
|
function select_max_buffer(max_buffer_time) {
|
||||||
|
var mbt = max_buffer_time;
|
||||||
|
var mbt_id = "#btn_mbt_" + mbt.toFixed(1).replace(".", "_");
|
||||||
|
select_max_buffer_time(mbt_id, mbt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var __active_bt = null;
|
||||||
|
function select_buffer_time(bt_id, buffer_time) {
|
||||||
|
srs_player.set_bt(buffer_time);
|
||||||
|
|
||||||
|
if (__active_bt) {
|
||||||
|
__active_bt.removeClass("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
__active_bt = $(bt_id).parent();
|
||||||
|
__active_bt.addClass("active");
|
||||||
|
|
||||||
|
select_max_buffer(srs_player.max_buffer_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
var __active_mbt = null;
|
||||||
|
function select_max_buffer_time(mbt_id, max_buffer_time) {
|
||||||
|
srs_player.set_mbt(max_buffer_time);
|
||||||
|
|
||||||
|
if (__active_mbt) {
|
||||||
|
__active_mbt.removeClass("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
__active_mbt = $(mbt_id).parent();
|
||||||
|
__active_mbt.addClass("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//格式化json显示
|
||||||
|
function syntaxHighlight(json) {
|
||||||
|
if (typeof json != 'string') {
|
||||||
|
json = JSON.stringify(json, undefined, 2);
|
||||||
|
}
|
||||||
|
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(match) {
|
||||||
|
var cls = 'number';
|
||||||
|
if (/^"/.test(match)) {
|
||||||
|
if (/:$/.test(match)) {
|
||||||
|
cls = 'key';
|
||||||
|
} else {
|
||||||
|
cls = 'string';
|
||||||
|
}
|
||||||
|
} else if (/true|false/.test(match)) {
|
||||||
|
cls = 'boolean';
|
||||||
|
} else if (/null/.test(match)) {
|
||||||
|
cls = 'null';
|
||||||
|
}
|
||||||
|
return '<span class="' + cls + '">' + match + '</span>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function http_get(url){
|
||||||
|
var retdata = null;
|
||||||
|
console.log("GET", url);
|
||||||
|
$.ajax({
|
||||||
|
type : "GET",
|
||||||
|
async : false,
|
||||||
|
url : url,
|
||||||
|
contentType: "text/html",
|
||||||
|
data : "",
|
||||||
|
complete : function() {
|
||||||
|
},
|
||||||
|
error : function(ret) {
|
||||||
|
alert("GET 请求失败:" + url);
|
||||||
|
},
|
||||||
|
success : function(ret) {
|
||||||
|
console.log(ret);
|
||||||
|
retdata = ret;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return retdata;
|
||||||
|
}
|
||||||
|
|
||||||
|
function http_post(url, data){
|
||||||
|
var retdata = null;
|
||||||
|
console.log("POST", url);
|
||||||
|
$.ajax({
|
||||||
|
type : "POST",
|
||||||
|
async : false,
|
||||||
|
url : url,
|
||||||
|
contentType: "application/json",
|
||||||
|
data : JSON.stringify(data),
|
||||||
|
complete : function() {
|
||||||
|
},
|
||||||
|
error : function(ret) {
|
||||||
|
alert("POST 请求失败:" + url);
|
||||||
|
},
|
||||||
|
success : function(ret) {
|
||||||
|
console.log(ret);
|
||||||
|
retdata = ret;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return retdata;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParentIdById(id) {
|
||||||
|
for (let parentId in devices) {
|
||||||
|
let parentDevices = devices[parentId];
|
||||||
|
for (let idx in parentDevices) {
|
||||||
|
if (parentDevices[idx].id == id) {
|
||||||
|
return parentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// request: {"device_id": "1", "channel_id": "1", "sub_stream": 0}
|
||||||
|
// response: {"code": 0, "data": {"channel_id": "1", "url": "webrtc://"}}
|
||||||
|
function channelOnClick(chidObj){
|
||||||
|
var chId = chidObj.text;
|
||||||
|
var parentId = getParentIdById(chId);
|
||||||
|
|
||||||
|
var body = {
|
||||||
|
"device_id": parentId,
|
||||||
|
"channel_id": chId,
|
||||||
|
"sub_stream": "0"
|
||||||
|
};
|
||||||
|
url = $("#txt_api_url").val();
|
||||||
|
var apiurl = url + "/srs-sip/v1/invite";
|
||||||
|
|
||||||
|
var ret = http_post(apiurl, body);
|
||||||
|
$('#sipSessionMessage').html(syntaxHighlight(ret));
|
||||||
|
|
||||||
|
if (ret == undefined || ret.code != 0) {
|
||||||
|
alert("invite请求失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
play(ret.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function play(data) {
|
||||||
|
$("#txt_rtc_url").val(data.url);
|
||||||
|
$("#btn_rtc_play").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshGb28181ChList(data) {
|
||||||
|
$("#channelList").empty();
|
||||||
|
devices = {};
|
||||||
|
|
||||||
|
// 遍历数据,将设备信息按照parent_id进行分组
|
||||||
|
for (let idx in data) {
|
||||||
|
var device = data[idx];
|
||||||
|
var parent = device.parent_id;
|
||||||
|
var id = device.device_id;
|
||||||
|
|
||||||
|
// 如果该parent_id不存在在devices对象中,则创建一个新数组
|
||||||
|
if (!devices[parent]) {
|
||||||
|
devices[parent] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将设备信息添加到对应的parent_id数组中
|
||||||
|
devices[parent].push({ id: id, ip: device.ip_address, name: device.name});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历devices对象,生成设备树
|
||||||
|
for (let parent in devices) {
|
||||||
|
var parentDevices = devices[parent];
|
||||||
|
var parentLi = "<li>" + parent + "<ul>";
|
||||||
|
|
||||||
|
// 遍历该parent_id下的设备信息
|
||||||
|
parentDevices.forEach(function(device) {
|
||||||
|
var id = device.id;
|
||||||
|
var title = device.name + "(" + device.ip + ")";
|
||||||
|
var childLi = "<li><a id='linkChannelId" + id + "' href='javascript:void(0)' title='" + title + "' onclick='channelOnClick(this)'>" + id + "</a></li>";
|
||||||
|
parentLi += childLi;
|
||||||
|
});
|
||||||
|
|
||||||
|
parentLi += "</ul></li>";
|
||||||
|
|
||||||
|
// 将生成的设备树添加到页面中
|
||||||
|
$("#channelList").append(parentLi);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async-await-promise based SRS RTC Player.
|
||||||
|
function SrsRtcPlayerAsync() {
|
||||||
|
var self = {};
|
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
// @url The WebRTC url to play with, for example:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream
|
||||||
|
// or specifies the API port:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/livestream
|
||||||
|
// or autostart the play:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?autostart=true
|
||||||
|
// or change the app from live to myapp:
|
||||||
|
// webrtc://r.ossrs.net:11985/myapp/livestream
|
||||||
|
// or change the stream from livestream to mystream:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/mystream
|
||||||
|
// or set the api server to myapi.domain.com:
|
||||||
|
// webrtc://myapi.domain.com/live/livestream
|
||||||
|
// or set the candidate(ip) of answer:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?eip=39.107.238.185
|
||||||
|
// or force to access https API:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?schema=https
|
||||||
|
// or use plaintext, without SRTP:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?encrypt=false
|
||||||
|
// or any other information, will pass-by in the query:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?vhost=xxx
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?token=xxx
|
||||||
|
self.play = async function(url) {
|
||||||
|
var conf = self.__internal.prepareUrl(url);
|
||||||
|
self.pc.addTransceiver("audio", {direction: "recvonly"});
|
||||||
|
self.pc.addTransceiver("video", {direction: "recvonly"});
|
||||||
|
|
||||||
|
var offer = await self.pc.createOffer();
|
||||||
|
await self.pc.setLocalDescription(offer);
|
||||||
|
var session = await new Promise(function(resolve, reject) {
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var data = {
|
||||||
|
api: conf.apiUrl, streamurl: conf.streamUrl, clientip: null, sdp: offer.sdp
|
||||||
|
};
|
||||||
|
console.log("Generated offer: ", data);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: "POST", url: conf.apiUrl, data: JSON.stringify(data),
|
||||||
|
contentType:'application/json', dataType: 'json'
|
||||||
|
}).done(function(data) {
|
||||||
|
console.log("Got answer: ", data);
|
||||||
|
if (data.code) {
|
||||||
|
reject(data); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(data);
|
||||||
|
}).fail(function(reason){
|
||||||
|
reject(reason);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await self.pc.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({type: 'answer', sdp: session.sdp})
|
||||||
|
);
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close the publisher.
|
||||||
|
self.close = function() {
|
||||||
|
self.pc.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
// The callback when got remote stream.
|
||||||
|
self.onaddstream = function (event) {};
|
||||||
|
|
||||||
|
// Internal APIs.
|
||||||
|
self.__internal = {
|
||||||
|
defaultPath: '/rtc/v1/play/',
|
||||||
|
prepareUrl: function (webrtcUrl) {
|
||||||
|
var urlObject = self.__internal.parse(webrtcUrl);
|
||||||
|
|
||||||
|
// If user specifies the schema, use it as API schema.
|
||||||
|
var schema = urlObject.user_query.schema;
|
||||||
|
schema = schema ? schema + ':' : window.location.protocol;
|
||||||
|
|
||||||
|
var port = urlObject.port || 1985;
|
||||||
|
if (schema === 'https:') {
|
||||||
|
port = urlObject.port || 443;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var api = urlObject.user_query.play || self.__internal.defaultPath;
|
||||||
|
if (api.lastIndexOf('/') !== api.length - 1) {
|
||||||
|
api += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
apiUrl = schema + '//' + urlObject.server + ':' + port + api;
|
||||||
|
for (var key in urlObject.user_query) {
|
||||||
|
if (key !== 'api' && key !== 'play') {
|
||||||
|
apiUrl += '&' + key + '=' + urlObject.user_query[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Replace /rtc/v1/play/&k=v to /rtc/v1/play/?k=v
|
||||||
|
var apiUrl = apiUrl.replace(api + '&', api + '?');
|
||||||
|
|
||||||
|
var streamUrl = urlObject.url;
|
||||||
|
|
||||||
|
return {apiUrl: apiUrl, streamUrl: streamUrl, schema: schema, urlObject: urlObject, port: port};
|
||||||
|
},
|
||||||
|
parse: function (url) {
|
||||||
|
// @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.href = url.replace("rtmp://", "http://")
|
||||||
|
.replace("webrtc://", "http://")
|
||||||
|
.replace("rtc://", "http://");
|
||||||
|
|
||||||
|
var vhost = a.hostname;
|
||||||
|
var app = a.pathname.substring(1, a.pathname.lastIndexOf("/"));
|
||||||
|
var stream = a.pathname.slice(a.pathname.lastIndexOf("/") + 1);
|
||||||
|
|
||||||
|
// parse the vhost in the params of app, that srs supports.
|
||||||
|
app = app.replace("...vhost...", "?vhost=");
|
||||||
|
if (app.indexOf("?") >= 0) {
|
||||||
|
var params = app.slice(app.indexOf("?"));
|
||||||
|
app = app.slice(0, app.indexOf("?"));
|
||||||
|
|
||||||
|
if (params.indexOf("vhost=") > 0) {
|
||||||
|
vhost = params.slice(params.indexOf("vhost=") + "vhost=".length);
|
||||||
|
if (vhost.indexOf("&") > 0) {
|
||||||
|
vhost = vhost.slice(0, vhost.indexOf("&"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when vhost equals to server, and server is ip,
|
||||||
|
// the vhost is __defaultVhost__
|
||||||
|
if (a.hostname === vhost) {
|
||||||
|
var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
|
||||||
|
if (re.test(a.hostname)) {
|
||||||
|
vhost = "__defaultVhost__";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the schema
|
||||||
|
var schema = "rtmp";
|
||||||
|
if (url.indexOf("://") > 0) {
|
||||||
|
schema = url.slice(0, url.indexOf("://"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = a.port;
|
||||||
|
if (!port) {
|
||||||
|
if (schema === 'http') {
|
||||||
|
port = 80;
|
||||||
|
} else if (schema === 'https') {
|
||||||
|
port = 443;
|
||||||
|
} else if (schema === 'rtmp') {
|
||||||
|
port = 1935;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = {
|
||||||
|
url: url,
|
||||||
|
schema: schema,
|
||||||
|
server: a.hostname, port: port,
|
||||||
|
vhost: vhost, app: app, stream: stream
|
||||||
|
};
|
||||||
|
self.__internal.fill_query(a.search, ret);
|
||||||
|
|
||||||
|
// For webrtc API, we use 443 if page is https, or schema specified it.
|
||||||
|
if (!ret.port) {
|
||||||
|
if (schema === 'webrtc' || schema === 'rtc') {
|
||||||
|
if (ret.user_query.schema === 'https') {
|
||||||
|
ret.port = 443;
|
||||||
|
} else if (window.location.href.indexOf('https://') === 0) {
|
||||||
|
ret.port = 443;
|
||||||
|
} else {
|
||||||
|
// For WebRTC, SRS use 1985 as default API port.
|
||||||
|
ret.port = 1985;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
fill_query: function (query_string, obj) {
|
||||||
|
// pure user query object.
|
||||||
|
obj.user_query = {};
|
||||||
|
|
||||||
|
if (query_string.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split again for angularjs.
|
||||||
|
if (query_string.indexOf("?") >= 0) {
|
||||||
|
query_string = query_string.split("?")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var queries = query_string.split("&");
|
||||||
|
for (var i = 0; i < queries.length; i++) {
|
||||||
|
var elem = queries[i];
|
||||||
|
|
||||||
|
var query = elem.split("=");
|
||||||
|
obj[query[0]] = query[1];
|
||||||
|
obj.user_query[query[0]] = query[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// alias domain for vhost.
|
||||||
|
if (obj.domain) {
|
||||||
|
obj.vhost = obj.domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pc = new RTCPeerConnection(null);
|
||||||
|
self.pc.onaddstream = function (event) {
|
||||||
|
if (self.onaddstream) {
|
||||||
|
self.onaddstream(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
var flvPlayer = null;
|
||||||
|
var hlsPlayer = null;
|
||||||
|
var devices = {};
|
||||||
|
|
||||||
|
var stopPlayers = function () {
|
||||||
|
if (flvPlayer) {
|
||||||
|
flvPlayer.destroy();
|
||||||
|
flvPlayer = null;
|
||||||
|
}
|
||||||
|
if (hlsPlayer) {
|
||||||
|
hlsPlayer.destroy();
|
||||||
|
hlsPlayer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var hide_for_error = function () {
|
||||||
|
$('#main_flash_alert').show();
|
||||||
|
$('#main_info').hide();
|
||||||
|
$('#main_tips').hide();
|
||||||
|
$('#video_player').hide();
|
||||||
|
//$('#btn_play').hide();
|
||||||
|
|
||||||
|
stopPlayers();
|
||||||
|
};
|
||||||
|
|
||||||
|
var show_for_ok = function () {
|
||||||
|
$('#main_flash_alert').hide();
|
||||||
|
$('#main_info').show();
|
||||||
|
$('#main_tips').show();
|
||||||
|
$('#video_player').show();
|
||||||
|
//$('#btn_play').show();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/****
|
||||||
|
* The parameters for this page:
|
||||||
|
* schema, the protocol schema, rtmp or http.
|
||||||
|
* server, the ip of the url.
|
||||||
|
* port, the rtmp port of url.
|
||||||
|
* vhost, the vhost of url, can equals to server.
|
||||||
|
* app, the app of url.
|
||||||
|
* stream, the stream of url, can endwith .flv or .mp4 or nothing for RTMP.
|
||||||
|
* autostart, whether auto play the stream.
|
||||||
|
* buffer, the buffer time in seconds.
|
||||||
|
* extra params:
|
||||||
|
* shp_identify, hls+ param.
|
||||||
|
* for example:
|
||||||
|
* http://localhost:8088/players/srs_player.html?vhost=ossrs.net&app=live&stream=livestream&server=ossrs.net&port=1935&autostart=true&schema=rtmp
|
||||||
|
* http://localhost:8088/players/srs_player.html?vhost=ossrs.net&app=live&stream=livestream.flv&server=ossrs.net&port=8080&autostart=true&schema=http
|
||||||
|
*/
|
||||||
|
var autoLoadPage = function() {
|
||||||
|
var query = parse_query_string();
|
||||||
|
|
||||||
|
// get the vhost and port to set the default url.
|
||||||
|
// url set to: http://localhost:8080/live/livestream.flv
|
||||||
|
srs_init_flv("#txt_url", "#main_modal");
|
||||||
|
srs_init_flv("#txt_url", "#rtc_player_modal");
|
||||||
|
|
||||||
|
// consts for buffer and max buffer.
|
||||||
|
var bts = [0.1, 0.2, 0.3, 0.5, 0.8, 1, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30];
|
||||||
|
var mbts = [0.6, 0.9, 1.2, 1.5, 2.4, 3, 6, 9, 12, 15, 18, 24, 30, 45, 60, 90];
|
||||||
|
|
||||||
|
// the play startup time.
|
||||||
|
var pst = new Date();
|
||||||
|
|
||||||
|
$("#main_modal").on("show", function(){
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#main_modal").on("hide", function(){
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
if (true) {
|
||||||
|
for (var bt of bts) {
|
||||||
|
var bt_id = "#btn_bt_" + bt.toFixed(1).replace(".", "_");
|
||||||
|
|
||||||
|
var bt_fun = function(id, v){
|
||||||
|
$(bt_id).click(function(){
|
||||||
|
select_buffer_time(id, v);
|
||||||
|
|
||||||
|
// remember the chagned buffer.
|
||||||
|
if (Number(query.buffer) != srs_player.buffer_time) {
|
||||||
|
query.buffer = srs_player.buffer_time;
|
||||||
|
apply_url_change();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
bt_fun(bt_id, bt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (true) {
|
||||||
|
for (var mbt of mbts) {
|
||||||
|
var mbt_id = "#btn_mbt_" + mbt.toFixed(1).replace(".", "_");
|
||||||
|
|
||||||
|
var mbt_fun = function(id, v){
|
||||||
|
$(mbt_id).click(function(){
|
||||||
|
select_max_buffer_time(id, v);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
mbt_fun(mbt_id, mbt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true){
|
||||||
|
var time_query = function(){
|
||||||
|
$("#btn_query_channel").click();
|
||||||
|
setTimeout(function () {$("#btn_query_channel").click()}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
$("#btn_sip_bye").click(function(){
|
||||||
|
var text = $("#sipSessionId").text();
|
||||||
|
if (text.indexOf("-->") != -1) {
|
||||||
|
var str = text.split("-->");
|
||||||
|
id = str[0];
|
||||||
|
var str2 = str[1].split(":")
|
||||||
|
chid = str2[1];
|
||||||
|
|
||||||
|
url = $("#txt_api_url").val();
|
||||||
|
var apiurl = url + "/srs-sip/v1/gb28181?action=sip_bye&id=" + id + "&chid="+chid;
|
||||||
|
var ret = http_get(apiurl);
|
||||||
|
$('#sipSessionMessage').html(syntaxHighlight(ret));
|
||||||
|
|
||||||
|
if (ret != undefined && ret.code == 0){
|
||||||
|
time_query();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btn_sip_querycatalog").click(function(){
|
||||||
|
var text = $("#sipSessionId").text();
|
||||||
|
if (text.indexOf("-->") != -1) {
|
||||||
|
var str = text.split("-->");
|
||||||
|
id = str[0];
|
||||||
|
var str2 = str[1].split(":")
|
||||||
|
chid = str2[0];
|
||||||
|
|
||||||
|
url = $("#txt_api_url").val();
|
||||||
|
var apiurl = url + "/srs-sip/v1/gb28181?action=sip_query_catalog&id=" + id;
|
||||||
|
var ret = http_get(apiurl);
|
||||||
|
$('#sipSessionMessage').html(syntaxHighlight(ret));
|
||||||
|
if (ret != undefined && ret.code == 0){
|
||||||
|
time_query();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btn_query_channel").click(function(){
|
||||||
|
url = $("#txt_api_url").val();
|
||||||
|
var apiurl = url + "/srs-sip/v1/channels/"
|
||||||
|
var ret = http_get(apiurl);
|
||||||
|
$('#apiMessage').html(syntaxHighlight(ret));
|
||||||
|
|
||||||
|
if (ret != undefined && ret.code == 0){
|
||||||
|
refreshGb28181ChList(ret.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var call_ptz_cmd = function(cmd) {
|
||||||
|
var str = $("#gb28181ChannelId").text();
|
||||||
|
var str_array = str.split("@")
|
||||||
|
var chid = "";
|
||||||
|
var id = "";
|
||||||
|
if (str_array.length < 1){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var speed = "136";
|
||||||
|
|
||||||
|
id = str_array[0];
|
||||||
|
chid = str_array[1];
|
||||||
|
|
||||||
|
url = $("#txt_api_url").val();
|
||||||
|
var apiurl = url + "/srs-sip/v1/gb28181?action=sip_ptz&id=" + id + "&chid="+chid+ "&ptzcmd="+cmd + "&speed=" + speed;
|
||||||
|
var ret = http_get(apiurl);
|
||||||
|
$('#apiMessage').html(syntaxHighlight(ret));
|
||||||
|
};
|
||||||
|
|
||||||
|
var ptz_cmd = ["up", "down", "right", "left", "zoomin", "zoomout"]
|
||||||
|
for (var i=0; i<ptz_cmd.length; i++){
|
||||||
|
var bt_fun = function(id, cmd){
|
||||||
|
$(bt_id).mousedown(function(){
|
||||||
|
call_ptz_cmd(cmd);
|
||||||
|
});
|
||||||
|
$(bt_id).mouseup(function(){
|
||||||
|
call_ptz_cmd("stop");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var bt_id = "#btn_ptz_"+ptz_cmd[i]+ "_rtc";
|
||||||
|
bt_fun(bt_id, ptz_cmd[i]);
|
||||||
|
|
||||||
|
bt_id = "#btn_ptz_"+ptz_cmd[i];
|
||||||
|
bt_fun(bt_id, ptz_cmd[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sdk = null; // Global handler to do cleanup when replaying.
|
||||||
|
var startPlay = function() {
|
||||||
|
$('#rtc_media_player').show();
|
||||||
|
|
||||||
|
// Close PC when user replay.
|
||||||
|
if (sdk) {
|
||||||
|
sdk.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
sdk = new SrsRtcPlayerAsync();
|
||||||
|
sdk.onaddstream = function (event) {
|
||||||
|
console.log('Start play, event: ', event);
|
||||||
|
$('#rtc_media_player').prop('srcObject', event.stream);
|
||||||
|
};
|
||||||
|
|
||||||
|
// For example:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream
|
||||||
|
var url = $("#txt_rtc_url").val();
|
||||||
|
sdk.play(url).then(function(session){
|
||||||
|
$('#sessionid').html(session.sessionid);
|
||||||
|
$('#simulator-drop').attr('href', session.simulator + '?drop=1&username=' + session.sessionid);
|
||||||
|
}).catch(function (reason) {
|
||||||
|
sdk.close();
|
||||||
|
$('#rtc_media_player').hide();
|
||||||
|
console.error(reason);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$("#btn_rtc_play").click(function(){
|
||||||
|
$('#rtc_media_player').width(srs_get_player_width);
|
||||||
|
$('#rtc_media_player').height(srs_get_player_height);
|
||||||
|
$("#rtc_player_modal").modal({show: true, keyboard: false});
|
||||||
|
|
||||||
|
startPlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$("#rtc_player_modal").on("hide", function(){
|
||||||
|
if (sdk) {
|
||||||
|
sdk.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
BIN
web/html/js/AdobeFlashPlayerInstall.swf
Normal file
BIN
web/html/js/AdobeFlashPlayerInstall.swf
Normal file
Binary file not shown.
5551
web/html/js/adapter-7.4.0.js
Normal file
5551
web/html/js/adapter-7.4.0.js
Normal file
File diff suppressed because it is too large
Load Diff
1
web/html/js/adapter-7.4.0.min.js
vendored
Normal file
1
web/html/js/adapter-7.4.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
web/html/js/bootstrap.min.js
vendored
Normal file
6
web/html/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
19
web/html/js/dash-v4.5.1.all.min.js
vendored
Normal file
19
web/html/js/dash-v4.5.1.all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/html/js/dash.all.min.js.map
Normal file
1
web/html/js/dash.all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
web/html/js/hls-1.4.14.min.js
vendored
Normal file
2
web/html/js/hls-1.4.14.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/html/js/hls.min.js.map
Normal file
1
web/html/js/hls.min.js.map
Normal file
File diff suppressed because one or more lines are too long
5
web/html/js/jquery-1.12.2.min.js
vendored
Normal file
5
web/html/js/jquery-1.12.2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/html/js/jquery-1.12.2.min.map
Normal file
1
web/html/js/jquery-1.12.2.min.map
Normal file
File diff suppressed because one or more lines are too long
486
web/html/js/json2.js
Normal file
486
web/html/js/json2.js
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
/*
|
||||||
|
json2.js
|
||||||
|
2013-05-26
|
||||||
|
|
||||||
|
Public Domain.
|
||||||
|
|
||||||
|
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
||||||
|
|
||||||
|
See http://www.JSON.org/js.html
|
||||||
|
|
||||||
|
|
||||||
|
This code should be minified before deployment.
|
||||||
|
See http://javascript.crockford.com/jsmin.html
|
||||||
|
|
||||||
|
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
|
||||||
|
NOT CONTROL.
|
||||||
|
|
||||||
|
|
||||||
|
This file creates a global JSON object containing two methods: stringify
|
||||||
|
and parse.
|
||||||
|
|
||||||
|
JSON.stringify(value, replacer, space)
|
||||||
|
value any JavaScript value, usually an object or array.
|
||||||
|
|
||||||
|
replacer an optional parameter that determines how object
|
||||||
|
values are stringified for objects. It can be a
|
||||||
|
function or an array of strings.
|
||||||
|
|
||||||
|
space an optional parameter that specifies the indentation
|
||||||
|
of nested structures. If it is omitted, the text will
|
||||||
|
be packed without extra whitespace. If it is a number,
|
||||||
|
it will specify the number of spaces to indent at each
|
||||||
|
level. If it is a string (such as '\t' or ' '),
|
||||||
|
it contains the characters used to indent at each level.
|
||||||
|
|
||||||
|
This method produces a JSON text from a JavaScript value.
|
||||||
|
|
||||||
|
When an object value is found, if the object contains a toJSON
|
||||||
|
method, its toJSON method will be called and the result will be
|
||||||
|
stringified. A toJSON method does not serialize: it returns the
|
||||||
|
value represented by the name/value pair that should be serialized,
|
||||||
|
or undefined if nothing should be serialized. The toJSON method
|
||||||
|
will be passed the key associated with the value, and this will be
|
||||||
|
bound to the value
|
||||||
|
|
||||||
|
For example, this would serialize Dates as ISO strings.
|
||||||
|
|
||||||
|
Date.prototype.toJSON = function (key) {
|
||||||
|
function f(n) {
|
||||||
|
// Format integers to have at least two digits.
|
||||||
|
return n < 10 ? '0' + n : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getUTCFullYear() + '-' +
|
||||||
|
f(this.getUTCMonth() + 1) + '-' +
|
||||||
|
f(this.getUTCDate()) + 'T' +
|
||||||
|
f(this.getUTCHours()) + ':' +
|
||||||
|
f(this.getUTCMinutes()) + ':' +
|
||||||
|
f(this.getUTCSeconds()) + 'Z';
|
||||||
|
};
|
||||||
|
|
||||||
|
You can provide an optional replacer method. It will be passed the
|
||||||
|
key and value of each member, with this bound to the containing
|
||||||
|
object. The value that is returned from your method will be
|
||||||
|
serialized. If your method returns undefined, then the member will
|
||||||
|
be excluded from the serialization.
|
||||||
|
|
||||||
|
If the replacer parameter is an array of strings, then it will be
|
||||||
|
used to select the members to be serialized. It filters the results
|
||||||
|
such that only members with keys listed in the replacer array are
|
||||||
|
stringified.
|
||||||
|
|
||||||
|
Values that do not have JSON representations, such as undefined or
|
||||||
|
functions, will not be serialized. Such values in objects will be
|
||||||
|
dropped; in arrays they will be replaced with null. You can use
|
||||||
|
a replacer function to replace those with JSON values.
|
||||||
|
JSON.stringify(undefined) returns undefined.
|
||||||
|
|
||||||
|
The optional space parameter produces a stringification of the
|
||||||
|
value that is filled with line breaks and indentation to make it
|
||||||
|
easier to read.
|
||||||
|
|
||||||
|
If the space parameter is a non-empty string, then that string will
|
||||||
|
be used for indentation. If the space parameter is a number, then
|
||||||
|
the indentation will be that many spaces.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
text = JSON.stringify(['e', {pluribus: 'unum'}]);
|
||||||
|
// text is '["e",{"pluribus":"unum"}]'
|
||||||
|
|
||||||
|
|
||||||
|
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
|
||||||
|
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
|
||||||
|
|
||||||
|
text = JSON.stringify([new Date()], function (key, value) {
|
||||||
|
return this[key] instanceof Date ?
|
||||||
|
'Date(' + this[key] + ')' : value;
|
||||||
|
});
|
||||||
|
// text is '["Date(---current time---)"]'
|
||||||
|
|
||||||
|
|
||||||
|
JSON.parse(text, reviver)
|
||||||
|
This method parses a JSON text to produce an object or array.
|
||||||
|
It can throw a SyntaxError exception.
|
||||||
|
|
||||||
|
The optional reviver parameter is a function that can filter and
|
||||||
|
transform the results. It receives each of the keys and values,
|
||||||
|
and its return value is used instead of the original value.
|
||||||
|
If it returns what it received, then the structure is not modified.
|
||||||
|
If it returns undefined then the member is deleted.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
// Parse the text. Values that look like ISO date strings will
|
||||||
|
// be converted to Date objects.
|
||||||
|
|
||||||
|
myData = JSON.parse(text, function (key, value) {
|
||||||
|
var a;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
a =
|
||||||
|
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
|
||||||
|
if (a) {
|
||||||
|
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
|
||||||
|
+a[5], +a[6]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
|
||||||
|
var d;
|
||||||
|
if (typeof value === 'string' &&
|
||||||
|
value.slice(0, 5) === 'Date(' &&
|
||||||
|
value.slice(-1) === ')') {
|
||||||
|
d = new Date(value.slice(5, -1));
|
||||||
|
if (d) {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
This is a reference implementation. You are free to copy, modify, or
|
||||||
|
redistribute.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*jslint evil: true, regexp: true */
|
||||||
|
|
||||||
|
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
|
||||||
|
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
|
||||||
|
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
|
||||||
|
lastIndex, length, parse, prototype, push, replace, slice, stringify,
|
||||||
|
test, toJSON, toString, valueOf
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// Create a JSON object only if one does not already exist. We create the
|
||||||
|
// methods in a closure to avoid creating global variables.
|
||||||
|
|
||||||
|
if (typeof JSON !== 'object') {
|
||||||
|
JSON = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function f(n) {
|
||||||
|
// Format integers to have at least two digits.
|
||||||
|
return n < 10 ? '0' + n : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Date.prototype.toJSON !== 'function') {
|
||||||
|
|
||||||
|
Date.prototype.toJSON = function () {
|
||||||
|
|
||||||
|
return isFinite(this.valueOf())
|
||||||
|
? this.getUTCFullYear() + '-' +
|
||||||
|
f(this.getUTCMonth() + 1) + '-' +
|
||||||
|
f(this.getUTCDate()) + 'T' +
|
||||||
|
f(this.getUTCHours()) + ':' +
|
||||||
|
f(this.getUTCMinutes()) + ':' +
|
||||||
|
f(this.getUTCSeconds()) + 'Z'
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
String.prototype.toJSON =
|
||||||
|
Number.prototype.toJSON =
|
||||||
|
Boolean.prototype.toJSON = function () {
|
||||||
|
return this.valueOf();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||||
|
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||||
|
gap,
|
||||||
|
indent,
|
||||||
|
meta = { // table of character substitutions
|
||||||
|
'\b': '\\b',
|
||||||
|
'\t': '\\t',
|
||||||
|
'\n': '\\n',
|
||||||
|
'\f': '\\f',
|
||||||
|
'\r': '\\r',
|
||||||
|
'"' : '\\"',
|
||||||
|
'\\': '\\\\'
|
||||||
|
},
|
||||||
|
rep;
|
||||||
|
|
||||||
|
|
||||||
|
function quote(string) {
|
||||||
|
|
||||||
|
// If the string contains no control characters, no quote characters, and no
|
||||||
|
// backslash characters, then we can safely slap some quotes around it.
|
||||||
|
// Otherwise we must also replace the offending characters with safe escape
|
||||||
|
// sequences.
|
||||||
|
|
||||||
|
escapable.lastIndex = 0;
|
||||||
|
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
|
||||||
|
var c = meta[a];
|
||||||
|
return typeof c === 'string'
|
||||||
|
? c
|
||||||
|
: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
||||||
|
}) + '"' : '"' + string + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function str(key, holder) {
|
||||||
|
|
||||||
|
// Produce a string from holder[key].
|
||||||
|
|
||||||
|
var i, // The loop counter.
|
||||||
|
k, // The member key.
|
||||||
|
v, // The member value.
|
||||||
|
length,
|
||||||
|
mind = gap,
|
||||||
|
partial,
|
||||||
|
value = holder[key];
|
||||||
|
|
||||||
|
// If the value has a toJSON method, call it to obtain a replacement value.
|
||||||
|
|
||||||
|
if (value && typeof value === 'object' &&
|
||||||
|
typeof value.toJSON === 'function') {
|
||||||
|
value = value.toJSON(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we were called with a replacer function, then call the replacer to
|
||||||
|
// obtain a replacement value.
|
||||||
|
|
||||||
|
if (typeof rep === 'function') {
|
||||||
|
value = rep.call(holder, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// What happens next depends on the value's type.
|
||||||
|
|
||||||
|
switch (typeof value) {
|
||||||
|
case 'string':
|
||||||
|
return quote(value);
|
||||||
|
|
||||||
|
case 'number':
|
||||||
|
|
||||||
|
// JSON numbers must be finite. Encode non-finite numbers as null.
|
||||||
|
|
||||||
|
return isFinite(value) ? String(value) : 'null';
|
||||||
|
|
||||||
|
case 'boolean':
|
||||||
|
case 'null':
|
||||||
|
|
||||||
|
// If the value is a boolean or null, convert it to a string. Note:
|
||||||
|
// typeof null does not produce 'null'. The case is included here in
|
||||||
|
// the remote chance that this gets fixed someday.
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
|
||||||
|
// If the type is 'object', we might be dealing with an object or an array or
|
||||||
|
// null.
|
||||||
|
|
||||||
|
case 'object':
|
||||||
|
|
||||||
|
// Due to a specification blunder in ECMAScript, typeof null is 'object',
|
||||||
|
// so watch out for that case.
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make an array to hold the partial results of stringifying this object value.
|
||||||
|
|
||||||
|
gap += indent;
|
||||||
|
partial = [];
|
||||||
|
|
||||||
|
// Is the value an array?
|
||||||
|
|
||||||
|
if (Object.prototype.toString.apply(value) === '[object Array]') {
|
||||||
|
|
||||||
|
// The value is an array. Stringify every element. Use null as a placeholder
|
||||||
|
// for non-JSON values.
|
||||||
|
|
||||||
|
length = value.length;
|
||||||
|
for (i = 0; i < length; i += 1) {
|
||||||
|
partial[i] = str(i, value) || 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join all of the elements together, separated with commas, and wrap them in
|
||||||
|
// brackets.
|
||||||
|
|
||||||
|
v = partial.length === 0
|
||||||
|
? '[]'
|
||||||
|
: gap
|
||||||
|
? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
|
||||||
|
: '[' + partial.join(',') + ']';
|
||||||
|
gap = mind;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the replacer is an array, use it to select the members to be stringified.
|
||||||
|
|
||||||
|
if (rep && typeof rep === 'object') {
|
||||||
|
length = rep.length;
|
||||||
|
for (i = 0; i < length; i += 1) {
|
||||||
|
if (typeof rep[i] === 'string') {
|
||||||
|
k = rep[i];
|
||||||
|
v = str(k, value);
|
||||||
|
if (v) {
|
||||||
|
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Otherwise, iterate through all of the keys in the object.
|
||||||
|
|
||||||
|
for (k in value) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||||
|
v = str(k, value);
|
||||||
|
if (v) {
|
||||||
|
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join all of the member texts together, separated with commas,
|
||||||
|
// and wrap them in braces.
|
||||||
|
|
||||||
|
v = partial.length === 0
|
||||||
|
? '{}'
|
||||||
|
: gap
|
||||||
|
? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
|
||||||
|
: '{' + partial.join(',') + '}';
|
||||||
|
gap = mind;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the JSON object does not yet have a stringify method, give it one.
|
||||||
|
|
||||||
|
if (typeof JSON.stringify !== 'function') {
|
||||||
|
JSON.stringify = function (value, replacer, space) {
|
||||||
|
|
||||||
|
// The stringify method takes a value and an optional replacer, and an optional
|
||||||
|
// space parameter, and returns a JSON text. The replacer can be a function
|
||||||
|
// that can replace values, or an array of strings that will select the keys.
|
||||||
|
// A default replacer method can be provided. Use of the space parameter can
|
||||||
|
// produce text that is more easily readable.
|
||||||
|
|
||||||
|
var i;
|
||||||
|
gap = '';
|
||||||
|
indent = '';
|
||||||
|
|
||||||
|
// If the space parameter is a number, make an indent string containing that
|
||||||
|
// many spaces.
|
||||||
|
|
||||||
|
if (typeof space === 'number') {
|
||||||
|
for (i = 0; i < space; i += 1) {
|
||||||
|
indent += ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the space parameter is a string, it will be used as the indent string.
|
||||||
|
|
||||||
|
} else if (typeof space === 'string') {
|
||||||
|
indent = space;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is a replacer, it must be a function or an array.
|
||||||
|
// Otherwise, throw an error.
|
||||||
|
|
||||||
|
rep = replacer;
|
||||||
|
if (replacer && typeof replacer !== 'function' &&
|
||||||
|
(typeof replacer !== 'object' ||
|
||||||
|
typeof replacer.length !== 'number')) {
|
||||||
|
throw new Error('JSON.stringify');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a fake root object containing our value under the key of ''.
|
||||||
|
// Return the result of stringifying the value.
|
||||||
|
|
||||||
|
return str('', {'': value});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// If the JSON object does not yet have a parse method, give it one.
|
||||||
|
|
||||||
|
if (typeof JSON.parse !== 'function') {
|
||||||
|
JSON.parse = function (text, reviver) {
|
||||||
|
|
||||||
|
// The parse method takes a text and an optional reviver function, and returns
|
||||||
|
// a JavaScript value if the text is a valid JSON text.
|
||||||
|
|
||||||
|
var j;
|
||||||
|
|
||||||
|
function walk(holder, key) {
|
||||||
|
|
||||||
|
// The walk method is used to recursively walk the resulting structure so
|
||||||
|
// that modifications can be made.
|
||||||
|
|
||||||
|
var k, v, value = holder[key];
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
for (k in value) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||||
|
v = walk(value, k);
|
||||||
|
if (v !== undefined) {
|
||||||
|
value[k] = v;
|
||||||
|
} else {
|
||||||
|
delete value[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reviver.call(holder, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Parsing happens in four stages. In the first stage, we replace certain
|
||||||
|
// Unicode characters with escape sequences. JavaScript handles many characters
|
||||||
|
// incorrectly, either silently deleting them, or treating them as line endings.
|
||||||
|
|
||||||
|
text = String(text);
|
||||||
|
cx.lastIndex = 0;
|
||||||
|
if (cx.test(text)) {
|
||||||
|
text = text.replace(cx, function (a) {
|
||||||
|
return '\\u' +
|
||||||
|
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the second stage, we run the text against regular expressions that look
|
||||||
|
// for non-JSON patterns. We are especially concerned with '()' and 'new'
|
||||||
|
// because they can cause invocation, and '=' because it can cause mutation.
|
||||||
|
// But just to be safe, we want to reject all unexpected forms.
|
||||||
|
|
||||||
|
// We split the second stage into 4 regexp operations in order to work around
|
||||||
|
// crippling inefficiencies in IE's and Safari's regexp engines. First we
|
||||||
|
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
|
||||||
|
// replace all simple value tokens with ']' characters. Third, we delete all
|
||||||
|
// open brackets that follow a colon or comma or that begin the text. Finally,
|
||||||
|
// we look to see that the remaining characters are only whitespace or ']' or
|
||||||
|
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
|
||||||
|
|
||||||
|
if (/^[\],:{}\s]*$/
|
||||||
|
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
|
||||||
|
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
|
||||||
|
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
|
||||||
|
|
||||||
|
// In the third stage we use the eval function to compile the text into a
|
||||||
|
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
|
||||||
|
// in JavaScript: it can begin a block or an object literal. We wrap the text
|
||||||
|
// in parens to eliminate the ambiguity.
|
||||||
|
|
||||||
|
j = eval('(' + text + ')');
|
||||||
|
|
||||||
|
// In the optional fourth stage, we recursively walk the new structure, passing
|
||||||
|
// each name/value pair to a reviver function for possible transformation.
|
||||||
|
|
||||||
|
return typeof reviver === 'function'
|
||||||
|
? walk({'': j}, '')
|
||||||
|
: j;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the text is not JSON parseable, then a SyntaxError is thrown.
|
||||||
|
|
||||||
|
throw new SyntaxError('JSON.parse');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}());
|
||||||
9
web/html/js/mpegts-1.7.3.min.js
vendored
Normal file
9
web/html/js/mpegts-1.7.3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/html/js/mpegts.js.map
Normal file
1
web/html/js/mpegts.js.map
Normal file
File diff suppressed because one or more lines are too long
37
web/html/js/srs.log.js
Normal file
37
web/html/js/srs.log.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* log specified, there must be a log element as:
|
||||||
|
<!-- for the log -->
|
||||||
|
<div class="alert alert-info fade in" id="txt_log">
|
||||||
|
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||||
|
<strong><span id="txt_log_title">标题:</span></strong>
|
||||||
|
<span id="txt_log_msg">日志内容</span>
|
||||||
|
</div>
|
||||||
|
*/
|
||||||
|
var srs_log_disabled = false;
|
||||||
|
function info(desc) {
|
||||||
|
if (srs_log_disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#txt_log").addClass("alert-info").removeClass("alert-error").removeClass("alert-warn");
|
||||||
|
$("#txt_log_title").text("Info:");
|
||||||
|
$("#txt_log_msg").html(desc);
|
||||||
|
}
|
||||||
|
function warn(code, desc) {
|
||||||
|
if (srs_log_disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#txt_log").removeClass("alert-info").removeClass("alert-error").addClass("alert-warn");
|
||||||
|
$("#txt_log_title").text("Warn:");
|
||||||
|
$("#txt_log_msg").html("code: " + code + ", " + desc);
|
||||||
|
}
|
||||||
|
function error(code, desc) {
|
||||||
|
if (srs_log_disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#txt_log").removeClass("alert-info").addClass("alert-error").removeClass("alert-warn");
|
||||||
|
$("#txt_log_title").text("Error:");
|
||||||
|
$("#txt_log_msg").html("code: " + code + ", " + desc);
|
||||||
|
}
|
||||||
182
web/html/js/srs.page.js
Normal file
182
web/html/js/srs.page.js
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
|
||||||
|
// to query the swf anti cache.
|
||||||
|
function srs_get_version_code() { return "1.33"; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* player specified size.
|
||||||
|
*/
|
||||||
|
function srs_get_player_modal() { return 740; }
|
||||||
|
function srs_get_player_width() { return srs_get_player_modal() - 30; }
|
||||||
|
function srs_get_player_height() { return srs_get_player_width() * 9 / 19; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update the navigator, add same query string.
|
||||||
|
*/
|
||||||
|
function update_nav() {
|
||||||
|
$("#nav_srs_player").attr("href", "srs_player.html" + window.location.search);
|
||||||
|
$("#nav_rtc_player").attr("href", "rtc_player.html" + window.location.search);
|
||||||
|
$("#nav_rtc_publisher").attr("href", "rtc_publisher.html" + window.location.search);
|
||||||
|
$("#nav_whip").attr("href", "whip.html" + window.location.search);
|
||||||
|
$("#nav_whep").attr("href", "whep.html" + window.location.search);
|
||||||
|
$("#nav_srs_publisher").attr("href", "srs_publisher.html" + window.location.search);
|
||||||
|
$("#nav_srs_chat").attr("href", "srs_chat.html" + window.location.search);
|
||||||
|
$("#nav_srs_bwt").attr("href", "srs_bwt.html" + window.location.search);
|
||||||
|
$("#nav_vlc").attr("href", "vlc.html" + window.location.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special extra params, such as auth_key.
|
||||||
|
function user_extra_params(query, params, rtc) {
|
||||||
|
var queries = params || [];
|
||||||
|
|
||||||
|
for (var key in query.user_query) {
|
||||||
|
if (key === 'app' || key === 'autostart' || key === 'dir'
|
||||||
|
|| key === 'filename' || key === 'host' || key === 'hostname'
|
||||||
|
|| key === 'http_port' || key === 'pathname' || key === 'port'
|
||||||
|
|| key === 'server' || key === 'stream' || key === 'buffer'
|
||||||
|
|| key === 'schema' || key === 'vhost' || key === 'api'
|
||||||
|
|| key === 'path'
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query[key]) {
|
||||||
|
queries.push(key + '=' + query[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_default_port(schema, port) {
|
||||||
|
return (schema === 'http' && port === 80)
|
||||||
|
|| (schema === 'https' && port === 443)
|
||||||
|
|| (schema === 'webrtc' && port === 1985)
|
||||||
|
|| (schema === 'rtmp' && port === 1935);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@param server the ip of server. default to window.location.hostname
|
||||||
|
@param vhost the vhost of HTTP-FLV. default to window.location.hostname
|
||||||
|
@param port the port of HTTP-FLV. default to 1935
|
||||||
|
@param app the app of HTTP-FLV. default to live.
|
||||||
|
@param stream the stream of HTTP-FLV. default to livestream.flv
|
||||||
|
*/
|
||||||
|
function build_default_flv_url() {
|
||||||
|
var query = parse_query_string();
|
||||||
|
|
||||||
|
var schema = (!query.schema)? "http":query.schema;
|
||||||
|
var server = (!query.server)? window.location.hostname:query.server;
|
||||||
|
var port = (!query.port)? (schema==="http"? 8080:1935) : Number(query.port);
|
||||||
|
var vhost = (!query.vhost)? window.location.hostname:query.vhost;
|
||||||
|
var app = (!query.app)? "live":query.app;
|
||||||
|
var stream = (!query.stream)? "livestream.flv":query.stream;
|
||||||
|
|
||||||
|
var queries = [];
|
||||||
|
if (server !== vhost && vhost !== "__defaultVhost__") {
|
||||||
|
queries.push("vhost=" + vhost);
|
||||||
|
}
|
||||||
|
queries = user_extra_params(query, queries);
|
||||||
|
|
||||||
|
var uri = schema + "://" + server;
|
||||||
|
if (!is_default_port(schema, port)) {
|
||||||
|
uri += ":" + port;
|
||||||
|
}
|
||||||
|
uri += "/" + app + "/" + stream + "?" + queries.join('&');
|
||||||
|
while (uri.indexOf("?") === uri.length - 1) {
|
||||||
|
uri = uri.slice(0, uri.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_default_rtc_url(query) {
|
||||||
|
// The format for query string to overwrite configs of server.
|
||||||
|
console.log('?eip=x.x.x.x to overwrite candidate. 覆盖服务器candidate(外网IP)配置');
|
||||||
|
console.log('?api=x to overwrite WebRTC API(1985).');
|
||||||
|
console.log('?schema=http|https to overwrite WebRTC API protocol.');
|
||||||
|
|
||||||
|
var server = (!query.server)? window.location.hostname:query.server;
|
||||||
|
var vhost = (!query.vhost)? window.location.hostname:query.vhost;
|
||||||
|
var app = (!query.app)? "live":query.app;
|
||||||
|
var stream = (!query.stream)? "livestream":query.stream;
|
||||||
|
var api = query.api? ':'+query.api : '';
|
||||||
|
|
||||||
|
var queries = [];
|
||||||
|
if (server !== vhost && vhost !== "__defaultVhost__") {
|
||||||
|
queries.push("vhost=" + vhost);
|
||||||
|
}
|
||||||
|
if (query.schema && window.location.protocol !== query.schema + ':') {
|
||||||
|
queries.push('schema=' + query.schema);
|
||||||
|
}
|
||||||
|
queries = user_extra_params(query, queries, true);
|
||||||
|
|
||||||
|
var uri = "webrtc://" + server + api + "/" + app + "/" + stream + "?" + queries.join('&');
|
||||||
|
while (uri.lastIndexOf("?") === uri.length - 1) {
|
||||||
|
uri = uri.slice(0, uri.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri;
|
||||||
|
};
|
||||||
|
|
||||||
|
function build_default_whip_whep_url(query, apiPath) {
|
||||||
|
// The format for query string to overwrite configs of server.
|
||||||
|
console.log('?eip=x.x.x.x to overwrite candidate. 覆盖服务器candidate(外网IP)配置');
|
||||||
|
console.log('?api=x to overwrite WebRTC API(1985).');
|
||||||
|
console.log('?schema=http|https to overwrite WebRTC API protocol.');
|
||||||
|
console.log(`?path=xxx to overwrite default ${apiPath}`);
|
||||||
|
|
||||||
|
var server = (!query.server)? window.location.hostname:query.server;
|
||||||
|
var vhost = (!query.vhost)? window.location.hostname:query.vhost;
|
||||||
|
var app = (!query.app)? "live":query.app;
|
||||||
|
var stream = (!query.stream)? "livestream":query.stream;
|
||||||
|
var api = ':' + (query.api || (window.location.protocol === 'http:' ? '1985' : '1990'));
|
||||||
|
const realApiPath = query.path || apiPath;
|
||||||
|
|
||||||
|
var queries = [];
|
||||||
|
if (server !== vhost && vhost !== "__defaultVhost__") {
|
||||||
|
queries.push("vhost=" + vhost);
|
||||||
|
}
|
||||||
|
if (query.schema && window.location.protocol !== query.schema + ':') {
|
||||||
|
queries.push('schema=' + query.schema);
|
||||||
|
}
|
||||||
|
queries = user_extra_params(query, queries, true);
|
||||||
|
|
||||||
|
var uri = window.location.protocol + "//" + server + api + realApiPath + "?app=" + app + "&stream=" + stream + "&" + queries.join('&');
|
||||||
|
while (uri.lastIndexOf("?") === uri.length - 1) {
|
||||||
|
uri = uri.slice(0, uri.length - 1);
|
||||||
|
}
|
||||||
|
while (uri.lastIndexOf("&") === uri.length - 1) {
|
||||||
|
uri = uri.slice(0, uri.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initialize the page.
|
||||||
|
* @param flv_url the div id contains the flv stream url to play
|
||||||
|
* @param hls_url the div id contains the hls stream url to play
|
||||||
|
* @param modal_player the div id contains the modal player
|
||||||
|
*/
|
||||||
|
function srs_init_flv(flv_url, modal_player) {
|
||||||
|
update_nav();
|
||||||
|
if (flv_url) {
|
||||||
|
$(flv_url).val(build_default_flv_url());
|
||||||
|
}
|
||||||
|
if (modal_player) {
|
||||||
|
$(modal_player).width(srs_get_player_modal() + "px");
|
||||||
|
$(modal_player).css("margin-left", "-" + srs_get_player_modal() / 2 +"px");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function srs_init_rtc(id, query) {
|
||||||
|
update_nav();
|
||||||
|
$(id).val(build_default_rtc_url(query));
|
||||||
|
}
|
||||||
|
function srs_init_whip(id, query) {
|
||||||
|
update_nav();
|
||||||
|
$(id).val(build_default_whip_whep_url(query, '/rtc/v1/whip/'));
|
||||||
|
}
|
||||||
|
function srs_init_whep(id, query) {
|
||||||
|
update_nav();
|
||||||
|
$(id).val(build_default_whip_whep_url(query, '/rtc/v1/whep/'));
|
||||||
|
}
|
||||||
382
web/html/js/srs.player.js
Normal file
382
web/html/js/srs.player.js
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
/**
|
||||||
|
* the SrsPlayer object.
|
||||||
|
* @param container the html container id.
|
||||||
|
* @param width a float value specifies the width of player.
|
||||||
|
* @param height a float value specifies the height of player.
|
||||||
|
* @param private_object [optional] an object that used as private object,
|
||||||
|
* for example, the logic chat object which owner this player.
|
||||||
|
* Usage:
|
||||||
|
<script type="text/javascript" src="js/swfobject.js"></script>
|
||||||
|
<script type="text/javascript" src="js/srs.player.js"></script>
|
||||||
|
<div id="player"></div>
|
||||||
|
var p = new SrsPlayer("player", 640, 480);
|
||||||
|
p.set_srs_player_url("srs_player.swf?v=1.0.0");
|
||||||
|
p.on_player_ready = function() {
|
||||||
|
p.set_bt(0.8);
|
||||||
|
p.set_mbt(1.2);
|
||||||
|
p.play("rtmp://ossrs.net/live/livestream");
|
||||||
|
};
|
||||||
|
p.on_player_metadata = function(metadata) {
|
||||||
|
console.log(metadata);
|
||||||
|
console.log(p.dump_log());
|
||||||
|
};
|
||||||
|
p.start();
|
||||||
|
*/
|
||||||
|
function SrsPlayer(container, width, height, private_object) {
|
||||||
|
if (!SrsPlayer.__id) {
|
||||||
|
SrsPlayer.__id = 100;
|
||||||
|
}
|
||||||
|
if (!SrsPlayer.__players) {
|
||||||
|
SrsPlayer.__players = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
SrsPlayer.__players.push(this);
|
||||||
|
|
||||||
|
this.private_object = private_object;
|
||||||
|
this.container = container;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.id = SrsPlayer.__id++;
|
||||||
|
this.stream_url = null;
|
||||||
|
this.buffer_time = 0.3; // default to 0.3
|
||||||
|
this.max_buffer_time = this.buffer_time * 3; // default to 3 x bufferTime.
|
||||||
|
this.volume = 1.0; // default to 100%
|
||||||
|
this.callbackObj = null;
|
||||||
|
this.srs_player_url = "srs_player/release/srs_player.swf?_version="+srs_get_version_code();
|
||||||
|
|
||||||
|
// callback set the following values.
|
||||||
|
this.meatadata = {}; // for on_player_metadata
|
||||||
|
this.time = 0; // current stream time.
|
||||||
|
this.buffer_length = 0; // current stream buffer length.
|
||||||
|
this.kbps = 0; // current stream bitrate(video+audio) in kbps.
|
||||||
|
this.fps = 0; // current stream video fps.
|
||||||
|
this.rtime = 0; // flash relative time in ms.
|
||||||
|
|
||||||
|
this.__fluency = {
|
||||||
|
total_empty_count: 0,
|
||||||
|
total_empty_time: 0,
|
||||||
|
current_empty_time: 0
|
||||||
|
};
|
||||||
|
this.__fluency.on_stream_empty = function(time) {
|
||||||
|
this.total_empty_count++;
|
||||||
|
this.current_empty_time = time;
|
||||||
|
};
|
||||||
|
this.__fluency.on_stream_full = function(time) {
|
||||||
|
if (this.current_empty_time > 0) {
|
||||||
|
this.total_empty_time += time - this.current_empty_time;
|
||||||
|
this.current_empty_time = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.__fluency.calc = function(time) {
|
||||||
|
var den = this.total_empty_count * 4 + this.total_empty_time * 2 + time;
|
||||||
|
if (den > 0) {
|
||||||
|
return time * 100 / den;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* user can set some callback, then start the player.
|
||||||
|
* @param url the default url.
|
||||||
|
* callbacks:
|
||||||
|
* on_player_ready():int, when srs player ready, user can play().
|
||||||
|
* on_player_metadata(metadata:Object):int, when srs player get metadata.
|
||||||
|
* methods:
|
||||||
|
* set_bt(t:Number):void, set the buffer time in seconds.
|
||||||
|
* set_mbt(t:Number):void, set the max buffer time in seconds.
|
||||||
|
* dump_log():String, get all logs of player.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.start = function(url) {
|
||||||
|
if (url) {
|
||||||
|
this.stream_url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// embed the flash.
|
||||||
|
var flashvars = {};
|
||||||
|
flashvars.id = this.id;
|
||||||
|
flashvars.on_player_ready = "__srs_on_player_ready";
|
||||||
|
flashvars.on_player_metadata = "__srs_on_player_metadata";
|
||||||
|
flashvars.on_player_timer = "__srs_on_player_timer";
|
||||||
|
flashvars.on_player_empty = "__srs_on_player_empty";
|
||||||
|
flashvars.on_player_full = "__srs_on_player_full";
|
||||||
|
flashvars.on_player_status = "__srs_on_player_status";
|
||||||
|
|
||||||
|
var params = {};
|
||||||
|
params.wmode = "opaque";
|
||||||
|
params.allowFullScreen = "true";
|
||||||
|
params.allowScriptAccess = "always";
|
||||||
|
|
||||||
|
var attributes = {};
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
swfobject.embedSWF(
|
||||||
|
this.srs_player_url,
|
||||||
|
this.container,
|
||||||
|
this.width, this.height,
|
||||||
|
"11.1.0", "js/AdobeFlashPlayerInstall.swf",
|
||||||
|
flashvars, params, attributes,
|
||||||
|
function(callbackObj){
|
||||||
|
self.callbackObj = callbackObj;
|
||||||
|
if (!callbackObj.success) {
|
||||||
|
console.error('Initialize player failed:'); console.error(callbackObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* play the stream.
|
||||||
|
* @param stream_url the url of stream, rtmp or http.
|
||||||
|
* @param volume the volume, 0 is mute, 1 is 100%, 2 is 200%.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.play = function(url, volume) {
|
||||||
|
this.stop();
|
||||||
|
SrsPlayer.__players.push(this);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
this.stream_url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// volume maybe 0, so never use if(volume) to check its value.
|
||||||
|
if (volume != null && volume != undefined) {
|
||||||
|
this.volume = volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callbackObj.ref.__play(this.stream_url, this.width, this.height, this.buffer_time, this.max_buffer_time, this.volume);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* stop play stream.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.stop = function() {
|
||||||
|
this.callbackObj.ref.__stop();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* pause the play.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.pause = function() {
|
||||||
|
this.callbackObj.ref.__pause();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* resume the play.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.resume = function() {
|
||||||
|
this.callbackObj.ref.__resume();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* get the stream fluency, where 100 is 100%.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.fluency = function() {
|
||||||
|
return this.__fluency.calc(this.rtime);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* get the stream empty count.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.empty_count = function() {
|
||||||
|
return this.__fluency.total_empty_count;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* get all log data.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.dump_log = function() {
|
||||||
|
return this.callbackObj.ref.__dump_log();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* to set the DAR, for example, DAR=16:9 where num=16,den=9.
|
||||||
|
* @param num, for example, 16.
|
||||||
|
* use metadata width if 0.
|
||||||
|
* use user specified width if -1.
|
||||||
|
* @param den, for example, 9.
|
||||||
|
* use metadata height if 0.
|
||||||
|
* use user specified height if -1.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.set_dar = function(num, den) {
|
||||||
|
this.callbackObj.ref.__set_dar(num, den);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* set the fullscreen size data.
|
||||||
|
* @refer the refer fullscreen mode. it can be:
|
||||||
|
* video: use video orignal size.
|
||||||
|
* screen: use screen size to rescale video.
|
||||||
|
* @param percent, the rescale percent, where
|
||||||
|
* 100 means 100%.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.set_fs = function(refer, percent) {
|
||||||
|
this.callbackObj.ref.__set_fs(refer, percent);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* set the stream buffer time in seconds.
|
||||||
|
* @buffer_time the buffer time in seconds.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.set_bt = function(buffer_time) {
|
||||||
|
if (this.buffer_time == buffer_time) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buffer_time = buffer_time;
|
||||||
|
this.callbackObj.ref.__set_bt(buffer_time);
|
||||||
|
|
||||||
|
// reset the max buffer time to 3 x buffer_time.
|
||||||
|
this.set_mbt(buffer_time * 3);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* set the stream max buffer time in seconds.
|
||||||
|
* @param max_buffer_time the max buffer time in seconds.
|
||||||
|
* @remark this is the key feature for realtime communication by flash.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.set_mbt = function(max_buffer_time) {
|
||||||
|
// we must atleast set the max buffer time to 0.6s.
|
||||||
|
max_buffer_time = Math.max(0.6, max_buffer_time);
|
||||||
|
// max buffer time always greater than buffer time.
|
||||||
|
max_buffer_time = Math.max(this.buffer_time, max_buffer_time);
|
||||||
|
|
||||||
|
if (parseInt(this.max_buffer_time * 10) == parseInt(max_buffer_time * 10)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.max_buffer_time = max_buffer_time;
|
||||||
|
this.callbackObj.ref.__set_mbt(max_buffer_time);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* set the srs_player.swf url
|
||||||
|
* @param url, srs_player.swf's url.
|
||||||
|
* @param params, object.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.set_srs_player_url = function(url, params) {
|
||||||
|
var query_array = [],
|
||||||
|
query_string = "",
|
||||||
|
p;
|
||||||
|
params = params || {};
|
||||||
|
params._version = srs_get_version_code();
|
||||||
|
for (p in params) {
|
||||||
|
if (params.hasOwnProperty(p)) {
|
||||||
|
query_array.push(p + "=" + encodeURIComponent(params[p]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query_string = query_array.join("&");
|
||||||
|
this.srs_player_url = url + "?" + query_string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* the callback when player is ready.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.on_player_ready = function() {
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* the callback when player got metadata.
|
||||||
|
* @param metadata the metadata which player got.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.on_player_metadata = function(metadata) {
|
||||||
|
// ignore.
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* the callback when player timer event.
|
||||||
|
* @param time current stream time.
|
||||||
|
* @param buffer_length current buffer length.
|
||||||
|
* @param kbps current video plus audio bitrate in kbps.
|
||||||
|
* @param fps current video fps.
|
||||||
|
* @param rtime current relative time by flash.util.getTimer().
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.on_player_timer = function(time, buffer_length, kbps, fps, rtime) {
|
||||||
|
// ignore.
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* the callback when player got NetStream.Buffer.Empty
|
||||||
|
* @param time current relative time by flash.util.getTimer().
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.on_player_empty = function(time) {
|
||||||
|
// ignore.
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* the callback when player got NetStream.Buffer.Full
|
||||||
|
* @param time current relative time by flash.util.getTimer().
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.on_player_full = function(time) {
|
||||||
|
// ignore.
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* the callback when player status change.
|
||||||
|
* @param code the status code, "init", "connected", "play", "closed", "rejected", "failed".
|
||||||
|
* init => connected/rejected/failed
|
||||||
|
* connected => play/rejected => closed
|
||||||
|
* @param desc the description for the status.
|
||||||
|
*/
|
||||||
|
SrsPlayer.prototype.on_player_status = function(code, desc) {
|
||||||
|
// ignore.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* helpers.
|
||||||
|
*/
|
||||||
|
function __srs_find_player(id) {
|
||||||
|
for (var i = 0; i < SrsPlayer.__players.length; i++) {
|
||||||
|
var player = SrsPlayer.__players[i];
|
||||||
|
|
||||||
|
if (player.id != id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("player not found. id=" + id);
|
||||||
|
}
|
||||||
|
function __srs_on_player_ready(id) {
|
||||||
|
var player = __srs_find_player(id);
|
||||||
|
player.on_player_ready();
|
||||||
|
}
|
||||||
|
function __srs_on_player_metadata(id, metadata) {
|
||||||
|
var player = __srs_find_player(id);
|
||||||
|
|
||||||
|
// user may override the on_player_metadata,
|
||||||
|
// so set the data before invoke it.
|
||||||
|
player.metadata = metadata;
|
||||||
|
|
||||||
|
player.on_player_metadata(metadata);
|
||||||
|
}
|
||||||
|
function __srs_on_player_timer(id, time, buffer_length, kbps, fps, rtime) {
|
||||||
|
var player = __srs_find_player(id);
|
||||||
|
|
||||||
|
buffer_length = Math.max(0, buffer_length);
|
||||||
|
buffer_length = Math.min(player.buffer_time, buffer_length);
|
||||||
|
|
||||||
|
time = Math.max(0, time);
|
||||||
|
|
||||||
|
// user may override the on_player_timer,
|
||||||
|
// so set the data before invoke it.
|
||||||
|
player.time = time;
|
||||||
|
player.buffer_length = buffer_length;
|
||||||
|
player.kbps = kbps;
|
||||||
|
player.fps = fps;
|
||||||
|
player.rtime = rtime;
|
||||||
|
|
||||||
|
player.on_player_timer(time, buffer_length, kbps, fps, rtime);
|
||||||
|
}
|
||||||
|
function __srs_on_player_empty(id, time) {
|
||||||
|
var player = __srs_find_player(id);
|
||||||
|
player.__fluency.on_stream_empty(time);
|
||||||
|
player.on_player_empty(time);
|
||||||
|
}
|
||||||
|
function __srs_on_player_full(id, time) {
|
||||||
|
var player = __srs_find_player(id);
|
||||||
|
player.__fluency.on_stream_full(time);
|
||||||
|
player.on_player_full(time);
|
||||||
|
}
|
||||||
|
function __srs_on_player_status(id, code, desc) {
|
||||||
|
var player = __srs_find_player(id);
|
||||||
|
player.on_player_status(code, desc);
|
||||||
|
|
||||||
|
if (code != "closed") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < SrsPlayer.__players.length; i++) {
|
||||||
|
var player = SrsPlayer.__players[i];
|
||||||
|
|
||||||
|
if (player.id != this.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SrsPlayer.__players.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
173
web/html/js/srs.publisher.js
Normal file
173
web/html/js/srs.publisher.js
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* the SrsPublisher object.
|
||||||
|
* @param container the html container id.
|
||||||
|
* @param width a float value specifies the width of publisher.
|
||||||
|
* @param height a float value specifies the height of publisher.
|
||||||
|
* @param private_object [optional] an object that used as private object,
|
||||||
|
* for example, the logic chat object which owner this publisher.
|
||||||
|
*/
|
||||||
|
function SrsPublisher(container, width, height, private_object) {
|
||||||
|
if (!SrsPublisher.__id) {
|
||||||
|
SrsPublisher.__id = 100;
|
||||||
|
}
|
||||||
|
if (!SrsPublisher.__publishers) {
|
||||||
|
SrsPublisher.__publishers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
SrsPublisher.__publishers.push(this);
|
||||||
|
|
||||||
|
this.private_object = private_object;
|
||||||
|
this.container = container;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.id = SrsPublisher.__id++;
|
||||||
|
this.callbackObj = null;
|
||||||
|
|
||||||
|
// set the values when publish.
|
||||||
|
this.url = null;
|
||||||
|
this.vcodec = {};
|
||||||
|
this.acodec = {};
|
||||||
|
|
||||||
|
// callback set the following values.
|
||||||
|
this.cameras = [];
|
||||||
|
this.microphones = [];
|
||||||
|
this.code = 0;
|
||||||
|
|
||||||
|
// error code defines.
|
||||||
|
this.errors = {
|
||||||
|
"100": "无法获取指定的摄像头。", //error_camera_get
|
||||||
|
"101": "无法获取指定的麦克风。", //error_microphone_get
|
||||||
|
"102": "摄像头为禁用状态,推流时请允许flash访问摄像头。", //error_camera_muted
|
||||||
|
"103": "服务器关闭了连接。", //error_connection_closed
|
||||||
|
"104": "服务器连接失败。", //error_connection_failed
|
||||||
|
"199": "未知错误。"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* user can set some callback, then start the publisher.
|
||||||
|
* callbacks:
|
||||||
|
* on_publisher_ready(cameras, microphones):int, when srs publisher ready, user can publish.
|
||||||
|
* on_publisher_error(code, desc):int, when srs publisher error, callback this method.
|
||||||
|
* on_publisher_warn(code, desc):int, when srs publisher warn, callback this method.
|
||||||
|
*/
|
||||||
|
SrsPublisher.prototype.start = function() {
|
||||||
|
// embed the flash.
|
||||||
|
var flashvars = {};
|
||||||
|
flashvars.id = this.id;
|
||||||
|
flashvars.width = this.width;
|
||||||
|
flashvars.height = this.height;
|
||||||
|
flashvars.on_publisher_ready = "__srs_on_publisher_ready";
|
||||||
|
flashvars.on_publisher_error = "__srs_on_publisher_error";
|
||||||
|
flashvars.on_publisher_warn = "__srs_on_publisher_warn";
|
||||||
|
|
||||||
|
var params = {};
|
||||||
|
params.wmode = "opaque";
|
||||||
|
params.allowFullScreen = "true";
|
||||||
|
params.allowScriptAccess = "always";
|
||||||
|
|
||||||
|
var attributes = {};
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
swfobject.embedSWF(
|
||||||
|
"srs_publisher/release/srs_publisher.swf?_version="+srs_get_version_code(),
|
||||||
|
this.container,
|
||||||
|
this.width, this.height,
|
||||||
|
"11.1.0", "js/AdobeFlashPlayerInstall.swf",
|
||||||
|
flashvars, params, attributes,
|
||||||
|
function(callbackObj){
|
||||||
|
self.callbackObj = callbackObj;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* publish stream to server.
|
||||||
|
* @param url a string indicates the rtmp url to publish.
|
||||||
|
* @param vcodec an object contains the video codec info.
|
||||||
|
* @param acodec an object contains the audio codec info.
|
||||||
|
*/
|
||||||
|
SrsPublisher.prototype.publish = function(url, vcodec, acodec) {
|
||||||
|
this.stop();
|
||||||
|
SrsPublisher.__publishers.push(this);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
if (vcodec) {
|
||||||
|
this.vcodec = vcodec;
|
||||||
|
}
|
||||||
|
if (acodec) {
|
||||||
|
this.acodec = acodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callbackObj.ref.__publish(this.url, this.width, this.height, this.vcodec, this.acodec);
|
||||||
|
}
|
||||||
|
SrsPublisher.prototype.stop = function() {
|
||||||
|
for (var i = 0; i < SrsPublisher.__publishers.length; i++) {
|
||||||
|
var player = SrsPublisher.__publishers[i];
|
||||||
|
|
||||||
|
if (player.id != this.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SrsPublisher.__publishers.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callbackObj.ref.__stop();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* when publisher ready.
|
||||||
|
* @param cameras a string array contains the names of cameras.
|
||||||
|
* @param microphones a string array contains the names of microphones.
|
||||||
|
*/
|
||||||
|
SrsPublisher.prototype.on_publisher_ready = function(cameras, microphones) {
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* when publisher error.
|
||||||
|
* @code the error code.
|
||||||
|
* @desc the error desc message.
|
||||||
|
*/
|
||||||
|
SrsPublisher.prototype.on_publisher_error = function(code, desc) {
|
||||||
|
throw new Error("publisher error. code=" + code + ", desc=" + desc);
|
||||||
|
}
|
||||||
|
SrsPublisher.prototype.on_publisher_warn = function(code, desc) {
|
||||||
|
throw new Error("publisher warn. code=" + code + ", desc=" + desc);
|
||||||
|
}
|
||||||
|
function __srs_find_publisher(id) {
|
||||||
|
for (var i = 0; i < SrsPublisher.__publishers.length; i++) {
|
||||||
|
var publisher = SrsPublisher.__publishers[i];
|
||||||
|
|
||||||
|
if (publisher.id != id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return publisher;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("publisher not found. id=" + id);
|
||||||
|
}
|
||||||
|
function __srs_on_publisher_ready(id, cameras, microphones) {
|
||||||
|
var publisher = __srs_find_publisher(id);
|
||||||
|
|
||||||
|
publisher.cameras = cameras;
|
||||||
|
publisher.microphones = microphones;
|
||||||
|
|
||||||
|
publisher.on_publisher_ready(cameras, microphones);
|
||||||
|
}
|
||||||
|
function __srs_on_publisher_error(id, code) {
|
||||||
|
var publisher = __srs_find_publisher(id);
|
||||||
|
|
||||||
|
publisher.code = code;
|
||||||
|
|
||||||
|
publisher.on_publisher_error(code, publisher.errors[""+code]);
|
||||||
|
}
|
||||||
|
function __srs_on_publisher_warn(id, code) {
|
||||||
|
var publisher = __srs_find_publisher(id);
|
||||||
|
|
||||||
|
publisher.code = code;
|
||||||
|
|
||||||
|
publisher.on_publisher_warn(code, publisher.errors[""+code]);
|
||||||
|
}
|
||||||
681
web/html/js/srs.sdk.js
Normal file
681
web/html/js/srs.sdk.js
Normal file
@ -0,0 +1,681 @@
|
|||||||
|
|
||||||
|
//
|
||||||
|
// Copyright (c) 2013-2021 Winlin
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function SrsError(name, message) {
|
||||||
|
this.name = name;
|
||||||
|
this.message = message;
|
||||||
|
this.stack = (new Error()).stack;
|
||||||
|
}
|
||||||
|
SrsError.prototype = Object.create(Error.prototype);
|
||||||
|
SrsError.prototype.constructor = SrsError;
|
||||||
|
|
||||||
|
// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
|
||||||
|
// Async-awat-prmise based SRS RTC Publisher.
|
||||||
|
function SrsRtcPublisherAsync() {
|
||||||
|
var self = {};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||||
|
self.constraints = {
|
||||||
|
audio: true,
|
||||||
|
video: {
|
||||||
|
width: {ideal: 320, max: 576}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
// @url The WebRTC url to play with, for example:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream
|
||||||
|
// or specifies the API port:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/livestream
|
||||||
|
// or autostart the publish:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?autostart=true
|
||||||
|
// or change the app from live to myapp:
|
||||||
|
// webrtc://r.ossrs.net:11985/myapp/livestream
|
||||||
|
// or change the stream from livestream to mystream:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/mystream
|
||||||
|
// or set the api server to myapi.domain.com:
|
||||||
|
// webrtc://myapi.domain.com/live/livestream
|
||||||
|
// or set the candidate(eip) of answer:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?candidate=39.107.238.185
|
||||||
|
// or force to access https API:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?schema=https
|
||||||
|
// or use plaintext, without SRTP:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?encrypt=false
|
||||||
|
// or any other information, will pass-by in the query:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?vhost=xxx
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?token=xxx
|
||||||
|
self.publish = async function (url) {
|
||||||
|
var conf = self.__internal.prepareUrl(url);
|
||||||
|
self.pc.addTransceiver("audio", {direction: "sendonly"});
|
||||||
|
self.pc.addTransceiver("video", {direction: "sendonly"});
|
||||||
|
//self.pc.addTransceiver("video", {direction: "sendonly"});
|
||||||
|
//self.pc.addTransceiver("audio", {direction: "sendonly"});
|
||||||
|
|
||||||
|
if (!navigator.mediaDevices && window.location.protocol === 'http:' && window.location.hostname !== 'localhost') {
|
||||||
|
throw new SrsError('HttpsRequiredError', `Please use HTTPS or localhost to publish, read https://github.com/ossrs/srs/issues/2762#issuecomment-983147576`);
|
||||||
|
}
|
||||||
|
var stream = await navigator.mediaDevices.getUserMedia(self.constraints);
|
||||||
|
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
stream.getTracks().forEach(function (track) {
|
||||||
|
self.pc.addTrack(track);
|
||||||
|
|
||||||
|
// Notify about local track when stream is ok.
|
||||||
|
self.ontrack && self.ontrack({track: track});
|
||||||
|
});
|
||||||
|
|
||||||
|
var offer = await self.pc.createOffer();
|
||||||
|
await self.pc.setLocalDescription(offer);
|
||||||
|
var session = await new Promise(function (resolve, reject) {
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var data = {
|
||||||
|
api: conf.apiUrl, tid: conf.tid, streamurl: conf.streamUrl,
|
||||||
|
clientip: null, sdp: offer.sdp
|
||||||
|
};
|
||||||
|
console.log("Generated offer: ", data);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.readyState !== xhr.DONE) return;
|
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
|
||||||
|
const data = JSON.parse(xhr.responseText);
|
||||||
|
console.log("Got answer: ", data);
|
||||||
|
return data.code ? reject(xhr) : resolve(data);
|
||||||
|
}
|
||||||
|
xhr.open('POST', conf.apiUrl, true);
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/json');
|
||||||
|
xhr.send(JSON.stringify(data));
|
||||||
|
});
|
||||||
|
await self.pc.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({type: 'answer', sdp: session.sdp})
|
||||||
|
);
|
||||||
|
session.simulator = conf.schema + '//' + conf.urlObject.server + ':' + conf.port + '/rtc/v1/nack/';
|
||||||
|
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close the publisher.
|
||||||
|
self.close = function () {
|
||||||
|
self.pc && self.pc.close();
|
||||||
|
self.pc = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The callback when got local stream.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
self.ontrack = function (event) {
|
||||||
|
// Add track to stream of SDK.
|
||||||
|
self.stream.addTrack(event.track);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Internal APIs.
|
||||||
|
self.__internal = {
|
||||||
|
defaultPath: '/rtc/v1/publish/',
|
||||||
|
prepareUrl: function (webrtcUrl) {
|
||||||
|
var urlObject = self.__internal.parse(webrtcUrl);
|
||||||
|
|
||||||
|
// If user specifies the schema, use it as API schema.
|
||||||
|
var schema = urlObject.user_query.schema;
|
||||||
|
schema = schema ? schema + ':' : window.location.protocol;
|
||||||
|
|
||||||
|
var port = urlObject.port || 1985;
|
||||||
|
if (schema === 'https:') {
|
||||||
|
port = urlObject.port || 443;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var api = urlObject.user_query.play || self.__internal.defaultPath;
|
||||||
|
if (api.lastIndexOf('/') !== api.length - 1) {
|
||||||
|
api += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiUrl = schema + '//' + urlObject.server + ':' + port + api;
|
||||||
|
for (var key in urlObject.user_query) {
|
||||||
|
if (key !== 'api' && key !== 'play') {
|
||||||
|
apiUrl += '&' + key + '=' + urlObject.user_query[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Replace /rtc/v1/play/&k=v to /rtc/v1/play/?k=v
|
||||||
|
apiUrl = apiUrl.replace(api + '&', api + '?');
|
||||||
|
|
||||||
|
var streamUrl = urlObject.url;
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiUrl: apiUrl, streamUrl: streamUrl, schema: schema, urlObject: urlObject, port: port,
|
||||||
|
tid: Number(parseInt(new Date().getTime()*Math.random()*100)).toString(16).slice(0, 7)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
parse: function (url) {
|
||||||
|
// @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.href = url.replace("rtmp://", "http://")
|
||||||
|
.replace("webrtc://", "http://")
|
||||||
|
.replace("rtc://", "http://");
|
||||||
|
|
||||||
|
var vhost = a.hostname;
|
||||||
|
var app = a.pathname.substring(1, a.pathname.lastIndexOf("/"));
|
||||||
|
var stream = a.pathname.slice(a.pathname.lastIndexOf("/") + 1);
|
||||||
|
|
||||||
|
// parse the vhost in the params of app, that srs supports.
|
||||||
|
app = app.replace("...vhost...", "?vhost=");
|
||||||
|
if (app.indexOf("?") >= 0) {
|
||||||
|
var params = app.slice(app.indexOf("?"));
|
||||||
|
app = app.slice(0, app.indexOf("?"));
|
||||||
|
|
||||||
|
if (params.indexOf("vhost=") > 0) {
|
||||||
|
vhost = params.slice(params.indexOf("vhost=") + "vhost=".length);
|
||||||
|
if (vhost.indexOf("&") > 0) {
|
||||||
|
vhost = vhost.slice(0, vhost.indexOf("&"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when vhost equals to server, and server is ip,
|
||||||
|
// the vhost is __defaultVhost__
|
||||||
|
if (a.hostname === vhost) {
|
||||||
|
var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
|
||||||
|
if (re.test(a.hostname)) {
|
||||||
|
vhost = "__defaultVhost__";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the schema
|
||||||
|
var schema = "rtmp";
|
||||||
|
if (url.indexOf("://") > 0) {
|
||||||
|
schema = url.slice(0, url.indexOf("://"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = a.port;
|
||||||
|
if (!port) {
|
||||||
|
// Finger out by webrtc url, if contains http or https port, to overwrite default 1985.
|
||||||
|
if (schema === 'webrtc' && url.indexOf(`webrtc://${a.host}:`) === 0) {
|
||||||
|
port = (url.indexOf(`webrtc://${a.host}:80`) === 0) ? 80 : 443;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guess by schema.
|
||||||
|
if (schema === 'http') {
|
||||||
|
port = 80;
|
||||||
|
} else if (schema === 'https') {
|
||||||
|
port = 443;
|
||||||
|
} else if (schema === 'rtmp') {
|
||||||
|
port = 1935;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = {
|
||||||
|
url: url,
|
||||||
|
schema: schema,
|
||||||
|
server: a.hostname, port: port,
|
||||||
|
vhost: vhost, app: app, stream: stream
|
||||||
|
};
|
||||||
|
self.__internal.fill_query(a.search, ret);
|
||||||
|
|
||||||
|
// For webrtc API, we use 443 if page is https, or schema specified it.
|
||||||
|
if (!ret.port) {
|
||||||
|
if (schema === 'webrtc' || schema === 'rtc') {
|
||||||
|
if (ret.user_query.schema === 'https') {
|
||||||
|
ret.port = 443;
|
||||||
|
} else if (window.location.href.indexOf('https://') === 0) {
|
||||||
|
ret.port = 443;
|
||||||
|
} else {
|
||||||
|
// For WebRTC, SRS use 1985 as default API port.
|
||||||
|
ret.port = 1985;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
fill_query: function (query_string, obj) {
|
||||||
|
// pure user query object.
|
||||||
|
obj.user_query = {};
|
||||||
|
|
||||||
|
if (query_string.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split again for angularjs.
|
||||||
|
if (query_string.indexOf("?") >= 0) {
|
||||||
|
query_string = query_string.split("?")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var queries = query_string.split("&");
|
||||||
|
for (var i = 0; i < queries.length; i++) {
|
||||||
|
var elem = queries[i];
|
||||||
|
|
||||||
|
var query = elem.split("=");
|
||||||
|
obj[query[0]] = query[1];
|
||||||
|
obj.user_query[query[0]] = query[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// alias domain for vhost.
|
||||||
|
if (obj.domain) {
|
||||||
|
obj.vhost = obj.domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pc = new RTCPeerConnection(null);
|
||||||
|
|
||||||
|
// To keep api consistent between player and publisher.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
// @see https://webrtc.org/getting-started/media-devices
|
||||||
|
self.stream = new MediaStream();
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
|
||||||
|
// Async-await-promise based SRS RTC Player.
|
||||||
|
function SrsRtcPlayerAsync() {
|
||||||
|
var self = {};
|
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
// @url The WebRTC url to play with, for example:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream
|
||||||
|
// or specifies the API port:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/livestream
|
||||||
|
// webrtc://r.ossrs.net:80/live/livestream
|
||||||
|
// or autostart the play:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?autostart=true
|
||||||
|
// or change the app from live to myapp:
|
||||||
|
// webrtc://r.ossrs.net:11985/myapp/livestream
|
||||||
|
// or change the stream from livestream to mystream:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/mystream
|
||||||
|
// or set the api server to myapi.domain.com:
|
||||||
|
// webrtc://myapi.domain.com/live/livestream
|
||||||
|
// or set the candidate(eip) of answer:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?candidate=39.107.238.185
|
||||||
|
// or force to access https API:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?schema=https
|
||||||
|
// or use plaintext, without SRTP:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?encrypt=false
|
||||||
|
// or any other information, will pass-by in the query:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?vhost=xxx
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?token=xxx
|
||||||
|
self.play = async function(url) {
|
||||||
|
var conf = self.__internal.prepareUrl(url);
|
||||||
|
self.pc.addTransceiver("audio", {direction: "recvonly"});
|
||||||
|
self.pc.addTransceiver("video", {direction: "recvonly"});
|
||||||
|
//self.pc.addTransceiver("video", {direction: "recvonly"});
|
||||||
|
//self.pc.addTransceiver("audio", {direction: "recvonly"});
|
||||||
|
|
||||||
|
var offer = await self.pc.createOffer();
|
||||||
|
await self.pc.setLocalDescription(offer);
|
||||||
|
var session = await new Promise(function(resolve, reject) {
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var data = {
|
||||||
|
api: conf.apiUrl, tid: conf.tid, streamurl: conf.streamUrl,
|
||||||
|
clientip: null, sdp: offer.sdp
|
||||||
|
};
|
||||||
|
console.log("Generated offer: ", data);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.readyState !== xhr.DONE) return;
|
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
|
||||||
|
const data = JSON.parse(xhr.responseText);
|
||||||
|
console.log("Got answer: ", data);
|
||||||
|
return data.code ? reject(xhr) : resolve(data);
|
||||||
|
}
|
||||||
|
xhr.open('POST', conf.apiUrl, true);
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/json');
|
||||||
|
xhr.send(JSON.stringify(data));
|
||||||
|
});
|
||||||
|
await self.pc.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({type: 'answer', sdp: session.sdp})
|
||||||
|
);
|
||||||
|
session.simulator = conf.schema + '//' + conf.urlObject.server + ':' + conf.port + '/rtc/v1/nack/';
|
||||||
|
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close the player.
|
||||||
|
self.close = function() {
|
||||||
|
self.pc && self.pc.close();
|
||||||
|
self.pc = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The callback when got remote track.
|
||||||
|
// Note that the onaddstream is deprecated, @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onaddstream
|
||||||
|
self.ontrack = function (event) {
|
||||||
|
// https://webrtc.org/getting-started/remote-streams
|
||||||
|
self.stream.addTrack(event.track);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Internal APIs.
|
||||||
|
self.__internal = {
|
||||||
|
defaultPath: '/rtc/v1/play/',
|
||||||
|
prepareUrl: function (webrtcUrl) {
|
||||||
|
var urlObject = self.__internal.parse(webrtcUrl);
|
||||||
|
|
||||||
|
// If user specifies the schema, use it as API schema.
|
||||||
|
var schema = urlObject.user_query.schema;
|
||||||
|
schema = schema ? schema + ':' : window.location.protocol;
|
||||||
|
|
||||||
|
var port = urlObject.port || 1985;
|
||||||
|
if (schema === 'https:') {
|
||||||
|
port = urlObject.port || 443;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var api = urlObject.user_query.play || self.__internal.defaultPath;
|
||||||
|
if (api.lastIndexOf('/') !== api.length - 1) {
|
||||||
|
api += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiUrl = schema + '//' + urlObject.server + ':' + port + api;
|
||||||
|
for (var key in urlObject.user_query) {
|
||||||
|
if (key !== 'api' && key !== 'play') {
|
||||||
|
apiUrl += '&' + key + '=' + urlObject.user_query[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Replace /rtc/v1/play/&k=v to /rtc/v1/play/?k=v
|
||||||
|
apiUrl = apiUrl.replace(api + '&', api + '?');
|
||||||
|
|
||||||
|
var streamUrl = urlObject.url;
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiUrl: apiUrl, streamUrl: streamUrl, schema: schema, urlObject: urlObject, port: port,
|
||||||
|
tid: Number(parseInt(new Date().getTime()*Math.random()*100)).toString(16).slice(0, 7)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
parse: function (url) {
|
||||||
|
// @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.href = url.replace("rtmp://", "http://")
|
||||||
|
.replace("webrtc://", "http://")
|
||||||
|
.replace("rtc://", "http://");
|
||||||
|
|
||||||
|
var vhost = a.hostname;
|
||||||
|
var app = a.pathname.substring(1, a.pathname.lastIndexOf("/"));
|
||||||
|
var stream = a.pathname.slice(a.pathname.lastIndexOf("/") + 1);
|
||||||
|
|
||||||
|
// parse the vhost in the params of app, that srs supports.
|
||||||
|
app = app.replace("...vhost...", "?vhost=");
|
||||||
|
if (app.indexOf("?") >= 0) {
|
||||||
|
var params = app.slice(app.indexOf("?"));
|
||||||
|
app = app.slice(0, app.indexOf("?"));
|
||||||
|
|
||||||
|
if (params.indexOf("vhost=") > 0) {
|
||||||
|
vhost = params.slice(params.indexOf("vhost=") + "vhost=".length);
|
||||||
|
if (vhost.indexOf("&") > 0) {
|
||||||
|
vhost = vhost.slice(0, vhost.indexOf("&"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when vhost equals to server, and server is ip,
|
||||||
|
// the vhost is __defaultVhost__
|
||||||
|
if (a.hostname === vhost) {
|
||||||
|
var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
|
||||||
|
if (re.test(a.hostname)) {
|
||||||
|
vhost = "__defaultVhost__";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the schema
|
||||||
|
var schema = "rtmp";
|
||||||
|
if (url.indexOf("://") > 0) {
|
||||||
|
schema = url.slice(0, url.indexOf("://"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = a.port;
|
||||||
|
if (!port) {
|
||||||
|
// Finger out by webrtc url, if contains http or https port, to overwrite default 1985.
|
||||||
|
if (schema === 'webrtc' && url.indexOf(`webrtc://${a.host}:`) === 0) {
|
||||||
|
port = (url.indexOf(`webrtc://${a.host}:80`) === 0) ? 80 : 443;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guess by schema.
|
||||||
|
if (schema === 'http') {
|
||||||
|
port = 80;
|
||||||
|
} else if (schema === 'https') {
|
||||||
|
port = 443;
|
||||||
|
} else if (schema === 'rtmp') {
|
||||||
|
port = 1935;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = {
|
||||||
|
url: url,
|
||||||
|
schema: schema,
|
||||||
|
server: a.hostname, port: port,
|
||||||
|
vhost: vhost, app: app, stream: stream
|
||||||
|
};
|
||||||
|
self.__internal.fill_query(a.search, ret);
|
||||||
|
|
||||||
|
// For webrtc API, we use 443 if page is https, or schema specified it.
|
||||||
|
if (!ret.port) {
|
||||||
|
if (schema === 'webrtc' || schema === 'rtc') {
|
||||||
|
if (ret.user_query.schema === 'https') {
|
||||||
|
ret.port = 443;
|
||||||
|
} else if (window.location.href.indexOf('https://') === 0) {
|
||||||
|
ret.port = 443;
|
||||||
|
} else {
|
||||||
|
// For WebRTC, SRS use 1985 as default API port.
|
||||||
|
ret.port = 1985;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
fill_query: function (query_string, obj) {
|
||||||
|
// pure user query object.
|
||||||
|
obj.user_query = {};
|
||||||
|
|
||||||
|
if (query_string.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split again for angularjs.
|
||||||
|
if (query_string.indexOf("?") >= 0) {
|
||||||
|
query_string = query_string.split("?")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var queries = query_string.split("&");
|
||||||
|
for (var i = 0; i < queries.length; i++) {
|
||||||
|
var elem = queries[i];
|
||||||
|
|
||||||
|
var query = elem.split("=");
|
||||||
|
obj[query[0]] = query[1];
|
||||||
|
obj.user_query[query[0]] = query[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// alias domain for vhost.
|
||||||
|
if (obj.domain) {
|
||||||
|
obj.vhost = obj.domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pc = new RTCPeerConnection(null);
|
||||||
|
|
||||||
|
// Create a stream to add track to the stream, @see https://webrtc.org/getting-started/remote-streams
|
||||||
|
self.stream = new MediaStream();
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
|
||||||
|
self.pc.ontrack = function(event) {
|
||||||
|
if (self.ontrack) {
|
||||||
|
self.ontrack(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
|
||||||
|
// Async-awat-prmise based SRS RTC Publisher by WHIP.
|
||||||
|
function SrsRtcWhipWhepAsync() {
|
||||||
|
var self = {};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||||
|
self.constraints = {
|
||||||
|
audio: true,
|
||||||
|
video: {
|
||||||
|
width: {ideal: 320, max: 576}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// See https://datatracker.ietf.org/doc/draft-ietf-wish-whip/
|
||||||
|
// @url The WebRTC url to publish with, for example:
|
||||||
|
// http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream
|
||||||
|
self.publish = async function (url) {
|
||||||
|
if (url.indexOf('/whip/') === -1) throw new Error(`invalid WHIP url ${url}`);
|
||||||
|
|
||||||
|
self.pc.addTransceiver("audio", {direction: "sendonly"});
|
||||||
|
self.pc.addTransceiver("video", {direction: "sendonly"});
|
||||||
|
|
||||||
|
if (!navigator.mediaDevices && window.location.protocol === 'http:' && window.location.hostname !== 'localhost') {
|
||||||
|
throw new SrsError('HttpsRequiredError', `Please use HTTPS or localhost to publish, read https://github.com/ossrs/srs/issues/2762#issuecomment-983147576`);
|
||||||
|
}
|
||||||
|
var stream = await navigator.mediaDevices.getUserMedia(self.constraints);
|
||||||
|
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
stream.getTracks().forEach(function (track) {
|
||||||
|
self.pc.addTrack(track);
|
||||||
|
|
||||||
|
// Notify about local track when stream is ok.
|
||||||
|
self.ontrack && self.ontrack({track: track});
|
||||||
|
});
|
||||||
|
|
||||||
|
var offer = await self.pc.createOffer();
|
||||||
|
await self.pc.setLocalDescription(offer);
|
||||||
|
const answer = await new Promise(function (resolve, reject) {
|
||||||
|
console.log("Generated offer: ", offer);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.readyState !== xhr.DONE) return;
|
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
|
||||||
|
const data = xhr.responseText;
|
||||||
|
console.log("Got answer: ", data);
|
||||||
|
return data.code ? reject(xhr) : resolve(data);
|
||||||
|
}
|
||||||
|
xhr.open('POST', url, true);
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/sdp');
|
||||||
|
xhr.send(offer.sdp);
|
||||||
|
});
|
||||||
|
await self.pc.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({type: 'answer', sdp: answer})
|
||||||
|
);
|
||||||
|
|
||||||
|
return self.__internal.parseId(url, offer.sdp, answer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// See https://datatracker.ietf.org/doc/draft-ietf-wish-whip/
|
||||||
|
// @url The WebRTC url to play with, for example:
|
||||||
|
// http://localhost:1985/rtc/v1/whep/?app=live&stream=livestream
|
||||||
|
self.play = async function(url) {
|
||||||
|
if (url.indexOf('/whip-play/') === -1 && url.indexOf('/whep/') === -1) throw new Error(`invalid WHEP url ${url}`);
|
||||||
|
|
||||||
|
self.pc.addTransceiver("audio", {direction: "recvonly"});
|
||||||
|
self.pc.addTransceiver("video", {direction: "recvonly"});
|
||||||
|
|
||||||
|
var offer = await self.pc.createOffer();
|
||||||
|
await self.pc.setLocalDescription(offer);
|
||||||
|
const answer = await new Promise(function(resolve, reject) {
|
||||||
|
console.log("Generated offer: ", offer);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.readyState !== xhr.DONE) return;
|
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
|
||||||
|
const data = xhr.responseText;
|
||||||
|
console.log("Got answer: ", data);
|
||||||
|
return data.code ? reject(xhr) : resolve(data);
|
||||||
|
}
|
||||||
|
xhr.open('POST', url, true);
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/sdp');
|
||||||
|
xhr.send(offer.sdp);
|
||||||
|
});
|
||||||
|
await self.pc.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({type: 'answer', sdp: answer})
|
||||||
|
);
|
||||||
|
|
||||||
|
return self.__internal.parseId(url, offer.sdp, answer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close the publisher.
|
||||||
|
self.close = function () {
|
||||||
|
self.pc && self.pc.close();
|
||||||
|
self.pc = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The callback when got local stream.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
self.ontrack = function (event) {
|
||||||
|
// Add track to stream of SDK.
|
||||||
|
self.stream.addTrack(event.track);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pc = new RTCPeerConnection(null);
|
||||||
|
|
||||||
|
// To keep api consistent between player and publisher.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
// @see https://webrtc.org/getting-started/media-devices
|
||||||
|
self.stream = new MediaStream();
|
||||||
|
|
||||||
|
// Internal APIs.
|
||||||
|
self.__internal = {
|
||||||
|
parseId: (url, offer, answer) => {
|
||||||
|
let sessionid = offer.substr(offer.indexOf('a=ice-ufrag:') + 'a=ice-ufrag:'.length);
|
||||||
|
sessionid = sessionid.substr(0, sessionid.indexOf('\n') - 1) + ':';
|
||||||
|
sessionid += answer.substr(answer.indexOf('a=ice-ufrag:') + 'a=ice-ufrag:'.length);
|
||||||
|
sessionid = sessionid.substr(0, sessionid.indexOf('\n'));
|
||||||
|
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
return {
|
||||||
|
sessionid: sessionid, // Should be ice-ufrag of answer:offer.
|
||||||
|
simulator: a.protocol + '//' + a.host + '/rtc/v1/nack/',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
|
||||||
|
self.pc.ontrack = function(event) {
|
||||||
|
if (self.ontrack) {
|
||||||
|
self.ontrack(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the codec of RTCRtpSender, kind(audio/video) is optional filter.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/WebRTC_codecs#getting_the_supported_codecs
|
||||||
|
function SrsRtcFormatSenders(senders, kind) {
|
||||||
|
var codecs = [];
|
||||||
|
senders.forEach(function (sender) {
|
||||||
|
var params = sender.getParameters();
|
||||||
|
params && params.codecs && params.codecs.forEach(function(c) {
|
||||||
|
if (kind && sender.track.kind !== kind) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.mimeType.indexOf('/red') > 0 || c.mimeType.indexOf('/rtx') > 0 || c.mimeType.indexOf('/fec') > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var s = '';
|
||||||
|
|
||||||
|
s += c.mimeType.replace('audio/', '').replace('video/', '');
|
||||||
|
s += ', ' + c.clockRate + 'HZ';
|
||||||
|
if (sender.track.kind === "audio") {
|
||||||
|
s += ', channels: ' + c.channels;
|
||||||
|
}
|
||||||
|
s += ', pt: ' + c.payloadType;
|
||||||
|
|
||||||
|
codecs.push(s);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return codecs.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
8
web/html/js/srs.utility.js
Normal file
8
web/html/js/srs.utility.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* parse the rtmp url,
|
||||||
|
* for example: rtmp://demo.srs.com:1935/live...vhost...players/livestream
|
||||||
|
* @return object {server, port, vhost, app, stream}
|
||||||
|
*/
|
||||||
|
function srs_parse_rtmp_url(rtmp_url) {
|
||||||
|
return parse_rtmp_url(rtmp_url);
|
||||||
|
}
|
||||||
4
web/html/js/swfobject.js
Normal file
4
web/html/js/swfobject.js
Normal file
File diff suppressed because one or more lines are too long
686
web/html/js/winlin.utility.js
Normal file
686
web/html/js/winlin.utility.js
Normal file
@ -0,0 +1,686 @@
|
|||||||
|
// winlin.utility.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* common utilities
|
||||||
|
* depends: jquery1.10
|
||||||
|
* https://gitee.com/winlinvip/codes/rpn0c2ewbomj81augzk4y59
|
||||||
|
* @see: http://blog.csdn.net/win_lin/article/details/17994347
|
||||||
|
* v 1.0.23
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* padding the output.
|
||||||
|
* padding(3, 5, '0') is 00003
|
||||||
|
* padding(3, 5, 'x') is xxxx3
|
||||||
|
* @see http://blog.csdn.net/win_lin/article/details/12065413
|
||||||
|
*/
|
||||||
|
function padding(number, length, prefix) {
|
||||||
|
if(String(number).length >= length){
|
||||||
|
return String(number);
|
||||||
|
}
|
||||||
|
return padding(prefix+number, length, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* extends system array, to remove all specified elem.
|
||||||
|
* @param arr the array to remove elem from.
|
||||||
|
* @param elem the elem to remove.
|
||||||
|
* @remark all elem will be removed.
|
||||||
|
* for example,
|
||||||
|
* arr = [10, 15, 20, 30, 20, 40]
|
||||||
|
* system_array_remove(arr, 10) // arr=[15, 20, 30, 20, 40]
|
||||||
|
* system_array_remove(arr, 20) // arr=[15, 30, 40]
|
||||||
|
*/
|
||||||
|
function system_array_remove(arr, elem) {
|
||||||
|
if (!arr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var removed = true;
|
||||||
|
var i = 0;
|
||||||
|
while (removed) {
|
||||||
|
removed = false;
|
||||||
|
for (; i < arr.length; i++) {
|
||||||
|
if (elem == arr[i]) {
|
||||||
|
arr.splice(i, 1);
|
||||||
|
removed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether the array contains specified element.
|
||||||
|
* @param arr the array to find.
|
||||||
|
* @param elem_or_function the element value or compare function.
|
||||||
|
* @returns true contains elem; otherwise false.
|
||||||
|
* for example,
|
||||||
|
* arr = [10, 15, 20, 30, 20, 40]
|
||||||
|
* system_array_contains(arr, 10) // true
|
||||||
|
* system_array_contains(arr, 11) // false
|
||||||
|
* system_array_contains(arr, function(elem){return elem == 30;}); // true
|
||||||
|
* system_array_contains(arr, function(elem){return elem == 60;}); // false
|
||||||
|
*/
|
||||||
|
function system_array_contains(arr, elem_or_function) {
|
||||||
|
return system_array_get(arr, elem_or_function) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the specified element from array
|
||||||
|
* @param arr the array to find.
|
||||||
|
* @param elem_or_function the element value or compare function.
|
||||||
|
* @returns the matched elem; otherwise null.
|
||||||
|
* for example,
|
||||||
|
* arr = [10, 15, 20, 30, 20, 40]
|
||||||
|
* system_array_get(arr, 10) // 10
|
||||||
|
* system_array_get(arr, 11) // null
|
||||||
|
* system_array_get(arr, function(elem){return elem == 30;}); // 30
|
||||||
|
* system_array_get(arr, function(elem){return elem == 60;}); // null
|
||||||
|
*/
|
||||||
|
function system_array_get(arr, elem_or_function) {
|
||||||
|
for (var i = 0; i < arr.length; i++) {
|
||||||
|
if (typeof elem_or_function == "function") {
|
||||||
|
if (elem_or_function(arr[i])) {
|
||||||
|
return arr[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (elem_or_function == arr[i]) {
|
||||||
|
return arr[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* to iterate on array.
|
||||||
|
* @param arr the array to iterate on.
|
||||||
|
* @param pfn the function to apply on it. return false to break loop.
|
||||||
|
* for example,
|
||||||
|
* arr = [10, 15, 20, 30, 20, 40]
|
||||||
|
* system_array_foreach(arr, function(elem, index){
|
||||||
|
* console.log('index=' + index + ',elem=' + elem);
|
||||||
|
* });
|
||||||
|
* @return true when iterate all elems.
|
||||||
|
*/
|
||||||
|
function system_array_foreach(arr, pfn) {
|
||||||
|
if (!pfn) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < arr.length; i++) {
|
||||||
|
if (!pfn(arr[i], i)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether the str starts with flag.
|
||||||
|
*/
|
||||||
|
function system_string_startswith(str, flag) {
|
||||||
|
if (typeof flag == "object" && flag.constructor == Array) {
|
||||||
|
for (var i = 0; i < flag.length; i++) {
|
||||||
|
if (system_string_startswith(str, flag[i])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str && flag && str.length >= flag.length && str.indexOf(flag) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether the str ends with flag.
|
||||||
|
*/
|
||||||
|
function system_string_endswith(str, flag) {
|
||||||
|
if (typeof flag == "object" && flag.constructor == Array) {
|
||||||
|
for (var i = 0; i < flag.length; i++) {
|
||||||
|
if (system_string_endswith(str, flag[i])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str && flag && str.length >= flag.length && str.indexOf(flag) == str.length - flag.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* trim the start and end of flag in str.
|
||||||
|
* @param flag a string to trim.
|
||||||
|
*/
|
||||||
|
function system_string_trim(str, flag) {
|
||||||
|
if (!flag || !flag.length || typeof flag != "string") {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (system_string_startswith(str, flag)) {
|
||||||
|
str = str.slice(flag.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (system_string_endswith(str, flag)) {
|
||||||
|
str = str.slice(0, str.length - flag.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* array sort asc, for example:
|
||||||
|
* [a, b] in [10, 11, 9]
|
||||||
|
* then sort to: [9, 10, 11]
|
||||||
|
* Usage, for example:
|
||||||
|
obj.data.data.sort(function(a, b){
|
||||||
|
return array_sort_asc(a.metadata.meta_id, b.metadata.meta_id);
|
||||||
|
});
|
||||||
|
* @see: http://blog.csdn.net/win_lin/article/details/17994347
|
||||||
|
* @remark, if need desc, use -1*array_sort_asc(a,b)
|
||||||
|
*/
|
||||||
|
function array_sort_asc(elem_a, elem_b) {
|
||||||
|
if (elem_a > elem_b) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return (elem_a < elem_b)? -1 : 0;
|
||||||
|
}
|
||||||
|
function array_sort_desc(elem_a, elem_b) {
|
||||||
|
return -1 * array_sort_asc(elem_a, elem_b);
|
||||||
|
}
|
||||||
|
function system_array_sort_asc(elem_a, elem_b) {
|
||||||
|
return array_sort_asc(elem_a, elem_b);
|
||||||
|
}
|
||||||
|
function system_array_sort_desc(elem_a, elem_b) {
|
||||||
|
return -1 * array_sort_asc(elem_a, elem_b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parse the query string to object.
|
||||||
|
* parse the url location object as: host(hostname:http_port), pathname(dir/filename)
|
||||||
|
* for example, url http://192.168.1.168:1980/ui/players.html?vhost=player.vhost.com&app=test&stream=livestream
|
||||||
|
* parsed to object:
|
||||||
|
{
|
||||||
|
host : "192.168.1.168:1980",
|
||||||
|
hostname : "192.168.1.168",
|
||||||
|
http_port : 1980,
|
||||||
|
pathname : "/ui/players.html",
|
||||||
|
dir : "/ui",
|
||||||
|
filename : "/players.html",
|
||||||
|
|
||||||
|
vhost : "player.vhost.com",
|
||||||
|
app : "test",
|
||||||
|
stream : "livestream"
|
||||||
|
}
|
||||||
|
* @see: http://blog.csdn.net/win_lin/article/details/17994347
|
||||||
|
*/
|
||||||
|
function parse_query_string(){
|
||||||
|
var obj = {};
|
||||||
|
|
||||||
|
// add the uri object.
|
||||||
|
// parse the host(hostname:http_port), pathname(dir/filename)
|
||||||
|
obj.host = window.location.host;
|
||||||
|
obj.hostname = window.location.hostname;
|
||||||
|
obj.http_port = (window.location.port == "")? 80:window.location.port;
|
||||||
|
obj.pathname = window.location.pathname;
|
||||||
|
if (obj.pathname.lastIndexOf("/") <= 0) {
|
||||||
|
obj.dir = "/";
|
||||||
|
obj.filename = "";
|
||||||
|
} else {
|
||||||
|
obj.dir = obj.pathname.slice(0, obj.pathname.lastIndexOf("/"));
|
||||||
|
obj.filename = obj.pathname.slice(obj.pathname.lastIndexOf("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// pure user query object.
|
||||||
|
obj.user_query = {};
|
||||||
|
|
||||||
|
// parse the query string.
|
||||||
|
var query_string = String(window.location.search).replace(" ", "").split("?")[1];
|
||||||
|
if(query_string === undefined){
|
||||||
|
query_string = String(window.location.hash).replace(" ", "").split("#")[1];
|
||||||
|
if(query_string === undefined){
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__fill_query(query_string, obj);
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __fill_query(query_string, obj) {
|
||||||
|
// pure user query object.
|
||||||
|
obj.user_query = {};
|
||||||
|
|
||||||
|
if (query_string.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split again for angularjs.
|
||||||
|
if (query_string.indexOf("?") >= 0) {
|
||||||
|
query_string = query_string.split("?")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var queries = query_string.split("&");
|
||||||
|
for (var i = 0; i < queries.length; i++) {
|
||||||
|
var elem = queries[i];
|
||||||
|
|
||||||
|
var query = elem.split("=");
|
||||||
|
obj[query[0]] = query[1];
|
||||||
|
obj.user_query[query[0]] = query[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// alias domain for vhost.
|
||||||
|
if (obj.domain) {
|
||||||
|
obj.vhost = obj.domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parse the rtmp url,
|
||||||
|
* for example: rtmp://demo.srs.com:1935/live...vhost...players/livestream
|
||||||
|
* @return object {server, port, vhost, app, stream}
|
||||||
|
* for exmaple, rtmp_url is rtmp://demo.srs.com:1935/live...vhost...players/livestream
|
||||||
|
* parsed to object:
|
||||||
|
{
|
||||||
|
server: "demo.srs.com",
|
||||||
|
port: 1935,
|
||||||
|
vhost: "players",
|
||||||
|
app: "live",
|
||||||
|
stream: "livestream"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
function parse_rtmp_url(rtmp_url) {
|
||||||
|
// @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.href = rtmp_url.replace("rtmp://", "http://")
|
||||||
|
.replace("webrtc://", "http://")
|
||||||
|
.replace("rtc://", "http://");
|
||||||
|
|
||||||
|
var vhost = a.hostname;
|
||||||
|
var app = a.pathname.substring(1, a.pathname.lastIndexOf("/"));
|
||||||
|
var stream = a.pathname.slice(a.pathname.lastIndexOf("/") + 1);
|
||||||
|
|
||||||
|
// parse the vhost in the params of app, that srs supports.
|
||||||
|
app = app.replace("...vhost...", "?vhost=");
|
||||||
|
if (app.indexOf("?") >= 0) {
|
||||||
|
var params = app.slice(app.indexOf("?"));
|
||||||
|
app = app.slice(0, app.indexOf("?"));
|
||||||
|
|
||||||
|
if (params.indexOf("vhost=") > 0) {
|
||||||
|
vhost = params.slice(params.indexOf("vhost=") + "vhost=".length);
|
||||||
|
if (vhost.indexOf("&") > 0) {
|
||||||
|
vhost = vhost.slice(0, vhost.indexOf("&"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when vhost equals to server, and server is ip,
|
||||||
|
// the vhost is __defaultVhost__
|
||||||
|
if (a.hostname === vhost) {
|
||||||
|
var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
|
||||||
|
if (re.test(a.hostname)) {
|
||||||
|
vhost = "__defaultVhost__";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the schema
|
||||||
|
var schema = "rtmp";
|
||||||
|
if (rtmp_url.indexOf("://") > 0) {
|
||||||
|
schema = rtmp_url.slice(0, rtmp_url.indexOf("://"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = a.port;
|
||||||
|
if (!port) {
|
||||||
|
if (schema === 'http') {
|
||||||
|
port = 80;
|
||||||
|
} else if (schema === 'https') {
|
||||||
|
port = 443;
|
||||||
|
} else if (schema === 'rtmp') {
|
||||||
|
port = 1935;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = {
|
||||||
|
url: rtmp_url,
|
||||||
|
schema: schema,
|
||||||
|
server: a.hostname, port: port,
|
||||||
|
vhost: vhost, app: app, stream: stream
|
||||||
|
};
|
||||||
|
__fill_query(a.search, ret);
|
||||||
|
|
||||||
|
// For webrtc API, we use 443 if page is https, or schema specified it.
|
||||||
|
if (!ret.port) {
|
||||||
|
if (schema === 'webrtc' || schema === 'rtc') {
|
||||||
|
if (ret.user_query.schema === 'https') {
|
||||||
|
ret.port = 443;
|
||||||
|
} else if (window.location.href.indexOf('https://') === 0) {
|
||||||
|
ret.port = 443;
|
||||||
|
} else {
|
||||||
|
// For WebRTC, SRS use 1985 as default API port.
|
||||||
|
ret.port = 1985;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the agent.
|
||||||
|
* @return an object specifies some browser.
|
||||||
|
* for example, get_browser_agents().MSIE
|
||||||
|
* @see: http://blog.csdn.net/win_lin/article/details/17994347
|
||||||
|
*/
|
||||||
|
function get_browser_agents() {
|
||||||
|
var agent = navigator.userAgent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
WindowsPC platform, Win7:
|
||||||
|
chrome 31.0.1650.63:
|
||||||
|
Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36
|
||||||
|
(KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
|
||||||
|
firefox 23.0.1:
|
||||||
|
Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20100101
|
||||||
|
Firefox/23.0
|
||||||
|
safari 5.1.7(7534.57.2):
|
||||||
|
Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.57.2
|
||||||
|
(KHTML, like Gecko) Version/5.1.7 Safari/534.57.2
|
||||||
|
opera 15.0.1147.153:
|
||||||
|
Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36
|
||||||
|
(KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36
|
||||||
|
OPR/15.0.1147.153
|
||||||
|
360 6.2.1.272:
|
||||||
|
Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64;
|
||||||
|
Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729;
|
||||||
|
.NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET4.0C;
|
||||||
|
.NET4.0E)
|
||||||
|
IE 10.0.9200.16750(update: 10.0.12):
|
||||||
|
Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64;
|
||||||
|
Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729;
|
||||||
|
.NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET4.0C;
|
||||||
|
.NET4.0E)
|
||||||
|
*/
|
||||||
|
|
||||||
|
return {
|
||||||
|
// platform
|
||||||
|
Android: agent.indexOf("Android") != -1,
|
||||||
|
Windows: agent.indexOf("Windows") != -1,
|
||||||
|
iPhone: agent.indexOf("iPhone") != -1,
|
||||||
|
// Windows Browsers
|
||||||
|
Chrome: agent.indexOf("Chrome") != -1,
|
||||||
|
Firefox: agent.indexOf("Firefox") != -1,
|
||||||
|
QQBrowser: agent.indexOf("QQBrowser") != -1,
|
||||||
|
MSIE: agent.indexOf("MSIE") != -1,
|
||||||
|
// Android Browsers
|
||||||
|
Opera: agent.indexOf("Presto") != -1,
|
||||||
|
MQQBrowser: agent.indexOf("MQQBrowser") != -1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* format relative seconds to HH:MM:SS,
|
||||||
|
* for example, 210s formated to 00:03:30
|
||||||
|
* @see: http://blog.csdn.net/win_lin/article/details/17994347
|
||||||
|
* @usage relative_seconds_to_HHMMSS(210)
|
||||||
|
*/
|
||||||
|
function relative_seconds_to_HHMMSS(seconds){
|
||||||
|
var date = new Date();
|
||||||
|
date.setTime(Number(seconds) * 1000);
|
||||||
|
|
||||||
|
var ret = padding(date.getUTCHours(), 2, '0')
|
||||||
|
+ ":" + padding(date.getUTCMinutes(), 2, '0')
|
||||||
|
+ ":" + padding(date.getUTCSeconds(), 2, '0');
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* format absolute seconds to HH:MM:SS,
|
||||||
|
* for example, 1389146480s (2014-01-08 10:01:20 GMT+0800) formated to 10:01:20
|
||||||
|
* @see: http://blog.csdn.net/win_lin/article/details/17994347
|
||||||
|
* @usage absolute_seconds_to_HHMMSS(new Date().getTime() / 1000)
|
||||||
|
*/
|
||||||
|
function absolute_seconds_to_HHMMSS(seconds){
|
||||||
|
var date = new Date();
|
||||||
|
date.setTime(Number(seconds) * 1000);
|
||||||
|
|
||||||
|
var ret = padding(date.getHours(), 2, '0')
|
||||||
|
+ ":" + padding(date.getMinutes(), 2, '0')
|
||||||
|
+ ":" + padding(date.getSeconds(), 2, '0');
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* format absolute seconds to YYYY-mm-dd,
|
||||||
|
* for example, 1389146480s (2014-01-08 10:01:20 GMT+0800) formated to 2014-01-08
|
||||||
|
* @see: http://blog.csdn.net/win_lin/article/details/17994347
|
||||||
|
* @usage absolute_seconds_to_YYYYmmdd(new Date().getTime() / 1000)
|
||||||
|
*/
|
||||||
|
function absolute_seconds_to_YYYYmmdd(seconds) {
|
||||||
|
var date = new Date();
|
||||||
|
date.setTime(Number(seconds) * 1000);
|
||||||
|
|
||||||
|
var ret = date.getFullYear()
|
||||||
|
+ "-" + padding(date.getMonth() + 1, 2, '0')
|
||||||
|
+ "-" + padding(date.getDate(), 2, '0');
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parse the date in str to Date object.
|
||||||
|
* @param str the date in str, format as "YYYY-mm-dd", for example, 2014-12-11
|
||||||
|
* @returns a date object.
|
||||||
|
* @usage YYYYmmdd_parse("2014-12-11")
|
||||||
|
*/
|
||||||
|
function YYYYmmdd_parse(str) {
|
||||||
|
var date = new Date();
|
||||||
|
date.setTime(Date.parse(str));
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* async refresh function call. to avoid multiple call.
|
||||||
|
* @remark AsyncRefresh is for jquery to refresh the speicified pfn in a page;
|
||||||
|
* if angularjs, use AsyncRefresh2 to change pfn, cancel previous request for angularjs use singleton object.
|
||||||
|
* @param refresh_interval the default refresh interval ms.
|
||||||
|
* @see: http://blog.csdn.net/win_lin/article/details/17994347
|
||||||
|
* the pfn can be implements as following:
|
||||||
|
var async_refresh = new AsyncRefresh(pfn, 3000);
|
||||||
|
function pfn() {
|
||||||
|
if (!async_refresh.refresh_is_enabled()) {
|
||||||
|
async_refresh.request(100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
type: 'GET', async: true, url: 'xxxxx',
|
||||||
|
complete: function(){
|
||||||
|
if (!async_refresh.refresh_is_enabled()) {
|
||||||
|
async_refresh.request(0);
|
||||||
|
} else {
|
||||||
|
async_refresh.request(async_refresh.refresh_interval);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
// if donot allow refresh, directly return.
|
||||||
|
if (!async_refresh.refresh_is_enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// render the res.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
function AsyncRefresh(pfn, refresh_interval) {
|
||||||
|
this.refresh_interval = refresh_interval;
|
||||||
|
|
||||||
|
this.__handler = null;
|
||||||
|
this.__pfn = pfn;
|
||||||
|
|
||||||
|
this.__enabled = true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* disable the refresher, the pfn must check the refresh state.
|
||||||
|
*/
|
||||||
|
AsyncRefresh.prototype.refresh_disable = function() {
|
||||||
|
this.__enabled = false;
|
||||||
|
}
|
||||||
|
AsyncRefresh.prototype.refresh_enable = function() {
|
||||||
|
this.__enabled = true;
|
||||||
|
}
|
||||||
|
AsyncRefresh.prototype.refresh_is_enabled = function() {
|
||||||
|
return this.__enabled;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* start new async request
|
||||||
|
* @param timeout the timeout in ms.
|
||||||
|
* user can use the refresh_interval of the AsyncRefresh object,
|
||||||
|
* which initialized in constructor.
|
||||||
|
*/
|
||||||
|
AsyncRefresh.prototype.request = function(timeout) {
|
||||||
|
if (this.__handler) {
|
||||||
|
clearTimeout(this.__handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__handler = setTimeout(this.__pfn, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* async refresh v2, support cancellable refresh, and change the refresh pfn.
|
||||||
|
* @remakr for angularjs. if user only need jquery, maybe AsyncRefresh is better.
|
||||||
|
* @see: http://blog.csdn.net/win_lin/article/details/17994347
|
||||||
|
* Usage:
|
||||||
|
bsmControllers.controller('CServers', ['$scope', 'MServer', function($scope, MServer){
|
||||||
|
async_refresh2.refresh_change(function(){
|
||||||
|
// 获取服务器列表
|
||||||
|
MServer.servers_load({}, function(data){
|
||||||
|
$scope.servers = data.data.servers;
|
||||||
|
async_refresh2.request();
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
async_refresh2.request(0);
|
||||||
|
}]);
|
||||||
|
bsmControllers.controller('CStreams', ['$scope', 'MStream', function($scope, MStream){
|
||||||
|
async_refresh2.refresh_change(function(){
|
||||||
|
// 获取流列表
|
||||||
|
MStream.streams_load({}, function(data){
|
||||||
|
$scope.streams = data.data.streams;
|
||||||
|
async_refresh2.request();
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
async_refresh2.request(0);
|
||||||
|
}]);
|
||||||
|
*/
|
||||||
|
function AsyncRefresh2() {
|
||||||
|
/**
|
||||||
|
* the function callback before call the pfn.
|
||||||
|
* the protype is function():bool, which return true to invoke, false to abort the call.
|
||||||
|
* null to ignore this callback.
|
||||||
|
*
|
||||||
|
* for example, user can abort the refresh by find the class popover:
|
||||||
|
* async_refresh2.on_before_call_pfn = function() {
|
||||||
|
* if ($(".popover").length > 0) {
|
||||||
|
* async_refresh2.request();
|
||||||
|
* return false;
|
||||||
|
* }
|
||||||
|
* return true;
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
this.on_before_call_pfn = null;
|
||||||
|
|
||||||
|
// use a anonymous function to call, and check the enabled when actually invoke.
|
||||||
|
this.__call = {
|
||||||
|
pfn: null,
|
||||||
|
timeout: 0,
|
||||||
|
__enabled: false,
|
||||||
|
__handler: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// singleton
|
||||||
|
var async_refresh2 = new AsyncRefresh2();
|
||||||
|
/**
|
||||||
|
* initialize or refresh change. cancel previous request, setup new request.
|
||||||
|
* @param pfn a function():void to request after timeout. null to disable refresher.
|
||||||
|
* @param timeout the timeout in ms, to call pfn. null to disable refresher.
|
||||||
|
*/
|
||||||
|
AsyncRefresh2.prototype.initialize = function(pfn, timeout) {
|
||||||
|
this.refresh_change(pfn, timeout);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* stop refresh, the refresh pfn is set to null.
|
||||||
|
*/
|
||||||
|
AsyncRefresh2.prototype.stop = function() {
|
||||||
|
this.__call.__enabled = false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* restart refresh, use previous config.
|
||||||
|
*/
|
||||||
|
AsyncRefresh2.prototype.restart = function() {
|
||||||
|
this.__call.__enabled = true;
|
||||||
|
this.request(0);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* change refresh pfn, the old pfn will set to disabled.
|
||||||
|
*/
|
||||||
|
AsyncRefresh2.prototype.refresh_change = function(pfn, timeout) {
|
||||||
|
// cancel the previous call.
|
||||||
|
if (this.__call.__handler) {
|
||||||
|
clearTimeout(this.__handler);
|
||||||
|
}
|
||||||
|
this.__call.__enabled = false;
|
||||||
|
|
||||||
|
// setup new call.
|
||||||
|
this.__call = {
|
||||||
|
pfn: pfn,
|
||||||
|
timeout: timeout,
|
||||||
|
__enabled: true,
|
||||||
|
__handler: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* start new request, we never auto start the request,
|
||||||
|
* user must start new request when previous completed.
|
||||||
|
* @param timeout [optional] if not specified, use the timeout in initialize or refresh_change.
|
||||||
|
*/
|
||||||
|
AsyncRefresh2.prototype.request = function(timeout) {
|
||||||
|
var self = this;
|
||||||
|
var this_call = this.__call;
|
||||||
|
|
||||||
|
// clear previous timeout.
|
||||||
|
if (this_call.__handler) {
|
||||||
|
clearTimeout(this_call.__handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// override the timeout
|
||||||
|
if (timeout == undefined) {
|
||||||
|
timeout = this_call.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if user disabled refresher.
|
||||||
|
if (this_call.pfn == null || timeout == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this_call.__handler = setTimeout(function(){
|
||||||
|
// cancelled by refresh_change, ignore.
|
||||||
|
if (!this_call.__enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// callback if the handler installled.
|
||||||
|
if (self.on_before_call_pfn) {
|
||||||
|
if (!self.on_before_call_pfn()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the actual call.
|
||||||
|
this_call.pfn();
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user