Merge branch 'develop' into feature/extract_rate_from_sdp
This commit is contained in:
commit
3ea9099adb
|
|
@ -325,6 +325,20 @@ testing:
|
|||
HELPER_EXPECT_FAILED(some_function_that_should_fail());
|
||||
}
|
||||
|
||||
debugging:
|
||||
memory_and_pointer_issues:
|
||||
description: "For memory corruption, crashes, pointer issues, or undefined behavior, proper debugging information is MANDATORY"
|
||||
required_information: "User MUST provide either coredump stack trace OR sanitizer output - without it, memory issues cannot be diagnosed"
|
||||
|
||||
sanitizer:
|
||||
description: "AddressSanitizer (ASAN) - Google's memory error detection tool for catching buffer overflows, use-after-free, memory leaks, etc."
|
||||
when_enabled: "Automatically enabled for unit tests (--utest=on), manually enabled with ./configure --sanitizer=on, disabled by default for production builds (causes memory leak and increasing forever)"
|
||||
how_to_enable: "./configure --sanitizer=on && make"
|
||||
|
||||
coredump:
|
||||
description: "When sanitizer cannot be used, coredump with stack trace is required"
|
||||
how_to_get: "ulimit -c unlimited, run SRS until crash, gdb ./objs/srs core.xxx -ex 'bt' -ex 'quit'"
|
||||
|
||||
code_review:
|
||||
github_pull_requests:
|
||||
- When reviewing or understanding GitHub pull requests, use the diff URL to get the code changes
|
||||
|
|
|
|||
2
trunk/3rdparty/signaling/main.go
vendored
2
trunk/3rdparty/signaling/main.go
vendored
|
|
@ -1,6 +1,6 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2025 Winlin
|
||||
// # Copyright (c) 2025 Winlin
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
|
|
|
|||
74
trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/errors.go
generated
vendored
74
trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/errors.go
generated
vendored
|
|
@ -2,88 +2,88 @@
|
|||
//
|
||||
// The traditional error handling idiom in Go is roughly akin to
|
||||
//
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// which applied recursively up the call stack results in error reports
|
||||
// without context or debugging information. The errors package allows
|
||||
// programmers to add context to the failure path in their code in a way
|
||||
// that does not destroy the original value of the error.
|
||||
//
|
||||
// Adding context to an error
|
||||
// # Adding context to an error
|
||||
//
|
||||
// The errors.Wrap function returns a new error that adds context to the
|
||||
// original error by recording a stack trace at the point Wrap is called,
|
||||
// and the supplied message. For example
|
||||
//
|
||||
// _, err := ioutil.ReadAll(r)
|
||||
// if err != nil {
|
||||
// return errors.Wrap(err, "read failed")
|
||||
// }
|
||||
// _, err := ioutil.ReadAll(r)
|
||||
// if err != nil {
|
||||
// return errors.Wrap(err, "read failed")
|
||||
// }
|
||||
//
|
||||
// If additional control is required the errors.WithStack and errors.WithMessage
|
||||
// functions destructure errors.Wrap into its component operations of annotating
|
||||
// an error with a stack trace and an a message, respectively.
|
||||
//
|
||||
// Retrieving the cause of an error
|
||||
// # Retrieving the cause of an error
|
||||
//
|
||||
// Using errors.Wrap constructs a stack of errors, adding context to the
|
||||
// preceding error. Depending on the nature of the error it may be necessary
|
||||
// to reverse the operation of errors.Wrap to retrieve the original error
|
||||
// for inspection. Any error value which implements this interface
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
|
||||
// the topmost error which does not implement causer, which is assumed to be
|
||||
// the original cause. For example:
|
||||
//
|
||||
// switch err := errors.Cause(err).(type) {
|
||||
// case *MyError:
|
||||
// // handle specifically
|
||||
// default:
|
||||
// // unknown error
|
||||
// }
|
||||
// switch err := errors.Cause(err).(type) {
|
||||
// case *MyError:
|
||||
// // handle specifically
|
||||
// default:
|
||||
// // unknown error
|
||||
// }
|
||||
//
|
||||
// causer interface is not exported by this package, but is considered a part
|
||||
// of stable public API.
|
||||
//
|
||||
// Formatted printing of errors
|
||||
// # Formatted printing of errors
|
||||
//
|
||||
// All error values returned from this package implement fmt.Formatter and can
|
||||
// be formatted by the fmt package. The following verbs are supported
|
||||
//
|
||||
// %s print the error. If the error has a Cause it will be
|
||||
// printed recursively
|
||||
// %v see %s
|
||||
// %+v extended format. Each Frame of the error's StackTrace will
|
||||
// be printed in detail.
|
||||
// %s print the error. If the error has a Cause it will be
|
||||
// printed recursively
|
||||
// %v see %s
|
||||
// %+v extended format. Each Frame of the error's StackTrace will
|
||||
// be printed in detail.
|
||||
//
|
||||
// Retrieving the stack trace of an error or wrapper
|
||||
// # Retrieving the stack trace of an error or wrapper
|
||||
//
|
||||
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
|
||||
// invoked. This information can be retrieved with the following interface.
|
||||
//
|
||||
// type stackTracer interface {
|
||||
// StackTrace() errors.StackTrace
|
||||
// }
|
||||
// type stackTracer interface {
|
||||
// StackTrace() errors.StackTrace
|
||||
// }
|
||||
//
|
||||
// Where errors.StackTrace is defined as
|
||||
//
|
||||
// type StackTrace []Frame
|
||||
// type StackTrace []Frame
|
||||
//
|
||||
// The Frame type represents a call site in the stack trace. Frame supports
|
||||
// the fmt.Formatter interface that can be used for printing information about
|
||||
// the stack trace of this error. For example:
|
||||
//
|
||||
// if err, ok := err.(stackTracer); ok {
|
||||
// for _, f := range err.StackTrace() {
|
||||
// fmt.Printf("%+s:%d", f)
|
||||
// }
|
||||
// }
|
||||
// if err, ok := err.(stackTracer); ok {
|
||||
// for _, f := range err.StackTrace() {
|
||||
// fmt.Printf("%+s:%d", f)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// stackTracer interface is not exported by this package, but is considered a part
|
||||
// of stable public API.
|
||||
|
|
@ -247,9 +247,9 @@ func (w *withMessage) Format(s fmt.State, verb rune) {
|
|||
// An error value has a cause if it implements the following
|
||||
// interface:
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// If the error does not implement Cause, the original error will
|
||||
// be returned. If the error is nil, nil will be returned without further
|
||||
|
|
|
|||
18
trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/stack.go
generated
vendored
18
trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/stack.go
generated
vendored
|
|
@ -40,15 +40,15 @@ func (f Frame) line() int {
|
|||
|
||||
// Format formats the frame according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s source file
|
||||
// %d source line
|
||||
// %n function name
|
||||
// %v equivalent to %s:%d
|
||||
// %s source file
|
||||
// %d source line
|
||||
// %n function name
|
||||
// %v equivalent to %s:%d
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+s path of source file relative to the compile time GOPATH
|
||||
// %+v equivalent to %+s:%d
|
||||
// %+s path of source file relative to the compile time GOPATH
|
||||
// %+v equivalent to %+s:%d
|
||||
func (f Frame) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
|
|
@ -82,12 +82,12 @@ type StackTrace []Frame
|
|||
|
||||
// Format formats the stack of Frames according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s lists source files for each Frame in the stack
|
||||
// %v lists the source file and line number for each Frame in the stack
|
||||
// %s lists source files for each Frame in the stack
|
||||
// %v lists the source file and line number for each Frame in the stack
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+v Prints filename, function, and line number for each Frame in the stack.
|
||||
// %+v Prints filename, function, and line number for each Frame in the stack.
|
||||
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
|
|
|
|||
1
trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/go17.go
generated
vendored
1
trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/go17.go
generated
vendored
|
|
@ -19,6 +19,7 @@
|
|||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
//go:build go1.7
|
||||
// +build go1.7
|
||||
|
||||
package logger
|
||||
|
|
|
|||
23
trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/logger.go
generated
vendored
23
trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/logger.go
generated
vendored
|
|
@ -20,18 +20,23 @@
|
|||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
// The oryx logger package provides connection-oriented log service.
|
||||
// logger.I(ctx, ...)
|
||||
// logger.T(ctx, ...)
|
||||
// logger.W(ctx, ...)
|
||||
// logger.E(ctx, ...)
|
||||
//
|
||||
// logger.I(ctx, ...)
|
||||
// logger.T(ctx, ...)
|
||||
// logger.W(ctx, ...)
|
||||
// logger.E(ctx, ...)
|
||||
//
|
||||
// Or use format:
|
||||
// logger.If(ctx, format, ...)
|
||||
// logger.Tf(ctx, format, ...)
|
||||
// logger.Wf(ctx, format, ...)
|
||||
// logger.Ef(ctx, format, ...)
|
||||
//
|
||||
// logger.If(ctx, format, ...)
|
||||
// logger.Tf(ctx, format, ...)
|
||||
// logger.Wf(ctx, format, ...)
|
||||
// logger.Ef(ctx, format, ...)
|
||||
//
|
||||
// @remark the Context is optional thus can be nil.
|
||||
// @remark From 1.7+, the ctx could be context.Context, wrap by logger.WithContext,
|
||||
// please read ExampleLogger_ContextGO17().
|
||||
//
|
||||
// please read ExampleLogger_ContextGO17().
|
||||
package logger
|
||||
|
||||
import (
|
||||
|
|
|
|||
1
trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/pre_go17.go
generated
vendored
1
trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/pre_go17.go
generated
vendored
|
|
@ -19,6 +19,7 @@
|
|||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
//go:build !go1.7
|
||||
// +build !go1.7
|
||||
|
||||
package logger
|
||||
|
|
|
|||
5
trunk/3rdparty/signaling/vendor/golang.org/x/net/websocket/websocket.go
generated
vendored
5
trunk/3rdparty/signaling/vendor/golang.org/x/net/websocket/websocket.go
generated
vendored
|
|
@ -8,8 +8,8 @@
|
|||
// This package currently lacks some features found in alternative
|
||||
// and more actively maintained WebSocket packages:
|
||||
//
|
||||
// https://godoc.org/github.com/gorilla/websocket
|
||||
// https://godoc.org/nhooyr.io/websocket
|
||||
// https://godoc.org/github.com/gorilla/websocket
|
||||
// https://godoc.org/nhooyr.io/websocket
|
||||
package websocket // import "golang.org/x/net/websocket"
|
||||
|
||||
import (
|
||||
|
|
@ -416,7 +416,6 @@ Trivial usage:
|
|||
// send binary frame
|
||||
data = []byte{0, 1, 2}
|
||||
websocket.Message.Send(ws, data)
|
||||
|
||||
*/
|
||||
var Message = Codec{marshal, unmarshal}
|
||||
|
||||
|
|
|
|||
622
trunk/3rdparty/signaling/www/demos/js/srs.sdk.js
vendored
622
trunk/3rdparty/signaling/www/demos/js/srs.sdk.js
vendored
|
|
@ -1,32 +1,23 @@
|
|||
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2013-2025 Winlin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
//
|
||||
// Copyright (c) 2013-2025 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() {
|
||||
// Async-awat-prmise based SRS RTC Publisher by WHIP.
|
||||
function SrsRtcWhipWhepAsync() {
|
||||
var self = {};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||
|
|
@ -37,80 +28,144 @@ function SrsRtcPublisherAsync() {
|
|||
}
|
||||
};
|
||||
|
||||
// @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(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.publish = async function (url) {
|
||||
var conf = self.__internal.prepareUrl(url);
|
||||
self.pc.addTransceiver("audio", {direction: "sendonly"});
|
||||
self.pc.addTransceiver("video", {direction: "sendonly"});
|
||||
// Store media streams to stop tracks when closing.
|
||||
self.displayStream = null;
|
||||
self.userStream = null;
|
||||
|
||||
var stream = await navigator.mediaDevices.getUserMedia(self.constraints);
|
||||
// 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
|
||||
// @options The options to control playing, supports:
|
||||
// camera: boolean, whether capture video from camera, default to true.
|
||||
// screen: boolean, whether capture video from screen, default to false.
|
||||
// audio: boolean, whether play audio, default to true.
|
||||
self.publish = async function (url, options) {
|
||||
if (url.indexOf('/whip/') === -1) throw new Error(`invalid WHIP url ${url}`);
|
||||
const hasAudio = options?.audio ?? true;
|
||||
const useCamera = options?.camera ?? true;
|
||||
const useScreen = options?.screen ?? false;
|
||||
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||
stream.getTracks().forEach(function (track) {
|
||||
self.pc.addTrack(track);
|
||||
if (!hasAudio && !useCamera && !useScreen) throw new Error(`The camera, screen and audio can't be false at the same time`);
|
||||
|
||||
// Notify about local track when stream is ok.
|
||||
self.ontrack && self.ontrack({track: track});
|
||||
});
|
||||
if (hasAudio) {
|
||||
self.pc.addTransceiver("audio", {direction: "sendonly"});
|
||||
} else {
|
||||
self.constraints.audio = false;
|
||||
}
|
||||
|
||||
if (useCamera || useScreen) {
|
||||
self.pc.addTransceiver("video", {direction: "sendonly"});
|
||||
}
|
||||
|
||||
if (!useCamera) {
|
||||
self.constraints.video = false;
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
if (useScreen) {
|
||||
self.displayStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: true
|
||||
});
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||
self.displayStream.getTracks().forEach(function (track) {
|
||||
self.pc.addTrack(track);
|
||||
// Notify about local track when stream is ok.
|
||||
self.ontrack && self.ontrack({track: track});
|
||||
});
|
||||
}
|
||||
|
||||
if (useCamera || hasAudio) {
|
||||
self.userStream = await navigator.mediaDevices.getUserMedia(self.constraints);
|
||||
|
||||
self.userStream.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 answer = await new Promise(function (resolve, reject) {
|
||||
console.log(`Generated offer: ${offer.sdp}`);
|
||||
|
||||
$.ajax({
|
||||
type: "POST", url: conf.apiUrl, data: JSON.stringify(data),
|
||||
contentType: 'application/json', dataType: 'json'
|
||||
}).done(function (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 = xhr.responseText;
|
||||
console.log("Got answer: ", data);
|
||||
if (data.code) {
|
||||
reject(data);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(data);
|
||||
}).fail(function (reason) {
|
||||
reject(reason);
|
||||
});
|
||||
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: session.sdp})
|
||||
new RTCSessionDescription({type: 'answer', sdp: answer})
|
||||
);
|
||||
session.simulator = conf.schema + '//' + conf.urlObject.server + ':' + conf.port + '/rtc/v1/nack/';
|
||||
|
||||
return session;
|
||||
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
|
||||
// @options The options to control playing, supports:
|
||||
// videoOnly: boolean, whether only play video, default to false.
|
||||
// audioOnly: boolean, whether only play audio, default to false.
|
||||
self.play = async function(url, options) {
|
||||
if (url.indexOf('/whip-play/') === -1 && url.indexOf('/whep/') === -1) throw new Error(`invalid WHEP url ${url}`);
|
||||
if (options?.videoOnly && options?.audioOnly) throw new Error(`The videoOnly and audioOnly in options can't be true at the same time`);
|
||||
|
||||
if (!options?.videoOnly) self.pc.addTransceiver("audio", {direction: "recvonly"});
|
||||
if (!options?.audioOnly) 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.sdp}`);
|
||||
|
||||
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;
|
||||
|
||||
// Stop all media tracks to release camera/microphone.
|
||||
if (self.displayStream) {
|
||||
self.displayStream.getTracks().forEach(function (track) {
|
||||
track.stop();
|
||||
});
|
||||
self.displayStream = null;
|
||||
}
|
||||
if (self.userStream) {
|
||||
self.userStream.getTracks().forEach(function (track) {
|
||||
track.stop();
|
||||
});
|
||||
self.userStream = null;
|
||||
}
|
||||
};
|
||||
|
||||
// The callback when got local stream.
|
||||
|
|
@ -120,147 +175,6 @@ function SrsRtcPublisherAsync() {
|
|||
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 += '/';
|
||||
}
|
||||
|
||||
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,
|
||||
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) {
|
||||
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.
|
||||
|
|
@ -268,231 +182,23 @@ function SrsRtcPublisherAsync() {
|
|||
// @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
|
||||
// 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, tid: conf.tid, 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})
|
||||
);
|
||||
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 += '/';
|
||||
}
|
||||
|
||||
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;
|
||||
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 {
|
||||
apiUrl: apiUrl, streamUrl: streamUrl, schema: schema, urlObject: urlObject, port: port,
|
||||
tid: Number(parseInt(new Date().getTime()*Math.random()*100)).toString(16).slice(0, 7)
|
||||
sessionid: sessionid, // Should be ice-ufrag of answer:offer.
|
||||
simulator: a.protocol + '//' + a.host + '/rtc/v1/nack/',
|
||||
};
|
||||
},
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
|
|
@ -503,33 +209,29 @@ function SrsRtcPlayerAsync() {
|
|||
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) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCStatsReport
|
||||
function SrsRtcFormatStats(stats, 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;
|
||||
}
|
||||
|
||||
stats.forEach((report) => {
|
||||
if (report.type === 'codec' && report.mimeType?.toLowerCase().startsWith(kind)) {
|
||||
var s = '';
|
||||
|
||||
s += c.mimeType.replace('audio/', '').replace('video/', '');
|
||||
s += ', ' + c.clockRate + 'HZ';
|
||||
if (sender.track.kind === "audio") {
|
||||
s += ', channels: ' + c.channels;
|
||||
s += report.mimeType.split('/')[1] || report.mimeType;
|
||||
|
||||
if (report.clockRate) {
|
||||
s += ', ' + report.clockRate + 'HZ';
|
||||
}
|
||||
s += ', pt: ' + c.payloadType;
|
||||
|
||||
if (kind === 'audio' && report.channels) {
|
||||
s += ', channels: ' + report.channels;
|
||||
}
|
||||
|
||||
if (report.payloadType) {
|
||||
s += ', pt: ' + report.payloadType;
|
||||
}
|
||||
|
||||
codecs.push(s);
|
||||
});
|
||||
}
|
||||
});
|
||||
return codecs.join(", ");
|
||||
}
|
||||
|
||||
}
|
||||
34
trunk/3rdparty/signaling/www/demos/one2one.html
vendored
34
trunk/3rdparty/signaling/www/demos/one2one.html
vendored
|
|
@ -213,23 +213,33 @@
|
|||
}
|
||||
};
|
||||
|
||||
// Convert webrtc:// URL to WHIP URL
|
||||
var convertToWhipUrl = function(host, room, display) {
|
||||
var schema = window.location.protocol;
|
||||
var port = 1985;
|
||||
if (schema === 'https:') {
|
||||
port = 443;
|
||||
}
|
||||
return schema + '//' + host + ':' + port + '/rtc/v1/whip/?app=' + room + '&stream=' + display + conf.query.replace('?', '&');
|
||||
};
|
||||
|
||||
var startPublish = function (host, room, display) {
|
||||
$(".ff_first").each(function(i,e) {
|
||||
$(e).text(display);
|
||||
});
|
||||
|
||||
var url = 'webrtc://' + host + '/' + room + '/' + display + conf.query;
|
||||
var whipUrl = convertToWhipUrl(host, room, display);
|
||||
$('#rtc_media_publisher').show();
|
||||
$('#publisher').show();
|
||||
|
||||
if (publisher) {
|
||||
publisher.close();
|
||||
}
|
||||
publisher = new SrsRtcPublisherAsync();
|
||||
publisher = new SrsRtcWhipWhepAsync();
|
||||
$('#rtc_media_publisher').prop('srcObject', publisher.stream);
|
||||
|
||||
return publisher.publish(url).then(function(session){
|
||||
$('#self').text('Self: ' + url);
|
||||
return publisher.publish(whipUrl).then(function(session){
|
||||
$('#self').text('Self: ' + display);
|
||||
}).catch(function (reason) {
|
||||
publisher.close();
|
||||
$('#rtc_media_publisher').hide();
|
||||
|
|
@ -237,12 +247,22 @@
|
|||
});
|
||||
};
|
||||
|
||||
// Convert webrtc:// URL to WHEP URL
|
||||
var convertToWhepUrl = function(host, room, display) {
|
||||
var schema = window.location.protocol;
|
||||
var port = 1985;
|
||||
if (schema === 'https:') {
|
||||
port = 443;
|
||||
}
|
||||
return schema + '//' + host + ':' + port + '/rtc/v1/whep/?app=' + room + '&stream=' + display + conf.query.replace('?', '&');
|
||||
};
|
||||
|
||||
var startPlay = function (host, room, display) {
|
||||
$(".ff_second").each(function(i,e) {
|
||||
$(e).text(display);
|
||||
});
|
||||
|
||||
var url = 'webrtc://' + host + '/' + room + '/' + display + conf.query;
|
||||
var whepUrl = convertToWhepUrl(host, room, display);
|
||||
$('#rtc_media_player').show();
|
||||
$('#player').show();
|
||||
|
||||
|
|
@ -250,10 +270,10 @@
|
|||
player.close();
|
||||
}
|
||||
|
||||
player = new SrsRtcPlayerAsync();
|
||||
player = new SrsRtcWhipWhepAsync();
|
||||
$('#rtc_media_player').prop('srcObject', player.stream);
|
||||
|
||||
player.play(url).then(function(session){
|
||||
player.play(whepUrl).then(function(session){
|
||||
$('#peer').text('Peer: ' + display);
|
||||
$('#rtc_media_player').prop('muted', false);
|
||||
}).catch(function (reason) {
|
||||
|
|
|
|||
36
trunk/3rdparty/signaling/www/demos/room.html
vendored
36
trunk/3rdparty/signaling/www/demos/room.html
vendored
|
|
@ -126,23 +126,33 @@
|
|||
});
|
||||
};
|
||||
|
||||
// Convert webrtc:// URL to WHIP URL
|
||||
var convertToWhipUrl = function(host, room, display) {
|
||||
var schema = window.location.protocol;
|
||||
var port = 1985;
|
||||
if (schema === 'https:') {
|
||||
port = 443;
|
||||
}
|
||||
return schema + '//' + host + ':' + port + '/rtc/v1/whip/?app=' + room + '&stream=' + display + conf.query.replace('?', '&');
|
||||
};
|
||||
|
||||
var startPublish = function (host, room, display) {
|
||||
$(".ff_first").each(function(i,e) {
|
||||
$(e).text(display);
|
||||
});
|
||||
|
||||
var url = 'webrtc://' + host + '/' + room + '/' + display + conf.query;
|
||||
var whipUrl = convertToWhipUrl(host, room, display);
|
||||
$('#rtc_media_publisher').show();
|
||||
$('#publisher').show();
|
||||
|
||||
if (publisher) {
|
||||
publisher.close();
|
||||
}
|
||||
publisher = new SrsRtcPublisherAsync();
|
||||
publisher = new SrsRtcWhipWhepAsync();
|
||||
$('#rtc_media_publisher').prop('srcObject', publisher.stream);
|
||||
|
||||
return publisher.publish(url).then(function(session){
|
||||
$('#self').text('Self: ' + url);
|
||||
return publisher.publish(whipUrl).then(function(session){
|
||||
$('#self').text('Self: ' + display);
|
||||
}).catch(function (reason) {
|
||||
publisher.close();
|
||||
$('#rtc_media_publisher').hide();
|
||||
|
|
@ -150,6 +160,16 @@
|
|||
});
|
||||
};
|
||||
|
||||
// Convert webrtc:// URL to WHEP URL
|
||||
var convertToWhepUrl = function(host, room, display) {
|
||||
var schema = window.location.protocol;
|
||||
var port = 1985;
|
||||
if (schema === 'https:') {
|
||||
port = 443;
|
||||
}
|
||||
return schema + '//' + host + ':' + port + '/rtc/v1/whep/?app=' + room + '&stream=' + display + conf.query.replace('?', '&');
|
||||
};
|
||||
|
||||
var startPlay = function (host, room, display) {
|
||||
$(".ff_second").each(function(i,e) {
|
||||
$(e).text(display);
|
||||
|
|
@ -165,20 +185,20 @@
|
|||
let ui = $('#player').clone().attr('id', 'player-' + display);
|
||||
let video = ui.children('#rtc_media_player');
|
||||
console.log(video.length);
|
||||
let player = new SrsRtcPlayerAsync();
|
||||
let player = new SrsRtcWhipWhepAsync();
|
||||
|
||||
players[display] = {ui:ui, video:video, player:player};
|
||||
$('.srs_players').append(ui);
|
||||
|
||||
// Start play for this user.
|
||||
var url = 'webrtc://' + host + '/' + room + '/' + display + conf.query;
|
||||
var whepUrl = convertToWhepUrl(host, room, display);
|
||||
video.show();
|
||||
ui.show();
|
||||
|
||||
video.prop('srcObject', player.stream);
|
||||
|
||||
player.play(url).then(function(session){
|
||||
ui.children('#peer').text('Peer: ' + url);
|
||||
player.play(whepUrl).then(function(session){
|
||||
ui.children('#peer').text('Peer: ' + display);
|
||||
video.prop('muted', false);
|
||||
}).catch(function (reason) {
|
||||
player.close();
|
||||
|
|
|
|||
|
|
@ -21,12 +21,13 @@
|
|||
package blackbox
|
||||
|
||||
import (
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
|
|
|||
|
|
@ -23,14 +23,15 @@ package blackbox
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
func TestFast_RtmpPublish_DvrFlv_Basic(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ package blackbox
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
|
|
@ -32,6 +30,9 @@ import (
|
|||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
func TestSlow_RtmpPublish_RtmpPlay_HEVC_Basic(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -23,14 +23,15 @@ package blackbox
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
func TestFast_RtmpPublish_HlsPlay_Basic(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -23,12 +23,13 @@ package blackbox
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
func TestFast_Http_Api_Basic_Auth(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -23,14 +23,15 @@ package blackbox
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
func TestFast_RtmpPublish_RtmpPlay_CodecMP3_Basic(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -23,14 +23,15 @@ package blackbox
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
func TestFast_RtmpPublish_RtmpPlay_Basic(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -23,14 +23,15 @@ package blackbox
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
func TestFast_SrtPublish_SrtPlay_Basic(t *testing.T) {
|
||||
|
|
|
|||
7
trunk/3rdparty/srs-bench/blackbox/util.go
vendored
7
trunk/3rdparty/srs-bench/blackbox/util.go
vendored
|
|
@ -26,9 +26,6 @@ import (
|
|||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
ohttp "github.com/ossrs/go-oryx-lib/http"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
|
|
@ -41,6 +38,10 @@ import (
|
|||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
ohttp "github.com/ossrs/go-oryx-lib/http"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
var srsLog *bool
|
||||
|
|
|
|||
5
trunk/3rdparty/srs-bench/janus/api.go
vendored
5
trunk/3rdparty/srs-bench/janus/api.go
vendored
|
|
@ -24,13 +24,14 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
type publisherInfo struct {
|
||||
|
|
|
|||
5
trunk/3rdparty/srs-bench/janus/janus.go
vendored
5
trunk/3rdparty/srs-bench/janus/janus.go
vendored
|
|
@ -24,12 +24,13 @@ import (
|
|||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
var sr string
|
||||
|
|
|
|||
2
trunk/3rdparty/srs-bench/live/live.go
vendored
2
trunk/3rdparty/srs-bench/live/live.go
vendored
|
|
@ -174,7 +174,7 @@ func Run(ctx context.Context) error {
|
|||
gStatLive.Publishers.Alive--
|
||||
logger.Tf(ctx, "Publisher %v done, alive=%v", pr, gStatLive.Publishers.Alive)
|
||||
|
||||
<- publisherStartedCtx.Done()
|
||||
<-publisherStartedCtx.Done()
|
||||
if gStatLive.Publishers.Alive == 0 {
|
||||
cancel()
|
||||
}
|
||||
|
|
|
|||
5
trunk/3rdparty/srs-bench/srs/srs.go
vendored
5
trunk/3rdparty/srs-bench/srs/srs.go
vendored
|
|
@ -24,14 +24,15 @@ import (
|
|||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
var sr, dumpAudio, dumpVideo string
|
||||
|
|
|
|||
3
trunk/3rdparty/srs-bench/srs/srs_test.go
vendored
3
trunk/3rdparty/srs-bench/srs/srs_test.go
vendored
|
|
@ -21,12 +21,13 @@
|
|||
package srs
|
||||
|
||||
import (
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ossrs/go-oryx-lib/logger"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
|
|
|||
17
trunk/3rdparty/srs-docs/doc/hls.md
vendored
17
trunk/3rdparty/srs-docs/doc/hls.md
vendored
|
|
@ -426,6 +426,23 @@ Then configure `hls_path` or create a soft link to the directory.
|
|||
|
||||
To deploy an HLS distribution cluster and edge distribution cluster for your own CDN to handle a large number of viewers, please refer to [Nginx for HLS](./nginx-for-hls.md).
|
||||
|
||||
## HLS with Reverse Proxy
|
||||
|
||||
When deploying SRS behind a reverse proxy with path rewriting, you may encounter issues where HLS master playlists use absolute paths (e.g., `/live/livestream.m3u8?hls_ctx=xxx`), which can break playback when the proxy rewrites request paths. For example, if the external URL is `http://proxy/srs/live/stream.m3u8` but gets rewritten to `http://origin/live/stream.m3u8`, the absolute path in the master playlist will drop the `/srs` prefix, causing a 404 error.
|
||||
|
||||
SRS (v7.0.104+) provides the `hls_master_m3u8_path_relative` option to use relative paths in master playlists for reverse proxy compatibility:
|
||||
|
||||
```bash
|
||||
vhost __defaultVhost__ {
|
||||
hls {
|
||||
enabled on;
|
||||
hls_master_m3u8_path_relative on; # Use relative path for reverse proxy
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When enabled, the master playlist uses relative paths (e.g., `livestream.m3u8?hls_ctx=xxx`) instead of absolute paths, allowing the player to correctly resolve URLs through the reverse proxy. This is useful for deployments with Nginx, Apache, HAProxy, API gateways, or multi-tenant systems with path-based routing. The default is `off` for backward compatibility.
|
||||
|
||||
## HLS Low Latency
|
||||
|
||||
How to reduce HLS latency? The key is to reduce the number of slices and the number of TS files in the m3u8. SRS's default configuration is 10 seconds per slice and 60 seconds per m3u8, resulting in a latency of about 30 seconds. Some players start requesting slices from the middle position, so there will be a delay of 3 slices.
|
||||
|
|
|
|||
28
trunk/3rdparty/srs-docs/doc/webrtc.md
vendored
28
trunk/3rdparty/srs-docs/doc/webrtc.md
vendored
|
|
@ -453,7 +453,7 @@ docker run --rm --env CANDIDATE=$CANDIDATE \
|
|||
|
||||
> Note: Please set CANDIDATE as the ip of server, please read [CANDIDATE](./webrtc.md#config-candidate).
|
||||
|
||||
Then startup the signaling, please read [usage](http://ossrs.net/srs.release/wiki/https://github.com/ossrs/signaling#usage):
|
||||
Then startup the signaling, please read [usage](https://github.com/ossrs/signaling#usage):
|
||||
|
||||
```bash
|
||||
docker run --rm -p 1989:1989 ossrs/signaling:1
|
||||
|
|
@ -585,6 +585,32 @@ This enables:
|
|||
- IPv4 clients to connect via: `http://192.168.1.100:1985/rtc/v1/whip/`
|
||||
- IPv6 clients to connect via: `http://[2001:db8::1]:1985/rtc/v1/whip/`
|
||||
|
||||
## Known Limitation: Initial Audio Loss
|
||||
|
||||
When publishing WebRTC streams, you may notice that the **first 4-6 seconds of audio are missing** in recordings (DVR),
|
||||
RTMP playback, or HTTP-FLV streams. This is a **known limitation** of WebRTC's audio/video synchronization mechanism,
|
||||
not a bug.
|
||||
|
||||
**Root Cause**: WebRTC uses RTCP Sender Reports (SR) to synchronize audio and video timestamps. When a WebRTC stream
|
||||
starts, both audio and video RTP packets arrive immediately. However, SRS needs RTCP Sender Reports to calculate proper
|
||||
timestamps for synchronizing audio and video. The A/V sync calculation requires **TWO** RTCP Sender Reports to establish
|
||||
the timing rate between RTP timestamps and system time. All RTP packets (both audio and video) with `avsync_time <= 0`
|
||||
are **discarded** to avoid timestamp problems in the live source. RTCP Sender Reports typically arrive every 2-3 seconds.
|
||||
After the **second** SR arrives (~4-6 seconds), the A/V sync rate is calculated, and packets start being accepted. If DVR
|
||||
is configured with `dvr_wait_keyframe on`, recording starts at the first video keyframe anyway. Video keyframes typically
|
||||
arrive every 2-4 seconds, so by the time the first keyframe arrives, A/V sync is often already established. However, audio
|
||||
packets that arrived before sync was established are **permanently lost**.
|
||||
|
||||
**Why This Won't Be Fixed**: This is a **fundamental limitation** of WebRTC's A/V synchronization mechanism. The RTCP-based
|
||||
A/V synchronization is essential for WebRTC. Without it, audio and video timestamps would be misaligned, causing severe sync
|
||||
issues throughout the entire stream. The current design prioritizes **correct A/V synchronization** over capturing the first
|
||||
few seconds. This is a reasonable trade-off for most live streaming scenarios where streams run for extended periods (minutes
|
||||
to hours), losing 4-6 seconds at the start is acceptable, and perfect A/V sync throughout the stream is critical. Fixing this
|
||||
would require fundamentally redesigning the WebRTC A/V sync mechanism, which is extremely complex and risky.
|
||||
|
||||
**Related Issues**: [#4418](https://github.com/ossrs/srs/issues/4418), [#4151](https://github.com/ossrs/srs/issues/4151),
|
||||
[#4076](https://github.com/ossrs/srs/issues/4076)
|
||||
|
||||

|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -48,140 +48,92 @@ if [[ $SRS_OSX == YES ]]; then
|
|||
echo "Please install brew at https://brew.sh/"; exit $ret;
|
||||
fi
|
||||
fi
|
||||
# Check perl, which is depended by automake for building libopus etc.
|
||||
perl --version >/dev/null 2>/dev/null; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
if [[ $OS_IS_CENTOS == YES ]]; then
|
||||
echo "Please install perl by:"
|
||||
echo " yum install -y perl"
|
||||
elif [[ $OS_IS_UBUNTU == YES ]]; then
|
||||
echo "Please install perl by:"
|
||||
echo " apt install -y perl"
|
||||
else
|
||||
echo "Please install perl"
|
||||
|
||||
# Arrays to track missing dependencies
|
||||
MISSING_DEPS=()
|
||||
MISSING_DEPS_UBUNTU=()
|
||||
MISSING_DEPS_CENTOS=()
|
||||
MISSING_DEPS_OSX=()
|
||||
|
||||
# Helper function to check if a command exists
|
||||
check_command() {
|
||||
local cmd=$1
|
||||
local cmd_name=$2
|
||||
local ubuntu_pkg=$3
|
||||
local centos_pkg=$4
|
||||
local osx_pkg=$5
|
||||
|
||||
$cmd >/dev/null 2>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
MISSING_DEPS+=("$cmd_name")
|
||||
if [[ ! -z "$ubuntu_pkg" ]]; then
|
||||
MISSING_DEPS_UBUNTU+=("$ubuntu_pkg")
|
||||
fi
|
||||
if [[ ! -z "$centos_pkg" ]]; then
|
||||
MISSING_DEPS_CENTOS+=("$centos_pkg")
|
||||
fi
|
||||
if [[ ! -z "$osx_pkg" ]]; then
|
||||
MISSING_DEPS_OSX+=("$osx_pkg")
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
exit $ret;
|
||||
fi
|
||||
gcc --version >/dev/null 2>/dev/null; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
if [[ $OS_IS_CENTOS == YES ]]; then
|
||||
echo "Please install gcc by:"
|
||||
echo " yum install -y gcc"
|
||||
elif [[ $OS_IS_UBUNTU == YES ]]; then
|
||||
echo "Please install gcc by:"
|
||||
echo " apt install -y gcc"
|
||||
else
|
||||
echo "Please install gcc"
|
||||
fi
|
||||
exit $ret;
|
||||
fi
|
||||
g++ --version >/dev/null 2>/dev/null; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
if [[ $OS_IS_CENTOS == YES ]]; then
|
||||
echo "Please install g++ by:"
|
||||
echo " yum install -y gcc-c++"
|
||||
elif [[ $OS_IS_UBUNTU == YES ]]; then
|
||||
echo "Please install g++ by:"
|
||||
echo " apt install -y g++"
|
||||
else
|
||||
echo "Please install gcc-c++"
|
||||
fi
|
||||
exit $ret;
|
||||
fi
|
||||
make --version >/dev/null 2>/dev/null; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
if [[ $OS_IS_CENTOS == YES ]]; then
|
||||
echo "Please install make by:"
|
||||
echo " yum install -y make"
|
||||
elif [[ $OS_IS_UBUNTU == YES ]]; then
|
||||
echo "Please install make by:"
|
||||
echo " apt install -y make"
|
||||
else
|
||||
echo "Please install make"
|
||||
fi
|
||||
exit $ret;
|
||||
fi
|
||||
patch --version >/dev/null 2>/dev/null; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
if [[ $OS_IS_CENTOS == YES ]]; then
|
||||
echo "Please install patch by:"
|
||||
echo " yum install -y patch"
|
||||
elif [[ $OS_IS_UBUNTU == YES ]]; then
|
||||
echo "Please install patch by:"
|
||||
echo " apt install -y patch"
|
||||
else
|
||||
echo "Please install patch"
|
||||
fi
|
||||
exit $ret;
|
||||
fi
|
||||
unzip -v >/dev/null 2>/dev/null; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
if [[ $OS_IS_CENTOS == YES ]]; then
|
||||
echo "Please install unzip by:"
|
||||
echo " yum install -y unzip"
|
||||
elif [[ $OS_IS_UBUNTU == YES ]]; then
|
||||
echo "Please install unzip by:"
|
||||
echo " apt install -y unzip"
|
||||
else
|
||||
echo "Please install unzip"
|
||||
fi
|
||||
exit $ret;
|
||||
fi
|
||||
automake --version >/dev/null 2>/dev/null; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
if [[ $OS_IS_CENTOS == YES ]]; then
|
||||
echo "Please install automake by:"
|
||||
echo " yum install -y automake"
|
||||
elif [[ $OS_IS_UBUNTU == YES ]]; then
|
||||
echo "Please install automake by:"
|
||||
echo " apt install -y automake"
|
||||
else
|
||||
echo "Please install automake"
|
||||
fi
|
||||
exit $ret;
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check required tools
|
||||
echo "Checking required tools: perl gcc g++ make patch unzip automake pkg-config which"
|
||||
check_command "perl --version" "perl" "perl" "perl" "perl"
|
||||
check_command "gcc --version" "gcc" "gcc" "gcc" "gcc"
|
||||
check_command "g++ --version" "g++" "g++" "gcc-c++" "gcc"
|
||||
check_command "make --version" "make" "make" "make" "make"
|
||||
check_command "patch --version" "patch" "patch" "patch" "gpatch"
|
||||
check_command "unzip -v" "unzip" "unzip" "unzip" "unzip"
|
||||
check_command "automake --version" "automake" "automake" "automake" "automake"
|
||||
check_command "pkg-config --version" "pkg-config" "pkg-config" "pkgconfig" "pkg-config"
|
||||
check_command "which ls" "which" "which" "which" "which"
|
||||
|
||||
# Check optional tools for valgrind
|
||||
if [[ $SRS_VALGRIND == YES ]]; then
|
||||
valgrind --version >/dev/null 2>/dev/null; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
echo "Please install valgrind"; exit $ret;
|
||||
fi
|
||||
check_command "valgrind --version" "valgrind" "valgrind" "valgrind" "valgrind"
|
||||
if [[ ! -f /usr/include/valgrind/valgrind.h ]]; then
|
||||
echo "Please install valgrind-dev"; exit $ret;
|
||||
MISSING_DEPS+=("valgrind-dev")
|
||||
MISSING_DEPS_UBUNTU+=("valgrind")
|
||||
MISSING_DEPS_CENTOS+=("valgrind-devel")
|
||||
MISSING_DEPS_OSX+=("valgrind")
|
||||
fi
|
||||
fi
|
||||
# Check tclsh, which is depended by SRT.
|
||||
|
||||
# Check optional tools for SRT
|
||||
if [[ $SRS_SRT == YES ]]; then
|
||||
tclsh <<< "exit" >/dev/null 2>&1; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
if [[ $OS_IS_CENTOS == YES ]]; then
|
||||
echo "Please install tclsh by:"
|
||||
echo " yum install -y tcl"
|
||||
elif [[ $OS_IS_UBUNTU == YES ]]; then
|
||||
echo "Please install tclsh by:"
|
||||
echo " apt install -y tclsh"
|
||||
else
|
||||
echo "Please install tclsh"
|
||||
fi
|
||||
exit $ret;
|
||||
fi
|
||||
cmake --version >/dev/null 2>/dev/null; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
if [[ $OS_IS_CENTOS == YES ]]; then
|
||||
echo "Please install cmake by:"
|
||||
echo " yum install -y cmake"
|
||||
elif [[ $OS_IS_UBUNTU == YES ]]; then
|
||||
echo "Please install cmake by:"
|
||||
echo " apt install -y cmake"
|
||||
else
|
||||
echo "Please install cmake"
|
||||
fi
|
||||
exit $ret;
|
||||
echo "Checking optional tools for SRT: tclsh cmake"
|
||||
# Special check for tclsh
|
||||
tclsh <<< "exit" >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
MISSING_DEPS+=("tclsh")
|
||||
MISSING_DEPS_UBUNTU+=("tclsh")
|
||||
MISSING_DEPS_CENTOS+=("tcl")
|
||||
MISSING_DEPS_OSX+=("tcl-tk")
|
||||
fi
|
||||
check_command "cmake --version" "cmake" "cmake" "cmake" "cmake"
|
||||
fi
|
||||
pkg-config --version >/dev/null 2>/dev/null; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
echo "Please install pkg-config"; exit $ret;
|
||||
fi
|
||||
which ls >/dev/null 2>/dev/null; ret=$?; if [[ 0 -ne $ret ]]; then
|
||||
if [[ $OS_IS_CENTOS == YES ]]; then
|
||||
echo "Please install which by:"
|
||||
echo " yum install -y which"
|
||||
elif [[ $OS_IS_UBUNTU == YES ]]; then
|
||||
echo "Please install which by:"
|
||||
echo " apt install -y which"
|
||||
|
||||
# Report all missing dependencies at once
|
||||
if [[ ${#MISSING_DEPS[@]} -gt 0 ]]; then
|
||||
echo ""
|
||||
echo -e "Missing dependencies (${#MISSING_DEPS[@]}): ${RED}${MISSING_DEPS[@]}${BLACK}"
|
||||
|
||||
if [[ $OS_IS_UBUNTU == YES && ${#MISSING_DEPS_UBUNTU[@]} -gt 0 ]]; then
|
||||
echo -e "Please install missing dependencies by: ${GREEN}sudo apt install -y ${MISSING_DEPS_UBUNTU[@]}${BLACK}"
|
||||
elif [[ $OS_IS_CENTOS == YES && ${#MISSING_DEPS_CENTOS[@]} -gt 0 ]]; then
|
||||
echo -e "Please install missing dependencies by: ${GREEN}sudo yum install -y ${MISSING_DEPS_CENTOS[@]}${BLACK}"
|
||||
elif [[ $SRS_OSX == YES && ${#MISSING_DEPS_OSX[@]} -gt 0 ]]; then
|
||||
echo -e "Please install missing dependencies by: ${GREEN}brew install ${MISSING_DEPS_OSX[@]}${BLACK}"
|
||||
else
|
||||
echo "Please install which"
|
||||
echo "Please install the missing dependencies above."
|
||||
fi
|
||||
exit $ret;
|
||||
|
||||
echo "Please install the missing dependencies above and rerun configure."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#####################################################################################
|
||||
|
|
|
|||
|
|
@ -1123,6 +1123,8 @@ vhost hooks.callback.srs.com {
|
|||
# @remark For SRS4, the HTTPS url is supported, for example:
|
||||
# on_publish https://xxx/api0 https://xxx/api1 https://xxx/apiN
|
||||
# Overwrite by env SRS_VHOST_HTTP_HOOKS_ON_PUBLISH for all vhosts.
|
||||
# @remark When using environment variables, use space-separated URLs with proper quoting:
|
||||
# SRS_VHOST_HTTP_HOOKS_ON_PUBLISH="https://xxx/api0 https://xxx/api1"
|
||||
on_publish http://127.0.0.1:8085/api/v1/streams http://localhost:8085/api/v1/streams;
|
||||
# when client(encoder) stop publish to vhost/app/stream, call the hook,
|
||||
# the request in the POST data string is a object encode by json:
|
||||
|
|
@ -1150,8 +1152,12 @@ vhost hooks.callback.srs.com {
|
|||
# "ip": "192.168.1.10", "vhost": "video.test.com", "app": "live",
|
||||
# "stream": "livestream", "param":"?token=xxx&salt=yyy",
|
||||
# "pageUrl": "http://www.test.com/live.html", "server_id": "vid-werty",
|
||||
# "stream_url": "video.test.com/live/livestream", "stream_id": "vid-124q9y3"
|
||||
# "stream_url": "video.test.com/live/livestream", "stream_id": "vid-124q9y3",
|
||||
# "clients": 3
|
||||
# }
|
||||
# Note: The "clients" field indicates the current number of clients playing this stream
|
||||
# (including the client that just started playing). This is useful for on-demand streaming
|
||||
# scenarios where you want to start a publisher when the first client connects (clients=1).
|
||||
# if valid, the hook must return HTTP code 200(Status OK) and response
|
||||
# an int value specifies the error code(0 corresponding to success):
|
||||
# 0
|
||||
|
|
@ -1160,6 +1166,8 @@ vhost hooks.callback.srs.com {
|
|||
# @remark For SRS4, the HTTPS url is supported, for example:
|
||||
# on_play https://xxx/api0 https://xxx/api1 https://xxx/apiN
|
||||
# Overwrite by env SRS_VHOST_HTTP_HOOKS_ON_PLAY for all vhosts.
|
||||
# @remark When using environment variables, use space-separated URLs with proper quoting:
|
||||
# SRS_VHOST_HTTP_HOOKS_ON_PLAY="https://xxx/api0 https://xxx/api1"
|
||||
on_play http://127.0.0.1:8085/api/v1/sessions http://localhost:8085/api/v1/sessions;
|
||||
# when client stop to play vhost/app/stream, call the hook,
|
||||
# the request in the POST data string is a object encode by json:
|
||||
|
|
@ -1168,8 +1176,12 @@ vhost hooks.callback.srs.com {
|
|||
# "client_id": "9308h583",
|
||||
# "ip": "192.168.1.10", "vhost": "video.test.com", "app": "live",
|
||||
# "stream": "livestream", "param":"?token=xxx&salt=yyy", "server_id": "vid-werty",
|
||||
# "stream_url": "video.test.com/live/livestream", "stream_id": "vid-124q9y3"
|
||||
# "stream_url": "video.test.com/live/livestream", "stream_id": "vid-124q9y3",
|
||||
# "clients": 2
|
||||
# }
|
||||
# Note: The "clients" field indicates the current number of clients still playing this stream
|
||||
# (after the client has stopped). This is useful for on-demand streaming scenarios where you
|
||||
# want to stop a publisher when the last client disconnects (clients=0).
|
||||
# if valid, the hook must return HTTP code 200(Status OK) and response
|
||||
# an int value specifies the error code(0 corresponding to success):
|
||||
# 0
|
||||
|
|
@ -1474,6 +1486,16 @@ vhost hls.srs.com {
|
|||
# Overwrite by env SRS_VHOST_HLS_HLS_TS_CTX for all vhosts.
|
||||
# Default: on
|
||||
hls_ts_ctx on;
|
||||
# Whether use relative path for media playlist URL in HLS master playlist.
|
||||
# When on, the master playlist uses relative path like "livestream.m3u8?hls_ctx=xxx".
|
||||
# When off, the master playlist uses absolute path like "/live/livestream.m3u8?hls_ctx=xxx".
|
||||
# Relative path is useful for reverse proxy scenarios with path rewriting.
|
||||
# For example, if external URL is "http://proxy/srs/live/stream.m3u8" and it's rewritten to
|
||||
# "http://origin/live/stream.m3u8", relative path ensures the player can correctly resolve
|
||||
# the media playlist URL.
|
||||
# Overwrite by env SRS_VHOST_HLS_HLS_MASTER_M3U8_PATH_RELATIVE for all vhosts.
|
||||
# Default: off
|
||||
hls_master_m3u8_path_relative off;
|
||||
|
||||
# whether using AES encryption.
|
||||
# Overwrite by env SRS_VHOST_HLS_HLS_KEYS for all vhosts.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
# http-hooks or http-callbacks config for srs.
|
||||
# @see full.conf for detail config.
|
||||
#
|
||||
# Multiple URLs can be specified for each hook (space-separated).
|
||||
# When using environment variables, use space-separated URLs with proper quoting:
|
||||
# SRS_VHOST_HTTP_HOOKS_ON_PLAY="http://url1 http://url2"
|
||||
|
||||
max_connections 1000;
|
||||
daemon off;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,14 @@ The changelog for SRS.
|
|||
<a name="v7-changes"></a>
|
||||
|
||||
## SRS 7.0 Changelog
|
||||
* v7.0, 2025-10-27, AI: HTTP-FLV: Enforce minimum 10ms sleep to prevent CPU busy-wait when mw_latency=0. v7.0.110 (#3963)
|
||||
* v7.0, 2025-10-26, AI: Edge: Fix stream names with dots being incorrectly truncated in source URL generation. v7.0.109 (#4011)
|
||||
* v7.0, 2025-10-26, AI: HTTPS: Handle SSL_ERROR_ZERO_RETURN as graceful connection closure. v7.0.108 (#4036)
|
||||
* v7.0, 2025-10-26, AI: API: Add clients field to on_play/on_stop webhooks and total field to HTTP API. v7.0.107 (#4147)
|
||||
* v7.0, 2025-10-26, AI: WebRTC: Fix camera/microphone not released after closing publisher. v7.0.106 (#4261)
|
||||
* v7.0, 2025-10-26, AI: Build: Improve dependency checking to report all missing dependencies at once. v7.0.105 (#4293)
|
||||
* v7.0, 2025-10-26, AI: HLS: Support hls_master_m3u8_path_relative for reverse proxy compatibility. v7.0.104 (#4338)
|
||||
* v7.0, 2025-10-25, AI: API: Remove minimum limit of 10 for count parameter in /api/v1/streams and /api/v1/clients. v7.0.103 (#4358)
|
||||
* v7.0, 2025-10-22, AI: Only support AAC/MP3/Opus audio codec. v7.0.102 (#4516)
|
||||
* v7.0, 2025-10-22, AI: Fix AAC audio sample rate reporting in API. v7.0.101 (#4518)
|
||||
* v7.0, 2025-10-20, Merge [#4537](https://github.com/ossrs/srs/pull/4537): Forward: Reject RTMPS destinations with clear error message. v7.0.100 (#4537)
|
||||
|
|
|
|||
|
|
@ -15,501 +15,6 @@ function SrsError(name, message) {
|
|||
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() {
|
||||
|
|
@ -523,6 +28,10 @@ function SrsRtcWhipWhepAsync() {
|
|||
}
|
||||
};
|
||||
|
||||
// Store media streams to stop tracks when closing.
|
||||
self.displayStream = null;
|
||||
self.userStream = null;
|
||||
|
||||
// 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
|
||||
|
|
@ -557,11 +66,11 @@ function SrsRtcWhipWhepAsync() {
|
|||
}
|
||||
|
||||
if (useScreen) {
|
||||
const displayStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
self.displayStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: true
|
||||
});
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||
displayStream.getTracks().forEach(function (track) {
|
||||
self.displayStream.getTracks().forEach(function (track) {
|
||||
self.pc.addTrack(track);
|
||||
// Notify about local track when stream is ok.
|
||||
self.ontrack && self.ontrack({track: track});
|
||||
|
|
@ -569,9 +78,9 @@ function SrsRtcWhipWhepAsync() {
|
|||
}
|
||||
|
||||
if (useCamera || hasAudio) {
|
||||
const userStream = await navigator.mediaDevices.getUserMedia(self.constraints);
|
||||
self.userStream = await navigator.mediaDevices.getUserMedia(self.constraints);
|
||||
|
||||
userStream.getTracks().forEach(function (track) {
|
||||
self.userStream.getTracks().forEach(function (track) {
|
||||
self.pc.addTrack(track);
|
||||
// Notify about local track when stream is ok.
|
||||
self.ontrack && self.ontrack({track: track});
|
||||
|
|
@ -643,6 +152,20 @@ function SrsRtcWhipWhepAsync() {
|
|||
self.close = function () {
|
||||
self.pc && self.pc.close();
|
||||
self.pc = null;
|
||||
|
||||
// Stop all media tracks to release camera/microphone.
|
||||
if (self.displayStream) {
|
||||
self.displayStream.getTracks().forEach(function (track) {
|
||||
track.stop();
|
||||
});
|
||||
self.displayStream = null;
|
||||
}
|
||||
if (self.userStream) {
|
||||
self.userStream.getTracks().forEach(function (track) {
|
||||
track.stop();
|
||||
});
|
||||
self.userStream = null;
|
||||
}
|
||||
};
|
||||
|
||||
// The callback when got local stream.
|
||||
|
|
|
|||
|
|
@ -67,6 +67,31 @@
|
|||
</div>
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
// Convert webrtc:// URL to WHEP URL
|
||||
// webrtc://domain:port/app/stream => http://domain:1985/rtc/v1/whep/?app=app&stream=stream
|
||||
var convertToWhepUrl = function(webrtcUrl) {
|
||||
var a = document.createElement("a");
|
||||
a.href = webrtcUrl.replace("webrtc://", "http://").replace("rtc://", "http://");
|
||||
|
||||
var schema = window.location.protocol;
|
||||
var port = a.port || 1985;
|
||||
if (schema === 'https:') {
|
||||
port = a.port || 443;
|
||||
}
|
||||
|
||||
var app = a.pathname.substring(1, a.pathname.lastIndexOf("/"));
|
||||
var stream = a.pathname.slice(a.pathname.lastIndexOf("/") + 1);
|
||||
|
||||
var whepUrl = schema + '//' + a.hostname + ':' + port + '/rtc/v1/whep/?app=' + app + '&stream=' + stream;
|
||||
|
||||
// Append query parameters from original URL
|
||||
if (a.search) {
|
||||
whepUrl += '&' + a.search.substring(1);
|
||||
}
|
||||
|
||||
return whepUrl;
|
||||
};
|
||||
|
||||
var sdk = null; // Global handler to do cleanup when replaying.
|
||||
var startPlay = function() {
|
||||
$('#rtc_media_player').show();
|
||||
|
|
@ -75,7 +100,7 @@ $(function(){
|
|||
if (sdk) {
|
||||
sdk.close();
|
||||
}
|
||||
sdk = new SrsRtcPlayerAsync();
|
||||
sdk = new SrsRtcWhipWhepAsync();
|
||||
|
||||
// https://webrtc.org/getting-started/remote-streams
|
||||
$('#rtc_media_player').prop('srcObject', sdk.stream);
|
||||
|
|
@ -83,8 +108,10 @@ $(function(){
|
|||
// sdk.ontrack = function (event) { console.log('Got track', event); sdk.stream.addTrack(event.track); };
|
||||
|
||||
// For example: webrtc://r.ossrs.net/live/livestream
|
||||
// Convert to WHEP URL: http://localhost:1985/rtc/v1/whep/?app=live&stream=livestream
|
||||
var url = $("#txt_url").val();
|
||||
sdk.play(url).then(function(session){
|
||||
var whepUrl = convertToWhepUrl(url);
|
||||
sdk.play(whepUrl).then(function(session){
|
||||
$('#sessionid').html(session.sessionid);
|
||||
$('#simulator-drop').attr('href', session.simulator + '?drop=1&username=' + session.sessionid);
|
||||
}).catch(function (reason) {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,33 @@
|
|||
</div>
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
// Convert webrtc:// URL to WHIP URL
|
||||
// webrtc://domain:port/app/stream => http://domain:1985/rtc/v1/whip/?app=app&stream=stream
|
||||
var convertToWhipUrl = function(webrtcUrl) {
|
||||
var a = document.createElement("a");
|
||||
a.href = webrtcUrl.replace("webrtc://", "http://").replace("rtc://", "http://");
|
||||
|
||||
var schema = window.location.protocol;
|
||||
var port = a.port || 1985;
|
||||
if (schema === 'https:') {
|
||||
port = a.port || 443;
|
||||
}
|
||||
|
||||
var app = a.pathname.substring(1, a.pathname.lastIndexOf("/"));
|
||||
var stream = a.pathname.slice(a.pathname.lastIndexOf("/") + 1);
|
||||
|
||||
var whipUrl = schema + '//' + a.hostname + ':' + port + '/rtc/v1/whip/?app=' + app + '&stream=' + stream;
|
||||
|
||||
// Append query parameters from original URL
|
||||
if (a.search) {
|
||||
whipUrl += '&' + a.search.substring(1);
|
||||
}
|
||||
|
||||
return whipUrl;
|
||||
};
|
||||
|
||||
var sdk = null; // Global handler to do cleanup when republishing.
|
||||
var statsTimer = null; // Timer for getting codec stats.
|
||||
var startPublish = function() {
|
||||
$('#rtc_media_player').show();
|
||||
|
||||
|
|
@ -79,7 +105,11 @@ $(function(){
|
|||
if (sdk) {
|
||||
sdk.close();
|
||||
}
|
||||
sdk = new SrsRtcPublisherAsync();
|
||||
if (statsTimer) {
|
||||
clearInterval(statsTimer);
|
||||
statsTimer = null;
|
||||
}
|
||||
sdk = new SrsRtcWhipWhepAsync();
|
||||
|
||||
// User should set the stream when publish is done, @see https://webrtc.org/getting-started/media-devices
|
||||
// However SRS SDK provides a consist API like https://webrtc.org/getting-started/remote-streams
|
||||
|
|
@ -87,17 +117,27 @@ $(function(){
|
|||
// Optional callback, SDK will add track to stream.
|
||||
// sdk.ontrack = function (event) { console.log('Got track', event); sdk.stream.addTrack(event.track); };
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/WebRTC_codecs#getting_the_supported_codecs
|
||||
sdk.pc.onicegatheringstatechange = function (event) {
|
||||
if (sdk.pc.iceGatheringState === "complete") {
|
||||
$('#acodecs').html(SrsRtcFormatSenders(sdk.pc.getSenders(), "audio"));
|
||||
$('#vcodecs').html(SrsRtcFormatSenders(sdk.pc.getSenders(), "video"));
|
||||
}
|
||||
};
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getStats
|
||||
statsTimer = setInterval(function() {
|
||||
sdk.pc.getStats(null).then(function(stats) {
|
||||
var audioStatsOutput = SrsRtcFormatStats(stats, 'audio');
|
||||
var videoStatsOutput = SrsRtcFormatStats(stats, 'video');
|
||||
|
||||
$('#acodecs').html(audioStatsOutput);
|
||||
$('#vcodecs').html(videoStatsOutput);
|
||||
|
||||
if (audioStatsOutput && videoStatsOutput) {
|
||||
clearInterval(statsTimer);
|
||||
statsTimer = null;
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// For example: webrtc://r.ossrs.net/live/livestream
|
||||
// Convert to WHIP URL: http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream
|
||||
var url = $("#txt_url").val();
|
||||
sdk.publish(url).then(function(session){
|
||||
var whipUrl = convertToWhipUrl(url);
|
||||
sdk.publish(whipUrl).then(function(session){
|
||||
$('#sessionid').html(session.sessionid);
|
||||
$('#simulator-drop').attr('href', session.simulator + '?drop=1&username=' + session.sessionid);
|
||||
}).catch(function (reason) {
|
||||
|
|
|
|||
|
|
@ -446,8 +446,34 @@
|
|||
}
|
||||
|
||||
|
||||
// Async-await-promise based SRS RTC Player.
|
||||
function SrsRtcPlayerAsync() {
|
||||
// Convert webrtc:// URL to WHEP URL
|
||||
// webrtc://domain:port/app/stream => http://domain:1985/rtc/v1/whep/?app=app&stream=stream
|
||||
function convertToWhepUrl(webrtcUrl) {
|
||||
var a = document.createElement("a");
|
||||
a.href = webrtcUrl.replace("webrtc://", "http://").replace("rtc://", "http://");
|
||||
|
||||
var schema = window.location.protocol;
|
||||
var port = a.port || 1985;
|
||||
if (schema === 'https:') {
|
||||
port = a.port || 443;
|
||||
}
|
||||
|
||||
var app = a.pathname.substring(1, a.pathname.lastIndexOf("/"));
|
||||
var stream = a.pathname.slice(a.pathname.lastIndexOf("/") + 1);
|
||||
|
||||
var whepUrl = schema + '//' + a.hostname + ':' + port + '/rtc/v1/whep/?app=' + app + '&stream=' + stream;
|
||||
|
||||
// Append query parameters from original URL
|
||||
if (a.search) {
|
||||
whepUrl += '&' + a.search.substring(1);
|
||||
}
|
||||
|
||||
return whepUrl;
|
||||
}
|
||||
|
||||
// Removed embedded SrsRtcPlayerAsync - now using SrsRtcWhipWhepAsync from srs.sdk.js
|
||||
// The old API-based player is deprecated, use WHIP/WHEP instead
|
||||
function SrsRtcPlayerAsync_Deprecated() {
|
||||
var self = {};
|
||||
|
||||
// @see https://github.com/rtcdn/rtcdn-draft
|
||||
|
|
@ -1018,16 +1044,17 @@
|
|||
sdk.close();
|
||||
}
|
||||
|
||||
sdk = new SrsRtcPlayerAsync();
|
||||
sdk.onaddstream = function (event) {
|
||||
console.log('Start play, event: ', event);
|
||||
$('#rtc_media_player').prop('srcObject', event.stream);
|
||||
};
|
||||
sdk = new SrsRtcWhipWhepAsync();
|
||||
|
||||
// https://webrtc.org/getting-started/remote-streams
|
||||
$('#rtc_media_player').prop('srcObject', sdk.stream);
|
||||
|
||||
// For example:
|
||||
// webrtc://r.ossrs.net/live/livestream
|
||||
// Convert to WHEP URL: http://localhost:1985/rtc/v1/whep/?app=live&stream=livestream
|
||||
var url = $("#txt_rtc_url").val();
|
||||
sdk.play(url).then(function(session){
|
||||
var whepUrl = convertToWhepUrl(url);
|
||||
sdk.play(whepUrl).then(function(session){
|
||||
$('#sessionid').html(session.sessionid);
|
||||
$('#simulator-drop').attr('href', session.simulator + '?drop=1&username=' + session.sessionid);
|
||||
}).catch(function (reason) {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
URL:
|
||||
<input type="text" id="txt_url" class="input-xxlarge" value="">
|
||||
<button class="btn btn-primary" id="btn_play">Play</button>
|
||||
<button class="btn btn-danger" id="btn_stop" disabled>Stop</button>
|
||||
</div>
|
||||
|
||||
<p></p>
|
||||
|
|
@ -130,6 +131,9 @@ $(function(){
|
|||
}).then(function(session){
|
||||
$('#sessionid').html(session.sessionid);
|
||||
$('#simulator-drop').attr('href', session.simulator + '?drop=1&username=' + session.sessionid);
|
||||
// Enable stop button after successful play
|
||||
$('#btn_stop').prop('disabled', false);
|
||||
$('#btn_play').prop('disabled', true);
|
||||
}).catch(function (reason) {
|
||||
sdk.close();
|
||||
$('#rtc_media_player').hide();
|
||||
|
|
@ -137,11 +141,28 @@ $(function(){
|
|||
});
|
||||
};
|
||||
|
||||
var stopPlay = function() {
|
||||
if (sdk) {
|
||||
sdk.close();
|
||||
|
||||
if (statsTimer) {
|
||||
clearInterval(statsTimer);
|
||||
statsTimer = null;
|
||||
}
|
||||
|
||||
$('#btn_stop').prop('disabled', true);
|
||||
$('#btn_play').prop('disabled', false);
|
||||
$('#sessionid').html('(stopped)');
|
||||
$('#rtc_media_player').hide();
|
||||
}
|
||||
};
|
||||
|
||||
$('#rtc_media_player').hide();
|
||||
var query = parse_query_string();
|
||||
srs_init_whep("#txt_url", query);
|
||||
|
||||
$("#btn_play").click(startPlay);
|
||||
$("#btn_stop").click(stopPlay);
|
||||
// Never play util windows loaded @see https://github.com/ossrs/srs/issues/2732
|
||||
if (query.autostart === 'true') {
|
||||
$('#rtc_media_player').prop('muted', true);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
URL:
|
||||
<input type="text" id="txt_url" class="input-xxlarge" value="">
|
||||
<button class="btn btn-primary" id="btn_publish">Publish</button>
|
||||
<button class="btn btn-danger" id="btn_stop" disabled>Stop</button>
|
||||
</div>
|
||||
|
||||
<p></p>
|
||||
|
|
@ -138,6 +139,9 @@ $(function(){
|
|||
}).then(function(session){
|
||||
$('#sessionid').html(session.sessionid);
|
||||
$('#simulator-drop').attr('href', session.simulator + '?drop=1&username=' + session.sessionid);
|
||||
// Enable stop button after successful publish
|
||||
$('#btn_stop').prop('disabled', false);
|
||||
$('#btn_publish').prop('disabled', true);
|
||||
}).catch(function (reason) {
|
||||
// Throw by sdk.
|
||||
if (reason instanceof SrsError) {
|
||||
|
|
@ -164,11 +168,29 @@ $(function(){
|
|||
});
|
||||
};
|
||||
|
||||
var stopPublish = function() {
|
||||
if (sdk) {
|
||||
sdk.close();
|
||||
|
||||
if (statsTimer) {
|
||||
clearInterval(statsTimer);
|
||||
statsTimer = null;
|
||||
}
|
||||
|
||||
$('#btn_stop').prop('disabled', true);
|
||||
$('#btn_publish').prop('disabled', false);
|
||||
$('#sessionid').html('(stopped)');
|
||||
$('#rtc_media_player').hide();
|
||||
console.log('PeerConnection closed. Check if camera indicator is still on!');
|
||||
}
|
||||
};
|
||||
|
||||
$('#rtc_media_player').hide();
|
||||
var query = parse_query_string();
|
||||
srs_init_whip("#txt_url", query);
|
||||
|
||||
$("#btn_publish").click(startPublish);
|
||||
$("#btn_stop").click(stopPublish);
|
||||
// Never play util windows loaded @see https://github.com/ossrs/srs/issues/2732
|
||||
if (query.autostart === 'true') {
|
||||
window.addEventListener("load", function(){ startPublish(); });
|
||||
|
|
|
|||
|
|
@ -2290,7 +2290,7 @@ srs_error_t SrsConfig::check_normal_config()
|
|||
} else if (n == "hls") {
|
||||
for (int j = 0; j < (int)conf->directives_.size(); j++) {
|
||||
string m = conf->at(j)->name_;
|
||||
if (m != "enabled" && m != "hls_entry_prefix" && m != "hls_path" && m != "hls_fragment" && m != "hls_window" && m != "hls_on_error" && m != "hls_storage" && m != "hls_mount" && m != "hls_td_ratio" && m != "hls_aof_ratio" && m != "hls_acodec" && m != "hls_vcodec" && m != "hls_m3u8_file" && m != "hls_ts_file" && m != "hls_ts_floor" && m != "hls_cleanup" && m != "hls_nb_notify" && m != "hls_wait_keyframe" && m != "hls_dispose" && m != "hls_keys" && m != "hls_fragments_per_key" && m != "hls_key_file" && m != "hls_key_file_path" && m != "hls_key_url" && m != "hls_dts_directly" && m != "hls_ctx" && m != "hls_ts_ctx" && m != "hls_use_fmp4" && m != "hls_fmp4_file" && m != "hls_init_file" && m != "hls_recover") {
|
||||
if (m != "enabled" && m != "hls_entry_prefix" && m != "hls_path" && m != "hls_fragment" && m != "hls_window" && m != "hls_on_error" && m != "hls_storage" && m != "hls_mount" && m != "hls_td_ratio" && m != "hls_aof_ratio" && m != "hls_acodec" && m != "hls_vcodec" && m != "hls_m3u8_file" && m != "hls_ts_file" && m != "hls_ts_floor" && m != "hls_cleanup" && m != "hls_nb_notify" && m != "hls_wait_keyframe" && m != "hls_dispose" && m != "hls_keys" && m != "hls_fragments_per_key" && m != "hls_key_file" && m != "hls_key_file_path" && m != "hls_key_url" && m != "hls_dts_directly" && m != "hls_ctx" && m != "hls_ts_ctx" && m != "hls_use_fmp4" && m != "hls_fmp4_file" && m != "hls_init_file" && m != "hls_recover" && m != "hls_master_m3u8_path_relative") {
|
||||
return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal vhost.hls.%s of %s", m.c_str(), vhost->arg0().c_str());
|
||||
}
|
||||
|
||||
|
|
@ -6692,6 +6692,25 @@ string SrsConfig::get_hls_key_url(std::string vhost)
|
|||
return conf->arg0();
|
||||
}
|
||||
|
||||
bool SrsConfig::get_hls_master_m3u8_path_relative(string vhost)
|
||||
{
|
||||
SRS_OVERWRITE_BY_ENV_BOOL("srs.vhost.hls.hls_master_m3u8_path_relative"); // SRS_VHOST_HLS_HLS_MASTER_M3U8_PATH_RELATIVE
|
||||
|
||||
static bool DEFAULT = false;
|
||||
|
||||
SrsConfDirective *conf = get_hls(vhost);
|
||||
if (!conf) {
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
conf = conf->get("hls_master_m3u8_path_relative");
|
||||
if (!conf || conf->arg0().empty()) {
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
return SRS_CONF_PREFER_FALSE(conf->arg0());
|
||||
}
|
||||
|
||||
bool SrsConfig::get_hls_recover(string vhost)
|
||||
{
|
||||
SRS_OVERWRITE_BY_ENV_BOOL2("srs.vhost.hls.hls_recover"); // SRS_VHOST_HLS_HLS_RECOVER
|
||||
|
|
|
|||
|
|
@ -519,6 +519,7 @@ public:
|
|||
virtual bool get_vhost_hls_dts_directly(std::string vhost) = 0;
|
||||
virtual bool get_hls_ctx_enabled(std::string vhost) = 0;
|
||||
virtual bool get_hls_ts_ctx_enabled(std::string vhost) = 0;
|
||||
virtual bool get_hls_master_m3u8_path_relative(std::string vhost) = 0;
|
||||
virtual bool get_hls_recover(std::string vhost) = 0;
|
||||
virtual bool get_dash_enabled(std::string vhost) = 0;
|
||||
virtual bool get_dash_enabled(SrsConfDirective *vhost) = 0;
|
||||
|
|
@ -1366,6 +1367,11 @@ public:
|
|||
// Whether enable session for ts file.
|
||||
// The ts file including .ts file for MPEG-ts segment, .m4s file and init.mp4 file for fmp4 segment.
|
||||
virtual bool get_hls_ts_ctx_enabled(std::string vhost);
|
||||
// Whether use relative path for media playlist URL in HLS master playlist.
|
||||
// When on, uses relative path (e.g., "livestream.m3u8?hls_ctx=xxx").
|
||||
// When off, uses absolute path (e.g., "/live/livestream.m3u8?hls_ctx=xxx").
|
||||
// Default is off for backward compatibility.
|
||||
virtual bool get_hls_master_m3u8_path_relative(std::string vhost);
|
||||
// Toggles HLS recover mode.
|
||||
// In this mode HLS sequence number is started from where it stopped last time.
|
||||
// Old fragments are kept. Default is on.
|
||||
|
|
|
|||
|
|
@ -781,13 +781,23 @@ srs_error_t SrsGoApiStreams::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa
|
|||
|
||||
if (r->is_http_get()) {
|
||||
if (!stream) {
|
||||
// Add total count of streams
|
||||
int64_t send_bytes = 0, recv_bytes = 0, nstreams = 0, nclients = 0, total_nclients = 0, nerrs = 0;
|
||||
if ((err = stat_->dumps_metrics(send_bytes, recv_bytes, nstreams, nclients, total_nclients, nerrs)) != srs_success) {
|
||||
int code = srs_error_code(err);
|
||||
srs_freep(err);
|
||||
return srs_api_response_code(w, r, code);
|
||||
}
|
||||
obj->set("total", SrsJsonAny::integer(nstreams));
|
||||
|
||||
// Add streams
|
||||
SrsJsonArray *data = SrsJsonAny::array();
|
||||
obj->set("streams", data);
|
||||
|
||||
std::string rstart = r->query_get("start");
|
||||
std::string rcount = r->query_get("count");
|
||||
int start = srs_max(0, atoi(rstart.c_str()));
|
||||
int count = srs_max(10, atoi(rcount.c_str()));
|
||||
int count = srs_max(1, atoi(rcount.c_str()));
|
||||
if ((err = stat_->dumps_streams(data, start, count)) != srs_success) {
|
||||
int code = srs_error_code(err);
|
||||
srs_freep(err);
|
||||
|
|
@ -843,13 +853,23 @@ srs_error_t SrsGoApiClients::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa
|
|||
|
||||
if (r->is_http_get()) {
|
||||
if (!client) {
|
||||
// Add total count of clients
|
||||
int64_t send_bytes = 0, recv_bytes = 0, nstreams = 0, nclients = 0, total_nclients = 0, nerrs = 0;
|
||||
if ((err = stat_->dumps_metrics(send_bytes, recv_bytes, nstreams, nclients, total_nclients, nerrs)) != srs_success) {
|
||||
int code = srs_error_code(err);
|
||||
srs_freep(err);
|
||||
return srs_api_response_code(w, r, code);
|
||||
}
|
||||
obj->set("total", SrsJsonAny::integer(nclients));
|
||||
|
||||
// Add clients
|
||||
SrsJsonArray *data = SrsJsonAny::array();
|
||||
obj->set("clients", data);
|
||||
|
||||
std::string rstart = r->query_get("start");
|
||||
std::string rcount = r->query_get("count");
|
||||
int start = srs_max(0, atoi(rstart.c_str()));
|
||||
int count = srs_max(10, atoi(rcount.c_str()));
|
||||
int count = srs_max(1, atoi(rcount.c_str()));
|
||||
if ((err = stat_->dumps_clients(data, start, count)) != srs_success) {
|
||||
int code = srs_error_code(err);
|
||||
srs_freep(err);
|
||||
|
|
|
|||
|
|
@ -241,6 +241,7 @@ srs_error_t SrsHttpHooks::on_play(string url, ISrsRequest *req)
|
|||
SrsStatisticStream *stream = stat->find_stream_by_url(req->get_stream_url());
|
||||
if (stream) {
|
||||
obj->set("stream_id", SrsJsonAny::str(stream->id_.c_str()));
|
||||
obj->set("clients", SrsJsonAny::integer(stream->nb_clients_));
|
||||
}
|
||||
|
||||
std::string data = obj->dumps();
|
||||
|
|
@ -282,6 +283,7 @@ void SrsHttpHooks::on_stop(string url, ISrsRequest *req)
|
|||
SrsStatisticStream *stream = stat->find_stream_by_url(req->get_stream_url());
|
||||
if (stream) {
|
||||
obj->set("stream_id", SrsJsonAny::str(stream->id_.c_str()));
|
||||
obj->set("clients", SrsJsonAny::integer(stream->nb_clients_));
|
||||
}
|
||||
|
||||
std::string data = obj->dumps();
|
||||
|
|
|
|||
|
|
@ -178,10 +178,21 @@ srs_error_t SrsHlsStream::serve_new_session(ISrsHttpResponseWriter *w, ISrsHttpM
|
|||
return srs_error_wrap(err, "HLS: http_hooks_on_play");
|
||||
}
|
||||
|
||||
// Determine the media playlist URL path in master playlist based on configuration.
|
||||
// When hls_master_m3u8_path_relative is on, use relative path (just filename) for better
|
||||
// compatibility with reverse proxies that do path rewriting.
|
||||
// For example, convert "/live/livestream.m3u8" to "livestream.m3u8"
|
||||
// When off (default), use absolute path for backward compatibility.
|
||||
std::string media_playlist_url = hr->path();
|
||||
if (_srs_config->get_hls_master_m3u8_path_relative(req->vhost_)) {
|
||||
SrsPath path;
|
||||
media_playlist_url = path.filepath_base(hr->path());
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "#EXTM3U" << SRS_CONSTS_LF;
|
||||
ss << "#EXT-X-STREAM-INF:BANDWIDTH=1,AVERAGE-BANDWIDTH=1" << SRS_CONSTS_LF;
|
||||
ss << hr->path() << "?" << SRS_CONTEXT_IN_HLS << "=" << ctx;
|
||||
ss << media_playlist_url << "?" << SRS_CONTEXT_IN_HLS << "=" << ctx;
|
||||
if (!hr->query().empty() && hr->query_get(SRS_CONTEXT_IN_HLS).empty()) {
|
||||
ss << "&" << hr->query();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -851,6 +851,13 @@ srs_error_t SrsLiveStream::do_serve_http(SrsLiveSource *source, ISrsLiveConsumer
|
|||
}
|
||||
|
||||
srs_utime_t mw_sleep = config_->get_mw_sleep(req_->vhost_);
|
||||
|
||||
// For HTTP FLV, we must wait for some milliseconds to avoid the spin.
|
||||
// See https://github.com/ossrs/srs/issues/3963 for more details.
|
||||
if (mw_sleep == 0) {
|
||||
mw_sleep = 10 * SRS_UTIME_MILLISECONDS;
|
||||
}
|
||||
|
||||
srs_trace("FLV %s, encoder=%s, mw_sleep=%dms, cache=%d, msgs=%d, dinm=%d, guess_av=%d/%d/%d",
|
||||
entry_->pattern.c_str(), enc_desc.c_str(), srsu2msi(mw_sleep), enc->has_cache(), msgs.max_, drop_if_not_match,
|
||||
has_audio, has_video, guess_has_av);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@
|
|||
|
||||
#define VERSION_MAJOR 7
|
||||
#define VERSION_MINOR 0
|
||||
#define VERSION_REVISION 102
|
||||
#define VERSION_REVISION 110
|
||||
|
||||
#endif
|
||||
|
|
@ -533,6 +533,12 @@ srs_error_t SrsSslConnection::read(void *plaintext, size_t nn_plaintext, ssize_t
|
|||
int r2 = BIO_ctrl_pending(bio_in_);
|
||||
int r3 = SSL_is_init_finished(ssl_);
|
||||
|
||||
// Peer gracefully close.
|
||||
if (r0 == 0 && r1 == SSL_ERROR_ZERO_RETURN) {
|
||||
return srs_error_new(ERROR_SOCKET_READ, "SSL_read r0=%d, r1=%d, r2=%d, r3=%d",
|
||||
r0, r1, r2, r3);
|
||||
}
|
||||
|
||||
// OK, got data.
|
||||
if (r0 > 0) {
|
||||
srs_assert(r0 <= (int)nn_plaintext);
|
||||
|
|
|
|||
|
|
@ -198,6 +198,12 @@ srs_error_t SrsSslClient::read(void *plaintext, size_t nn_plaintext, ssize_t *nr
|
|||
int r2 = BIO_ctrl_pending(bio_in_);
|
||||
int r3 = SSL_is_init_finished(ssl_);
|
||||
|
||||
// Peer gracefully close.
|
||||
if (r0 == 0 && r1 == SSL_ERROR_ZERO_RETURN) {
|
||||
return srs_error_new(ERROR_SOCKET_READ, "SSL_read r0=%d, r1=%d, r2=%d, r3=%d",
|
||||
r0, r1, r2, r3);
|
||||
}
|
||||
|
||||
// OK, got data.
|
||||
if (r0 > 0) {
|
||||
srs_assert(r0 <= (int)nn_plaintext);
|
||||
|
|
|
|||
|
|
@ -260,9 +260,21 @@ string srs_net_url_encode_sid(string vhost, string app, string stream)
|
|||
url += vhost;
|
||||
}
|
||||
url += "/" + app;
|
||||
// Note that we ignore any extension.
|
||||
SrsPath path;
|
||||
url += "/" + path.filepath_filename(stream);
|
||||
|
||||
// Strip only known streaming extensions, not arbitrary dots in stream names.
|
||||
// This fixes issue 4011 where stream names like "WavMain.exe_rooms_290" were
|
||||
// incorrectly truncated to "WavMain" because filepath_filename() stripped
|
||||
// everything after any dot.
|
||||
std::string stream_name = stream;
|
||||
size_t pos = stream_name.rfind(".");
|
||||
if (pos != string::npos) {
|
||||
std::string ext = stream_name.substr(pos);
|
||||
// Only strip known streaming extensions
|
||||
if (ext == ".flv" || ext == ".m3u8" || ext == ".mp4" || ext == ".ts") {
|
||||
stream_name = stream_name.substr(0, pos);
|
||||
}
|
||||
}
|
||||
url += "/" + stream_name;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -842,6 +842,11 @@ bool MockAppConfig::get_hls_ts_ctx_enabled(std::string vhost)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool MockAppConfig::get_hls_master_m3u8_path_relative(std::string vhost)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MockAppConfig::get_hls_recover(std::string vhost)
|
||||
{
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -456,6 +456,7 @@ public:
|
|||
virtual bool get_vhost_hls_dts_directly(std::string vhost);
|
||||
virtual bool get_hls_ctx_enabled(std::string vhost);
|
||||
virtual bool get_hls_ts_ctx_enabled(std::string vhost);
|
||||
virtual bool get_hls_master_m3u8_path_relative(std::string vhost);
|
||||
virtual bool get_hls_recover(std::string vhost);
|
||||
virtual bool get_forward_enabled(std::string vhost);
|
||||
virtual SrsConfDirective *get_forwards(std::string vhost);
|
||||
|
|
|
|||
|
|
@ -4972,6 +4972,51 @@ VOID TEST(ProtocolKbpsTest, StreamIdentify)
|
|||
EXPECT_STREQ("ossrs.io/live/livestream", srs_net_url_encode_sid("ossrs.io", "live", "livestream.m3u8").c_str());
|
||||
}
|
||||
|
||||
// Reproduce bug from issue 4011: Stream names with dots get truncated incorrectly
|
||||
// https://github.com/ossrs/srs/issues/4011
|
||||
// The filepath_filename() function removes everything after the LAST dot in the stream name.
|
||||
// This was intended to strip extensions like .flv, but it breaks stream names that contain
|
||||
// dots as part of the identifier (like WavMain.exe_rooms_290_20240402).
|
||||
// This causes edge servers to treat different streams as the same source.
|
||||
//
|
||||
// This test FAILS to demonstrate the bug - when the bug is fixed, this test should PASS.
|
||||
VOID TEST(ProtocolKbpsTest, StreamIdentifyWithDotsInName_Issue4011)
|
||||
{
|
||||
// Case 1: THE ACTUAL BUG from issue 4011
|
||||
// Stream names like "WavMain.exe_rooms_290_20240402" contain a dot in ".exe"
|
||||
// Expected: Each stream should have a unique source URL preserving the full name
|
||||
// Actual (BUG): filepath_filename() strips everything after the dot in ".exe"
|
||||
std::string url1 = srs_net_url_encode_sid("", "imlive", "WavMain.exe_rooms_290_20240402");
|
||||
std::string url2 = srs_net_url_encode_sid("", "imlive", "WavMain.exe_rooms_311_20240402");
|
||||
std::string url3 = srs_net_url_encode_sid("", "imlive", "WavMain.exe_rooms_222_20240402");
|
||||
|
||||
// EXPECTED: Each stream should preserve its full name
|
||||
EXPECT_STREQ("/imlive/WavMain.exe_rooms_290_20240402", url1.c_str());
|
||||
EXPECT_STREQ("/imlive/WavMain.exe_rooms_311_20240402", url2.c_str());
|
||||
EXPECT_STREQ("/imlive/WavMain.exe_rooms_222_20240402", url3.c_str());
|
||||
|
||||
// EXPECTED: Different streams should have different URLs
|
||||
EXPECT_STRNE(url1.c_str(), url2.c_str());
|
||||
EXPECT_STRNE(url1.c_str(), url3.c_str());
|
||||
EXPECT_STRNE(url2.c_str(), url3.c_str());
|
||||
|
||||
// Case 2: Stream names with multiple dots should preserve the full name
|
||||
// (only strip known extensions like .flv, .m3u8, not arbitrary suffixes)
|
||||
EXPECT_STREQ("/live/test.backup.stream", srs_net_url_encode_sid("", "live", "test.backup.stream").c_str());
|
||||
EXPECT_STREQ("/live/my.stream.v2", srs_net_url_encode_sid("", "live", "my.stream.v2").c_str());
|
||||
EXPECT_STREQ("/live/camera.1.hd", srs_net_url_encode_sid("", "live", "camera.1.hd").c_str());
|
||||
|
||||
// Case 3: Different streams with same prefix should be different
|
||||
std::string backup1 = srs_net_url_encode_sid("", "live", "test.backup.stream");
|
||||
std::string backup2 = srs_net_url_encode_sid("", "live", "test.backup.stream2");
|
||||
EXPECT_STRNE(backup1.c_str(), backup2.c_str()); // Should be different
|
||||
|
||||
// Case 4: The intended behavior - stripping ONLY known extensions
|
||||
// These should work correctly (strip .flv and .m3u8)
|
||||
EXPECT_STREQ("/live/livestream", srs_net_url_encode_sid("", "live", "livestream.flv").c_str());
|
||||
EXPECT_STREQ("/live/livestream", srs_net_url_encode_sid("", "live", "livestream.m3u8").c_str());
|
||||
}
|
||||
|
||||
VOID TEST(ProtocolHTTPTest, ParseHTTPMessage)
|
||||
{
|
||||
srs_error_t err = srs_success;
|
||||
|
|
|
|||
|
|
@ -85,8 +85,8 @@ VOID TEST(StTest, StUtimePerformance)
|
|||
|
||||
// Calculate absolute difference between the two elapsed times
|
||||
int time_diff = gettimeofday_elapsed_time > st_utime_elapsed_time
|
||||
? gettimeofday_elapsed_time - st_utime_elapsed_time
|
||||
: st_utime_elapsed_time - gettimeofday_elapsed_time;
|
||||
? gettimeofday_elapsed_time - st_utime_elapsed_time
|
||||
: st_utime_elapsed_time - gettimeofday_elapsed_time;
|
||||
|
||||
// The difference should be less than N clock ticks (microseconds)
|
||||
EXPECT_LT(time_diff, 100);
|
||||
|
|
@ -111,8 +111,8 @@ VOID TEST(StTest, StUtimePerformance)
|
|||
|
||||
// Calculate absolute difference between the two elapsed times
|
||||
int time_diff = gettimeofday_elapsed_time > st_utime_elapsed_time
|
||||
? gettimeofday_elapsed_time - st_utime_elapsed_time
|
||||
: st_utime_elapsed_time - gettimeofday_elapsed_time;
|
||||
? gettimeofday_elapsed_time - st_utime_elapsed_time
|
||||
: st_utime_elapsed_time - gettimeofday_elapsed_time;
|
||||
|
||||
// The difference should be less than N clock ticks (microseconds)
|
||||
EXPECT_LT(time_diff, 100);
|
||||
|
|
@ -139,8 +139,8 @@ VOID TEST(StTest, StUtimePerformance)
|
|||
|
||||
// Calculate absolute difference between the two elapsed times
|
||||
int time_diff = gettimeofday_elapsed_time > st_utime_elapsed_time
|
||||
? gettimeofday_elapsed_time - st_utime_elapsed_time
|
||||
: st_utime_elapsed_time - gettimeofday_elapsed_time;
|
||||
? gettimeofday_elapsed_time - st_utime_elapsed_time
|
||||
: st_utime_elapsed_time - gettimeofday_elapsed_time;
|
||||
|
||||
// The difference should be less than N clock ticks (microseconds)
|
||||
EXPECT_LT(time_diff, 100);
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ VOID TEST(BasicWorkflowHttpConnTest, ManuallyVerifyForHttpStream)
|
|||
live_stream->hooks_ = mock_hooks.get();
|
||||
|
||||
// Do not wait for utest, consume messages immediately. Remove this when HTTP stream use cond signal.
|
||||
mock_config->mw_sleep_ = 0;
|
||||
mock_config->mw_sleep_ = 100; // 0.1ms
|
||||
|
||||
mock_entry->enabled = true;
|
||||
mock_entry->pattern = "/live/livestream.flv";
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user