16 Commits
v4.0.3 ... v4

Author SHA1 Message Date
ZiShuo
988878f4c3 update 2025-01-18 20:24:20 +08:00
ZiShuo
3c9381d76b update 2025-01-18 20:20:41 +08:00
wancheng1990
2dfd35348f update pro 2025-01-09 22:25:12 +08:00
langhuihui
3228fb0040 fix: change html lang to zh 2024-03-13 13:38:41 +08:00
dexter
5f8109cb82 Merge pull request #1 from dwdcth/patch-1
Update README.md
2024-02-06 08:57:13 +08:00
banshan
f70d78dec4 Update README.md 2024-02-05 18:01:49 +08:00
wancheng1990
6bfd6b3861 update pro version 2023-12-26 11:07:07 +08:00
langhuihui
6e5ce878ed fix: lock map before read 2023-12-14 13:26:34 +08:00
langhuihui
d0d43f80c4 feat: format update 2023-12-07 10:35:37 +08:00
wancheng1990
6c66245e57 update jessibuca pro version 2023-12-07 00:09:18 +08:00
wancheng1990
c8f87bd888 fix bug 2023-11-29 11:18:05 +08:00
wancheng1990
100a59c34d update pro 2023-11-28 17:26:12 +08:00
langhuihui
afc942ee5f fix: https default port 8443 2023-06-30 10:24:29 +08:00
langhuihui
d62e29e1f9 feat: add webrtc play h265 and aac 2023-06-16 22:31:01 +08:00
langhuihui
382855fbc5 feat: upgrade to jesscibuca pro demo 2023-06-11 13:24:33 +08:00
dexter
33b9393d59 适配引擎升级 2023-04-04 20:24:39 +08:00
22 changed files with 139316 additions and 142 deletions

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/plugin-preview.iml" filepath="$PROJECT_DIR$/.idea/plugin-preview.iml" />
</modules>
</component>
</project>

