diff --git a/.augment-guidelines b/.augment-guidelines
index ddd6de9a1..4c8093f57 100644
--- a/.augment-guidelines
+++ b/.augment-guidelines
@@ -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
diff --git a/trunk/3rdparty/signaling/main.go b/trunk/3rdparty/signaling/main.go
index a3969200a..8e3b2ea2d 100644
--- a/trunk/3rdparty/signaling/main.go
+++ b/trunk/3rdparty/signaling/main.go
@@ -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
diff --git a/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/errors.go b/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/errors.go
index 257bc3ccd..d64470404 100644
--- a/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/errors.go
+++ b/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/errors.go
@@ -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
diff --git a/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/stack.go b/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/stack.go
index 6c42db5a8..7e5aacc48 100644
--- a/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/stack.go
+++ b/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/errors/stack.go
@@ -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':
diff --git a/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/go17.go b/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/go17.go
index 65bdeb76f..917f571a0 100644
--- a/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/go17.go
+++ b/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/go17.go
@@ -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
diff --git a/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/logger.go b/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/logger.go
index 61cea362f..90b22fc35 100644
--- a/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/logger.go
+++ b/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/logger.go
@@ -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 (
diff --git a/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/pre_go17.go b/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/pre_go17.go
index 24041dc88..1c46e8dde 100644
--- a/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/pre_go17.go
+++ b/trunk/3rdparty/signaling/vendor/github.com/ossrs/go-oryx-lib/logger/pre_go17.go
@@ -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
diff --git a/trunk/3rdparty/signaling/vendor/golang.org/x/net/websocket/websocket.go b/trunk/3rdparty/signaling/vendor/golang.org/x/net/websocket/websocket.go
index 6c45c7352..ea422e110 100644
--- a/trunk/3rdparty/signaling/vendor/golang.org/x/net/websocket/websocket.go
+++ b/trunk/3rdparty/signaling/vendor/golang.org/x/net/websocket/websocket.go
@@ -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}
diff --git a/trunk/3rdparty/signaling/www/demos/js/srs.sdk.js b/trunk/3rdparty/signaling/www/demos/js/srs.sdk.js
index 6895807e4..34673c948 100644
--- a/trunk/3rdparty/signaling/www/demos/js/srs.sdk.js
+++ b/trunk/3rdparty/signaling/www/demos/js/srs.sdk.js
@@ -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(", ");
-}
-
+}
\ No newline at end of file
diff --git a/trunk/3rdparty/signaling/www/demos/one2one.html b/trunk/3rdparty/signaling/www/demos/one2one.html
index 033980103..f7d03fbc1 100644
--- a/trunk/3rdparty/signaling/www/demos/one2one.html
+++ b/trunk/3rdparty/signaling/www/demos/one2one.html
@@ -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) {
diff --git a/trunk/3rdparty/signaling/www/demos/room.html b/trunk/3rdparty/signaling/www/demos/room.html
index 5c67bf271..956876de0 100644
--- a/trunk/3rdparty/signaling/www/demos/room.html
+++ b/trunk/3rdparty/signaling/www/demos/room.html
@@ -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();
diff --git a/trunk/3rdparty/srs-bench/blackbox/blackbox_test.go b/trunk/3rdparty/srs-bench/blackbox/blackbox_test.go
index 5c950d070..43a559085 100644
--- a/trunk/3rdparty/srs-bench/blackbox/blackbox_test.go
+++ b/trunk/3rdparty/srs-bench/blackbox/blackbox_test.go
@@ -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) {
diff --git a/trunk/3rdparty/srs-bench/blackbox/dvr_test.go b/trunk/3rdparty/srs-bench/blackbox/dvr_test.go
index 5cf199660..6cef7ee8f 100644
--- a/trunk/3rdparty/srs-bench/blackbox/dvr_test.go
+++ b/trunk/3rdparty/srs-bench/blackbox/dvr_test.go
@@ -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) {
diff --git a/trunk/3rdparty/srs-bench/blackbox/hevc_test.go b/trunk/3rdparty/srs-bench/blackbox/hevc_test.go
index 892c6643c..12de11573 100644
--- a/trunk/3rdparty/srs-bench/blackbox/hevc_test.go
+++ b/trunk/3rdparty/srs-bench/blackbox/hevc_test.go
@@ -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) {
diff --git a/trunk/3rdparty/srs-bench/blackbox/hls_test.go b/trunk/3rdparty/srs-bench/blackbox/hls_test.go
index 395bf6da3..b2aa45768 100644
--- a/trunk/3rdparty/srs-bench/blackbox/hls_test.go
+++ b/trunk/3rdparty/srs-bench/blackbox/hls_test.go
@@ -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) {
diff --git a/trunk/3rdparty/srs-bench/blackbox/http_api_test.go b/trunk/3rdparty/srs-bench/blackbox/http_api_test.go
index d81034a3c..f6d704c6c 100644
--- a/trunk/3rdparty/srs-bench/blackbox/http_api_test.go
+++ b/trunk/3rdparty/srs-bench/blackbox/http_api_test.go
@@ -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) {
diff --git a/trunk/3rdparty/srs-bench/blackbox/mp3_test.go b/trunk/3rdparty/srs-bench/blackbox/mp3_test.go
index 5fd9d2fb6..9507d9c82 100644
--- a/trunk/3rdparty/srs-bench/blackbox/mp3_test.go
+++ b/trunk/3rdparty/srs-bench/blackbox/mp3_test.go
@@ -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) {
diff --git a/trunk/3rdparty/srs-bench/blackbox/rtmp_test.go b/trunk/3rdparty/srs-bench/blackbox/rtmp_test.go
index c7c00ec1d..058d0c52f 100644
--- a/trunk/3rdparty/srs-bench/blackbox/rtmp_test.go
+++ b/trunk/3rdparty/srs-bench/blackbox/rtmp_test.go
@@ -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) {
diff --git a/trunk/3rdparty/srs-bench/blackbox/srt_test.go b/trunk/3rdparty/srs-bench/blackbox/srt_test.go
index 41f52c282..c9d2e9e2a 100644
--- a/trunk/3rdparty/srs-bench/blackbox/srt_test.go
+++ b/trunk/3rdparty/srs-bench/blackbox/srt_test.go
@@ -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) {
diff --git a/trunk/3rdparty/srs-bench/blackbox/util.go b/trunk/3rdparty/srs-bench/blackbox/util.go
index d1c691e97..85da836c6 100644
--- a/trunk/3rdparty/srs-bench/blackbox/util.go
+++ b/trunk/3rdparty/srs-bench/blackbox/util.go
@@ -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
diff --git a/trunk/3rdparty/srs-bench/janus/api.go b/trunk/3rdparty/srs-bench/janus/api.go
index 3c3317caf..3a534a53e 100644
--- a/trunk/3rdparty/srs-bench/janus/api.go
+++ b/trunk/3rdparty/srs-bench/janus/api.go
@@ -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 {
diff --git a/trunk/3rdparty/srs-bench/janus/janus.go b/trunk/3rdparty/srs-bench/janus/janus.go
index ee9b3d44a..62c5bf8c6 100644
--- a/trunk/3rdparty/srs-bench/janus/janus.go
+++ b/trunk/3rdparty/srs-bench/janus/janus.go
@@ -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
diff --git a/trunk/3rdparty/srs-bench/live/live.go b/trunk/3rdparty/srs-bench/live/live.go
index 6dc3a8efe..af02afa3c 100644
--- a/trunk/3rdparty/srs-bench/live/live.go
+++ b/trunk/3rdparty/srs-bench/live/live.go
@@ -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()
}
diff --git a/trunk/3rdparty/srs-bench/srs/srs.go b/trunk/3rdparty/srs-bench/srs/srs.go
index 931b459a6..eff8f004f 100644
--- a/trunk/3rdparty/srs-bench/srs/srs.go
+++ b/trunk/3rdparty/srs-bench/srs/srs.go
@@ -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
diff --git a/trunk/3rdparty/srs-bench/srs/srs_test.go b/trunk/3rdparty/srs-bench/srs/srs_test.go
index 92edeac2e..1aca9502c 100644
--- a/trunk/3rdparty/srs-bench/srs/srs_test.go
+++ b/trunk/3rdparty/srs-bench/srs/srs_test.go
@@ -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) {
diff --git a/trunk/3rdparty/srs-docs/doc/hls.md b/trunk/3rdparty/srs-docs/doc/hls.md
index 2322e274e..80623844c 100644
--- a/trunk/3rdparty/srs-docs/doc/hls.md
+++ b/trunk/3rdparty/srs-docs/doc/hls.md
@@ -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.
diff --git a/trunk/3rdparty/srs-docs/doc/webrtc.md b/trunk/3rdparty/srs-docs/doc/webrtc.md
index 83b2939ed..221489f49 100644
--- a/trunk/3rdparty/srs-docs/doc/webrtc.md
+++ b/trunk/3rdparty/srs-docs/doc/webrtc.md
@@ -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)
+