12
.idea/plugin-preview.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -20,10 +20,16 @@ import (
## API
- `/preview/[streamPath]?type=[hdl|hls|ws|wt|rtc]` 可用于预览直播流
- `/preview/[filepath]` 可用于预览录像文件(开发中)
### GET `/preview`
视频流预览页面
### GET `/preview/[streamPath]?type=[hdl|hls|ws|wt|rtc]`
预览指定的视频流,比如 `preview/live/test?type=ws`, 使用 ws 方式预览 live/test 直播流
### GET `/preview/[filepath]`
(开发中)预览制定的录像文件,比如 `preview/record/flv/xxx.flv`
## 使用WebTransport注意事项
- 本地测试需要本地启动https服务并配置有效的证书
- 由于证书与域名绑定所以需要host里面配置对应的域名 例如:`127.0.0.1 monibuca.com`
- 由于证书与域名绑定所以需要host里面配置对应的域名 例如:`127.0.0.1 monibuca.com`

2
go.mod
View File

@@ -1,3 +1,3 @@
module m7s.live/plugin/preview/v4
module git.zishuo.net/zishuo/preview/v4
go 1.18

19
main.go
View File

@@ -25,22 +25,23 @@ func (p *PreviewConfig) OnEvent(event any) {
var _ = InstallPlugin(&PreviewConfig{})
func (p *PreviewConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/preview/" {
var s string
if r.URL.Path == "/" {
s := "<h1><h1><h2>Live Streams 引擎中正在发布的流</h2>"
Streams.Range(func(streamPath string, stream *Stream) {
s += fmt.Sprintf("<a href='%s'>%s</a> [ %s ]<br>", streamPath, streamPath, stream.GetType())
})
if s != "" {
s = "<b>Live Streams</b><br>" + s
}
s += "<h2>pull stream on subscribe 订阅时才会触发拉流的流</h2>"
for name, p := range Plugins {
if pullcfg, ok := p.Config.(config.PullConfig); ok {
if pullonsub := pullcfg.GetPullConfig().PullOnSub; pullonsub != nil {
s += fmt.Sprintf("<b>%s pull stream on subscribe</b><br>", name)
for streamPath, url := range pullonsub {
pullconf := pullcfg.GetPullConfig()
pullconf.PullOnSubLocker.RLock()
if pullconf.PullOnSub != nil {
s += fmt.Sprintf("<h3>%s</h3>", name)
for streamPath, url := range pullconf.PullOnSub {
s += fmt.Sprintf("<a href='%s'>%s</a> <-- %s<br>", streamPath, streamPath, url)
}
}
pullconf.PullOnSubLocker.RUnlock()
}
}
w.Write([]byte(s))
@@ -53,7 +54,7 @@ func (p *PreviewConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else {
//w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
//w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
b, err = f.ReadFile("ui/index.html")
b, err = f.ReadFile("ui/demo.html")
w.Write(b)
}
}

2651
ui/api.md Normal file

File diff suppressed because one or more lines are too long

BIN
ui/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

File diff suppressed because one or more lines are too long

BIN
ui/decoder-pro-simd.wasm Executable file → Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
ui/decoder-pro.wasm Executable file → Normal file

Binary file not shown.

583
ui/demo.html Normal file
View File

@@ -0,0 +1,583 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Preview by JessibucaPro</title>
<script src="./vconsole.js"></script>
<script src="./jessibuca-pro-demo.js"></script>
<style>
body {
background: linear-gradient(90deg, red, cyan);
}
body::before {
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
-webkit-mask-image: linear-gradient(to bottom, transparent, black);
background: linear-gradient(90deg, hotpink, rgb(102, 51, 153));
}
.root {
display: flex;
place-content: center;
margin-top: 3rem;
}
.container-shell {
backdrop-filter: blur(5px);
background: hsla(0, 0%, 50%, 0.5);
padding: 30px 4px 10px 4px;
/* border: 2px solid black; */
width: auto;
position: relative;
border-radius: 5px;
box-shadow: 0 10px 20px;
}
.container-shell:before {
content: "preview powered by jessibuca pro (v2.1.4)";
position: absolute;
color: darkgray;
top: 4px;
left: 10px;
text-shadow: 1px 1px black;
}
#container {
background: rgba(13, 14, 27, 0.7);
width: 960px;
height: 597px;
}
.input {
display: flex;
margin-top: 10px;
color: white;
place-content: stretch;
}
.input2 {
bottom: 0px;
}
.input input {
flex: auto;
}
.err {
position: absolute;
top: 40px;
left: 10px;
color: red;
}
.option {
position: absolute;
top: 4px;
right: 10px;
display: flex;
place-content: center;
font-size: 12px;
}
.option span {
color: white;
}
.page {
/* background-repeat: no-repeat;
background-position: top; */
}
@media (max-width: 720px) {
#container {
width: 90vw;
height: 52.7vw;
}
}
</style>
</head>
<body class="page">
<div class="root">
<div class="container-shell">
<div id="container"></div>
<div class="input">
<div>
<div>
<span style="color: green">硬解码:</span>
<input type="checkbox" checked id="useMSE" /><span>MediaSource</span>
<input type="checkbox" id="useWCS" /><span>Webcodec</span>
</div>
<div>
<span style="color:red;">软解码:</span>
<input type="checkbox" id="useWASM" disabled checked/> <span>WASM(默认)</span>
<input type="checkbox" id="useSIMD" /><span>WASM SIMD</span>
<input type="checkbox" id="useSIMDV2" /><span>WASM SIMD(V2)</span>
</div>
</div>
</div>
<div class="input">
<span>协议切换:</span>
<select id="protocol">
<option value="hdl">hdl(http-flv)</option>
<option value="ws-flv">ws-flv</option>
<option value="ws-raw">jessica(ws-raw)</option>
<option value="ws-h265">ws-h265</option>
<option value="ws-h264">ws-h264</option>
<option value="http-h265">http-h265</option>
<option value="http-h264">http-h264</option>
<option value="fmp4">fmp4</option>
<option value="hls">hls</option>
<option value="webrtc">webrtc</option>
<option value="webtransport">webtransport</option>
</select>
</div>
<div class="input">
<div>
<span>缓存时长:</span>
<input placeholder="单位:秒" type="text" id="videoBuffer" style="width: 50px" value="0">
<span>缓存延迟(延迟超过会触发丢帧)</span>
<input placeholder="单位:秒" type="text" id="videoBufferDelay" style="width: 50px" value="5">
<button id="replay">重播</button>
</div>
</div>
<div class="input">
<div>输入URL</div>
<input autocomplete="on" id="playUrl" value="" />
<button id="play">播放</button>
<button id="pause" style="display: none">停止</button>
</div>
<div class="input" style="line-height: 30px">
<button id="destroy">销毁</button>
<span class="fps-inner"></span>
</div>
</div>
</div>
<div class="input">
<div>
当前浏览器:
<span id="mseSupport264" style="color: green;display: none">支持MSE H264解码</span>
<span id="mseSupport" style="color: green;display: none">支持MSE H265解码</span>
<span id="mseNotSupport" style="color: red;display: none">不支持MSE H264解码</span>
<span id="mseNotSupport264" style="color: red;display: none">不支持MSE H265解码,会自动切换成wasm解码</span>
</div>
</div>
<div class="input">
<div>
当前浏览器:
<span id="wcsSupport264" style="color: green;display: none">支持Webcodecs H264解码</span>
<span id="wcsSupport" style="color: green;display: none">支持Webcodecs H265解码</span>
<span id="wcsNotSupport264" style="color: red;display: none">不支持Webcodecs H264解码(https/localhost)</span>
<span id="wcsNotSupport" style="color: red;display: none">不支持Webcodecs
H265解码(https/localhost),会自动切换成wasm解码</span>
</div>
</div>
<div class="input">
<div>
当前浏览器:
<span id="simdSupport" style="color: green;display: none">支持WASM SIMD解码</span>
<span id="simdNotSupport" style="color: red;display: none">不支持WASM SIMD解码,会自动切换成wasm解码</span>
</div>
</div>
<script>
function getBrowser() {
const UserAgent = window.navigator.userAgent.toLowerCase() || '';
let browserInfo = {
type: '',
version: ''
};
var browserArray = {
IE: window.ActiveXObject || "ActiveXObject" in window, // IE
Chrome: UserAgent.indexOf('chrome') > -1 && UserAgent.indexOf('safari') > -1, // Chrome浏览器
Firefox: UserAgent.indexOf('firefox') > -1, // 火狐浏览器
Opera: UserAgent.indexOf('opera') > -1, // Opera浏览器
Safari: UserAgent.indexOf('safari') > -1 && UserAgent.indexOf('chrome') == -1, // safari浏览器
Edge: UserAgent.indexOf('edge') > -1, // Edge浏览器
QQBrowser: /qqbrowser/.test(UserAgent), // qq浏览器
WeixinBrowser: /MicroMessenger/i.test(UserAgent) // 微信浏览器
};
// console.log(browserArray)
for (let i in browserArray) {
if (browserArray[i]) {
let versions = '';
if (i === 'IE') {
const versionArray = UserAgent.match(/(msie\s|trident.*rv:)([\w.]+)/);
if (versionArray && versionArray.length > 2) {
versions = UserAgent.match(/(msie\s|trident.*rv:)([\w.]+)/)[2];
}
} else if (i === 'Chrome') {
for (let mt in navigator.mimeTypes) {
//检测是否是360浏览器(测试只有pc端的360才起作用)
if (navigator.mimeTypes[mt]['type'] === 'application/360softmgrplugin') {
i = '360';
}
}
const versionArray = UserAgent.match(/chrome\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
} else if (i === 'Firefox') {
const versionArray = UserAgent.match(/firefox\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
} else if (i === 'Opera') {
const versionArray = UserAgent.match(/opera\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
} else if (i === 'Safari') {
const versionArray = UserAgent.match(/version\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
} else if (i === 'Edge') {
const versionArray = UserAgent.match(/edge\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
} else if (i === 'QQBrowser') {
const versionArray = UserAgent.match(/qqbrowser\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
}
browserInfo.type = i;
browserInfo.version = parseInt(versions);
}
}
return browserInfo;
}
function checkSupportMSEHevc() {
return window.MediaSource && window.MediaSource.isTypeSupported('video/mp4; codecs="hev1.1.6.L123.b0"');
}
function checkSupportMSEH264() {
return window.MediaSource && window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.64002A"');
}
function checkSupportWCSHevc() {
const browserInfo = getBrowser();
return browserInfo.type.toLowerCase() === 'chrome' && browserInfo.version >= 107 && (location.protocol === 'https:' || location.hostname === 'localhost');
}
function checkSupportWCS() {
return "VideoEncoder" in window;
}
function checkSupportSIMD() {
return WebAssembly && WebAssembly.validate(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11]));
}
let support = document.getElementById('mseSupport');
let notSupport = document.getElementById('mseNotSupport');
if (checkSupportMSEHevc()) {
support.style.display = 'inline-block';
} else {
notSupport.style.display = 'inline-block';
}
let supportH264 = document.getElementById('mseSupport264');
let notSupportH264 = document.getElementById('mseNotSupport264');
if (checkSupportMSEH264()) {
supportH264.style.display = 'inline-block';
} else {
notSupportH264.style.display = 'inline-block';
}
let supportWcsHevc = document.getElementById('wcsSupport');
let notSupportWcsHevc = document.getElementById('wcsNotSupport');
if (checkSupportWCSHevc()) {
supportWcsHevc.style.display = 'inline-block';
} else {
notSupportWcsHevc.style.display = 'inline-block';
}
let supportWcs = document.getElementById('wcsSupport264');
let notSupportWcs = document.getElementById('wcsNotSupport264');
if (checkSupportWCS()) {
supportWcs.style.display = 'inline-block';
} else {
notSupportWcs.style.display = 'inline-block';
}
let supportSimd = document.getElementById('simdSupport');
let notSupportSimd = document.getElementById('simdNotSupport');
if (checkSupportSIMD()) {
supportSimd.style.display = 'inline-block';
} else {
notSupportSimd.style.display = 'inline-block';
}
</script>
<script>
var $player = document.getElementById('play');
var $pause = document.getElementById('pause');
var $playHref = document.getElementById('playUrl');
var $container = document.getElementById('container');
var $destroy = document.getElementById('destroy');
var $useMSE = document.getElementById('useMSE');
var $useSIMD = document.getElementById('useSIMD');
var $useSIMDV2 = document.getElementById('useSIMDV2');
var $useWASM = document.getElementById('useWASM');
var $useWCS = document.getElementById('useWCS');
var $videoBuffer = document.getElementById('videoBuffer');
var $videoBufferDelay = document.getElementById('videoBufferDelay');
var $replay = document.getElementById('replay');
var $fps = document.querySelector('.fps-inner');
var $protocol = document.getElementById('protocol');
var showOperateBtns = true; // 是否显示按钮
var forceNoOffscreen = true; //
var jessibuca = null;
function isMobile() {
return (/iphone|ipad|android.*mobile|windows.*phone|blackberry.*mobile/i.test(window.navigator.userAgent.toLowerCase()));
}
function isPad() {
return (/ipad|android(?!.*mobile)|tablet|kindle|silk/i.test(window.navigator.userAgent.toLowerCase()));
}
const useVconsole = isMobile() || isPad();
if (useVconsole && window.VConsole) {
new window.VConsole();
}
const query = new URLSearchParams(location.search);
const isHttps = location.protocol.startsWith("https");
const type = query.get('type') || 'hdl';
$protocol.value = type;
let pluginName = type;
let ext = "." + type;
switch (type) {
case 'hdl':
ext = '.flv';
break;
case 'ws-flv':
ext = '.flv';
pluginName = 'jessica';
break;
case 'ws-raw':
ext = "";
pluginName = 'jessica';
break;
case 'ws-h265':
ext = '.h265';
pluginName = 'jessica';
break;
case 'ws-h264':
ext = '.h264';
pluginName = 'jessica';
break;
case 'http-h265':
ext = '.h265';
pluginName = 'jessica';
break;
case 'http-h264':
ext = '.h264';
pluginName = 'jessica';
break;
case 'fmp4':
ext = '.mp4';
break;
case 'hls':
ext = '.m3u8';
break;
}
$playHref.value = (type.startsWith('ws') ? (isHttps ? "wss://" : "ws://") + location.host : location.origin) + location.pathname.replace('preview', pluginName) + ext;
switch (type) {
case 'webrtc':
$playHref.value = 'webrtc://' + location.host + location.pathname.replace('preview', pluginName + '/play');
break;
case 'webtransport':
$playHref.value = 'wt://' + location.hostname + ':4433' + location.pathname.replace('preview', 'play');
break;
}
function create() {
jessibuca = new JessibucaPro({
container: $container,
videoBuffer: Number($videoBuffer.value), // 缓存时长
videoBufferDelay: Number($videoBufferDelay.value), // 1000s
isResize: false,
text: "",
loadingText: "加载中",
debug: true,
isEmitSEI: true,
debugLevel: "debug",
useMSE: $useMSE.checked === true,
useSIMD: $useSIMD.checked === true,
useWCS: $useWCS.checked === true,
isFFmpegSIMD: $useSIMDV2.checked === true,
showBandwidth: showOperateBtns, // 显示网速
showPerformance: showOperateBtns, // 显示性能
operateBtns: {
fullscreen: showOperateBtns,
screenshot: showOperateBtns,
play: showOperateBtns,
audio: showOperateBtns,
ptz: showOperateBtns,
quality: showOperateBtns,
performance: showOperateBtns,
},
heartTimeoutReplayUseLastFrameShow: true,
audioEngine: "worklet",
qualityConfig: ['普清', '高清', '超清', '4K', '8K'],
forceNoOffscreen: forceNoOffscreen,
isNotMute: false,
heartTimeout: 10,
// isFlv: true
},);
jessibuca.on('ptz', (arrow) => {
console.log('ptz', arrow);
});
jessibuca.on('streamQualityChange', (value) => {
console.log('streamQualityChange', value);
});
jessibuca.on('timeUpdate', (value) => {
// console.log('timeUpdate', value);
});
jessibuca.on('videoSEI', (value) => {
console.warn(`videoSEI ts is ${value.ts}, data is ${value.data}`);
})
jessibuca.on('stats', (stats) => {
// console.log('stats', stats);
$fps.textContent = `FPS: ${stats.fps} DFPS: ${stats.dfps}`;
});
jessibuca.on(JessibucaPro.EVENTS.crashLog, (log) => {
console.log('crashLog', log);
});
$player.style.display = 'inline-block';
$pause.style.display = 'none';
$destroy.style.display = 'none';
$fps.textContent = '';
}
create();
function play() {
var href = $playHref.value;
if (href) {
jessibuca.play(href);
$player.style.display = 'none';
$pause.style.display = 'inline-block';
$destroy.style.display = 'inline-block';
}
}
function replay() {
if (jessibuca) {
jessibuca.destroy().then(() => {
create();
play();
});
} else {
create();
play();
}
}
$replay.addEventListener('click', function () {
replay();
});
$player.addEventListener('click', function () {
play();
}, false);
$pause.addEventListener('click', function () {
$player.style.display = 'inline-block';
$pause.style.display = 'none';
$fps.textContent = '';
jessibuca.pause();
});
$destroy.addEventListener('click', function () {
if (jessibuca) {
jessibuca.destroy().then(() => {
create();
});
} else {
create();
}
});
$useMSE.addEventListener('click', function () {
const checked = $useMSE.checked;
if (checked) {
$useSIMD.checked = false;
$useSIMDV2.checked = false;
$useWCS.checked = false;
}
replay();
});
$useSIMD.addEventListener('click', function () {
const checked = $useSIMD.checked;
if (checked) {
$useMSE.checked = false;
$useWCS.checked = false;
}
else {
$useSIMDV2.checked = false;
}
replay();
});
$useSIMDV2.addEventListener('click', function () {
const checked = $useSIMDV2.checked;
if (checked) {
$useSIMD.checked = true;
$useMSE.checked = false;
$useWCS.checked = false;
}
replay();
});
$useWCS.addEventListener('click', function () {
const checked = $useWCS.checked;
if (checked) {
$useMSE.checked = false;
$useSIMD.checked = false;
$useSIMDV2.checked = false;
}
replay();
});
$protocol.addEventListener('change', function () {
if ($protocol.value === 'webtransport')
location.replace('https://local.monibuca.com:8443' + location.pathname + '?type=' + $protocol.value);
else
location.replace(location.origin + location.pathname + '?type=' + $protocol.value);
});
</script>
</body>
</html>

View File

@@ -1,24 +1,487 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>M7s Preview 预览</title>
</head>
<body>
<a href="javascript:void 0" id="unmute">解除静音</a>
<script src="jessibuca-pro-preview.js"></script>
<script>
(function(){
var ctx = new AudioContext();
if(ctx.state == "running"){
document.getElementById("unmute").style.display = "none";
} else {
ctx.onstatechange = ()=> {
if(ctx.state == 'running'){
document.getElementById("unmute").style.display = "none";
}
}}
})()
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Jessica demo</title>
<!--<script src="./vconsole.js"></script>-->
<script src="./jessibuca-pro.js"></script>
<!--<script src="./jessibuca-pro-demo.js"></script>-->
<style>
.root {
display: flex;
place-content: center;
margin-top: 3rem;
}
.container-shell {
backdrop-filter: blur(5px);
background: hsla(0, 0%, 50%, 0.5);
padding: 30px 4px 10px 4px;
/* border: 2px solid black; */
width: auto;
position: relative;
border-radius: 5px;
box-shadow: 0 10px 20px;
}
.container-shell:before {
content: "chipeak live player";
position: absolute;
color: darkgray;
top: 4px;
left: 10px;
text-shadow: 1px 1px black;
}
#container {
background: rgba(13, 14, 27, 0.7);
width: 960px;
height: 597px;
}
.input {
display: flex;
margin-top: 10px;
color: white;
place-content: stretch;
}
.input2 {
bottom: 0px;
}
.input input {
flex: auto;
}
.err {
position: absolute;
top: 40px;
left: 10px;
color: red;
}
.option {
position: absolute;
top: 4px;
right: 10px;
display: flex;
place-content: center;
font-size: 12px;
}
.option span {
color: white;
}
.page {
background: url('./bg.jpg');
background-repeat: no-repeat;
background-position: top;
}
@media (max-width: 720px) {
#container {
width: 90vw;
height: 52.7vw;
}
}
</style>
</head>
<body class="page">
<div class="root">
<div class="container-shell">
<div id="container"></div>
<div class="input">
<div>
当前浏览器:<span id="mseSupport" style="color: green;display: none">支持MSE H265解码</span>
<span id="mseNotSupport" style="color: red;display: none">不支持MSE H265解码,会自动切换成wasm解码</span>
</div>
</div>
<div class="input">
<div>
当前浏览器:<span id="wcsSupport" style="color: green;display: none">支持Webcodecs H265解码</span>
<span id="wcsNotSupport" style="color: red;display: none">不支持Webcodecs H265解码(https/localhost),会自动切换成wasm解码</span>
</div>
</div>
<div class="input">
<div>
当前浏览器:<span id="simdSupport" style="color: green;display: none">支持SIMD解码</span>
<span id="simdNotSupport" style="color: red;display: none">不支持SIMD解码,会自动切换成wasm解码</span>
</div>
</div>
<div class="input">
<div><input
type="checkbox"
checked
id="useMSE"
/><span>MediaSource</span>
<input
type="checkbox"
id="useWCS"
/><span>Webcodec</span>
<input
type="checkbox"
id="useSIMD"
/><span>SIMD</span>
</div>
</div>
<div class="input">
<span>渲染标签:</span>
<select id="renderDom" onchange="replay()">
<option value="video" selected>video</option>
<option value="canvas" >canvas</option>
</select>
<span>canvas渲染技术</span>
<select id="isUseWebGPU" onchange="replay()">
<option value="webgl" >webgl</option>
<option value="webgpu" selected>webgpu</option>
</select>
<span id="supportWebgpu"></span>
</div>
<div class="input">
<div>
<span>缓存时长:</span>
<input placeholder="单位:秒" type="text" id="videoBuffer" style="width: 50px" value="0.2">
<span>缓存延迟(延迟超过会触发丢帧)</span>
<input placeholder="单位:秒" type="text" id="videoBufferDelay" style="width: 50px" value="1">
<button id="replay">重播</button>
</div>
<span>协议切换:</span>
<select id="protocol">
<option value="hdl">hdl(http-flv)</option>
<option value="ws-flv">ws-flv</option>
<option value="ws-raw">jessica(ws-raw)</option>
<option value="ws-h265">ws-h265</option>
<option value="ws-h264">ws-h264</option>
<option value="http-h265">http-h265</option>
<option value="http-h264">http-h264</option>
<option value="fmp4">fmp4</option>
<option value="hls">hls</option>
<option value="webrtc">webrtc</option>
<option value="webtransport">webtransport</option>
</select>
</div>
<div class="input">
<div>输入URL</div>
<input autocomplete="on" id="playUrl" value="http://183.136.206.149:1133/hdl/live/180.flv" />
<button id="play">播放</button>
<button id="pause" style="display: none">停止</button>
</div>
<div class="input" style="line-height: 30px">
<button id="destroy">销毁</button>
<span class="fps-inner"></span>
</div>
</div>
</div>
<script>
function getBrowser() {
const UserAgent = window.navigator.userAgent.toLowerCase() || '';
let browserInfo = {
type: '',
version: ''
};
var browserArray = {
IE: window.ActiveXObject || "ActiveXObject" in window, // IE
Chrome: UserAgent.indexOf('chrome') > -1 && UserAgent.indexOf('safari') > -1, // Chrome浏览器
Firefox: UserAgent.indexOf('firefox') > -1, // 火狐浏览器
Opera: UserAgent.indexOf('opera') > -1, // Opera浏览器
Safari: UserAgent.indexOf('safari') > -1 && UserAgent.indexOf('chrome') == -1, // safari浏览器
Edge: UserAgent.indexOf('edge') > -1, // Edge浏览器
QQBrowser: /qqbrowser/.test(UserAgent), // qq浏览器
WeixinBrowser: /MicroMessenger/i.test(UserAgent) // 微信浏览器
};
// console.log(browserArray)
for (let i in browserArray) {
if (browserArray[i]) {
let versions = '';
if (i === 'IE') {
const versionArray = UserAgent.match(/(msie\s|trident.*rv:)([\w.]+)/)
if (versionArray && versionArray.length > 2) {
versions = UserAgent.match(/(msie\s|trident.*rv:)([\w.]+)/)[2];
}
} else if (i === 'Chrome') {
for (let mt in navigator.mimeTypes) {
//检测是否是360浏览器(测试只有pc端的360才起作用)
if (navigator.mimeTypes[mt]['type'] === 'application/360softmgrplugin') {
i = '360';
}
}
const versionArray = UserAgent.match(/chrome\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
} else if (i === 'Firefox') {
const versionArray = UserAgent.match(/firefox\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
} else if (i === 'Opera') {
const versionArray = UserAgent.match(/opera\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
} else if (i === 'Safari') {
const versionArray = UserAgent.match(/version\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
} else if (i === 'Edge') {
const versionArray = UserAgent.match(/edge\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
} else if (i === 'QQBrowser') {
const versionArray = UserAgent.match(/qqbrowser\/([\d.]+)/);
if (versionArray && versionArray.length > 1) {
versions = versionArray[1];
}
}
browserInfo.type = i;
browserInfo.version = parseInt(versions);
}
}
return browserInfo;
}
function checkSupportMSEHevc() {
return window.MediaSource && window.MediaSource.isTypeSupported('video/mp4; codecs="hev1.1.6.L123.b0"');
}
function checkSupportWCSHevc() {
const browserInfo = getBrowser();
return browserInfo.type.toLowerCase() === 'chrome' && browserInfo.version >= 107 && (location.protocol === 'https:' || location.hostname === 'localhost');
}
function checkSupportSIMD() {
return WebAssembly && WebAssembly.validate(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11]));
}
let support = document.getElementById('mseSupport');
let notSupport = document.getElementById('mseNotSupport');
if (checkSupportMSEHevc()) {
support.style.display = 'inline-block'
} else {
notSupport.style.display = 'inline-block'
}
let supportWcsHevc = document.getElementById('wcsSupport');
let notSupportWcsHevc = document.getElementById('wcsNotSupport');
if (checkSupportWCSHevc()) {
supportWcsHevc.style.display = 'inline-block';
} else {
notSupportWcsHevc.style.display = 'inline-block'
}
let supportSimd = document.getElementById('simdSupport');
let notSupportSimd = document.getElementById('simdNotSupport');
if (checkSupportSIMD()) {
supportSimd.style.display = 'inline-block';
} else {
notSupportSimd.style.display = 'inline-block'
}
let $supportWebgpu = document.getElementById('supportWebgpu')
if('gpu' in navigator){
$supportWebgpu.innerText = "支持webgpu渲染";
$supportWebgpu.style.color = "green";
}
else {
$supportWebgpu.innerText = "不支持webgpu渲染,会自动切换成webgl渲染(适用于wasm解码+canvas渲染)"
$supportWebgpu.style.color = "red";
}
</script>
<script>
var $player = document.getElementById('play');
var $pause = document.getElementById('pause');
var $playHref = document.getElementById('playUrl');
var $container = document.getElementById('container');
var $destroy = document.getElementById('destroy');
var $useMSE = document.getElementById('useMSE');
var $useSIMD = document.getElementById('useSIMD');
var $useWCS = document.getElementById('useWCS');
var $videoBuffer = document.getElementById('videoBuffer');
var $videoBufferDelay = document.getElementById('videoBufferDelay');
var $replay = document.getElementById('replay');
var $fps = document.querySelector('.fps-inner');
var $renderDom = document.getElementById('renderDom');
var $isUseWebGPU = document.getElementById('isUseWebGPU');
var showOperateBtns = true; // 是否显示按钮
var forceNoOffscreen = true; //
var jessibuca = null;
function isMobile() {
return (/iphone|ipad|android.*mobile|windows.*phone|blackberry.*mobile/i.test(window.navigator.userAgent.toLowerCase()));
}
function isPad() {
return (/ipad|android(?!.*mobile)|tablet|kindle|silk/i.test(window.navigator.userAgent.toLowerCase()));
}
const useVconsole = isMobile() || isPad()
if (useVconsole && window.VConsole) {
new window.VConsole();
}
function create() {
jessibuca = new JessibucaPro({
container: $container,
videoBuffer: Number($videoBuffer.value), // 缓存时长
videoBufferDelay: Number($videoBufferDelay.value), // 1000s
isResize: false,
text: "",
loadingText: "加载中",
debug: true,
debugLevel: "debug",
useMSE: $useMSE.checked === true,
useSIMD: $useSIMD.checked === true,
useWCS: $useWCS.checked === true,
showBandwidth: showOperateBtns, // 显示网速
showPerformance: showOperateBtns, // 显示性能
operateBtns: {
fullscreen: showOperateBtns,
screenshot: showOperateBtns,
play: showOperateBtns,
audio: showOperateBtns,
ptz: showOperateBtns,
quality: showOperateBtns,
performance: showOperateBtns,
},
heartTimeoutReplayUseLastFrameShow: true,
audioEngine: "worklet",
qualityConfig: ['普清', '高清', '超清', '4K', '8K'],
forceNoOffscreen: forceNoOffscreen,
isNotMute: false,
heartTimeout: 10,
ptzZoomShow:true,
useCanvasRender: $renderDom.value === 'canvas',
useWebGPU: $isUseWebGPU.value === 'webgpu',
controlHtml:'<div>我是 <span style="color: red">test</span>文案</div>'
// audioEngine:"worklet",
// isFlv: true
},);
jessibuca.on('ptz', (arrow) => {
console.log('ptz', arrow);
})
jessibuca.on('streamQualityChange', (value) => {
console.log('streamQualityChange', value);
})
jessibuca.on('timeUpdate', (value) => {
// console.log('timeUpdate', value);
})
jessibuca.on('stats', (stats) => {
// console.log('stats', stats);
$fps.textContent = `FPS: ${stats.fps} DFPS: ${stats.dfps}`
})
$player.style.display = 'inline-block';
$pause.style.display = 'none';
$destroy.style.display = 'none';
$fps.textContent = '';
}
create();
function play() {
var href = $playHref.value;
if (href) {
jessibuca.play(href);
$player.style.display = 'none';
$pause.style.display = 'inline-block';
$destroy.style.display = 'inline-block';
}
}
function replay() {
if (jessibuca) {
jessibuca.destroy().then(() => {
create();
play();
});
} else {
create();
play();
}
}
$replay.addEventListener('click', function () {
replay();
})
$player.addEventListener('click', function () {
play();
}, false)
$pause.addEventListener('click', function () {
$player.style.display = 'inline-block';
$pause.style.display = 'none';
$fps.textContent = '';
jessibuca.pause();
})
$destroy.addEventListener('click', function () {
if (jessibuca) {
jessibuca.destroy().then(() => {
create();
});
} else {
create();
}
})
$useMSE.addEventListener('click', function () {
const checked = $useMSE.checked;
if (checked) {
$useSIMD.checked = false
$useWCS.checked = false
}
replay();
})
$useSIMD.addEventListener('click', function () {
const checked = $useSIMD.checked;
if (checked) {
$useMSE.checked = false
$useWCS.checked = false
}
replay();
})
$useWCS.addEventListener('click', function () {
const checked = $useWCS.checked;
if (checked) {
$useMSE.checked = false
$useSIMD.checked = false
}
replay();
})
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

107
ui/jessibuca-pro-talk.d.ts vendored Normal file
View File

@@ -0,0 +1,107 @@
declare namespace JessibucaProTalk {
enum EVENTS {
talkStreamClose = 'talkStreamClose',
talkStreamError = 'talkStreamError',
talkStreamInactive = 'talkStreamInactive',
talkGetUserMediaTimeout = 'talkGetUserMediaTimeout'
}
interface Config {
// 语音编码类型,支持`g711a`和`g711u`,默认是`g711a`
encType: string,
// 语音包类型,支持`rtp`,默认是`rtp`
packetType: string,
// rtp包的ssrc10位
rtpSsrc: string,
// 采样通道
numberChannels: number,
// 采样率
sampleRate: number,
// 采样精度
sampleBitsWidth: number,
// 是否开启debug模式
debug: boolean,
// debug模式下的日志级别支持`debug`、`warn`,默认是`warn`
debugLevel: string,
// 是否开启测试麦克风,不连接ws
testMicrophone: boolean,
// 语音引擎,支持`worklet`和`script`,默认是`worklet`
engine: string,
// 是否开启检测getUserMedia超时
checkGetUserMediaTimeout: boolean,
// getUserMedia超时时间单位ms
getUserMediaTimeout: number
}
}
declare class JessibucaProTalk {
constructor(config?: JessibucaProTalk.Config);
/**
* 开启语音
* @param wsUrl
* @param options
*/
startTalk(wsUrl, options: JessibucaProTalk.Config): Promise<any>;
/**
* 关闭语音
*/
stopTalk(): Promise<any>;
/**
* 获取语音音量
* * 返回值是一个0-100的数字表示当前语音音量
*/
getTalkVolume(): Promise<Number>;
/**
* 设置语音音量
* @param volume 0-100的数字表示当前语音音量
*/
setTalkVolume(volume: number): Promise<any>;
/**
* 监听ws 断开
* @param event
* @param callback
*/
on(event: JessibucaProTalk.EVENTS.talkStreamClose, callback: Function): void;
/**
* 监听 ws error
* @param event
* @param callback
*/
on(event: JessibucaProTalk.EVENTS.talkStreamError, callback: Function): void;
/**
* 监听 stream oninactive
* @param event
* @param callback
*/
on(event: JessibucaProTalk.EVENTS.talkStreamInactive, callback: Function): void;
/**
* 检查 getUserMedia 超时
* @param event
* @param callback
*/
on(event: JessibucaProTalk.EVENTS.talkGetUserMediaTimeout, callback: Function): void;
/**
* 监听方法
*
@example
JessibucaProTalk.on("talkStreamClose",function(){console.log('talkStreamClose')})
*/
on(event: string, callback: Function): void;
}
export default JessibucaProTalk;

1647
ui/jessibuca-pro-talk.js Normal file

File diff suppressed because it is too large Load Diff

56067
ui/jessibuca-pro.js Normal file

File diff suppressed because one or more lines are too long

10
ui/vconsole.js Normal file

File diff suppressed because one or more lines are too long

1647
ui/web-player-pro-talk.js Normal file

File diff suppressed because it is too large Load Diff

56067
ui/web-player-pro.js Normal file

File diff suppressed because one or more lines are too long