diff --git a/trunk/auto/depends.sh b/trunk/auto/depends.sh
index b1fe69d21..bfc9a08b4 100755
--- a/trunk/auto/depends.sh
+++ b/trunk/auto/depends.sh
@@ -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
#####################################################################################
diff --git a/trunk/conf/full.conf b/trunk/conf/full.conf
index dd0900188..7d4de2a11 100644
--- a/trunk/conf/full.conf
+++ b/trunk/conf/full.conf
@@ -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.
diff --git a/trunk/conf/http.hooks.callback.conf b/trunk/conf/http.hooks.callback.conf
index 91fdba401..94b65b642 100644
--- a/trunk/conf/http.hooks.callback.conf
+++ b/trunk/conf/http.hooks.callback.conf
@@ -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;
diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md
index e8b2454d4..721ee3ed1 100644
--- a/trunk/doc/CHANGELOG.md
+++ b/trunk/doc/CHANGELOG.md
@@ -7,6 +7,14 @@ The changelog for SRS.
## 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)
diff --git a/trunk/research/players/js/srs.sdk.js b/trunk/research/players/js/srs.sdk.js
index 7d49ddc79..34673c948 100644
--- a/trunk/research/players/js/srs.sdk.js
+++ b/trunk/research/players/js/srs.sdk.js
@@ -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.
diff --git a/trunk/research/players/rtc_player.html b/trunk/research/players/rtc_player.html
index e5a00308a..971f9fa77 100644
--- a/trunk/research/players/rtc_player.html
+++ b/trunk/research/players/rtc_player.html
@@ -67,6 +67,31 @@