mirror of
https://github.com/bytedance/xgplayer.git
synced 2025-04-05 03:05:02 +08:00
global: 同步3.0.1
This commit is contained in:
parent
2c532737f9
commit
fc8e862ffd
@ -24,44 +24,102 @@
|
||||
</style>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//unpkg.pstatp.com/xgplayer/3.0.0-alpha.103-8/dist/xgplayer.min.css"
|
||||
href="//unpkg.pstatp.com/xgplayer/3.0.0-alpha.110-18/dist/xgplayer.min.css"
|
||||
/>
|
||||
<!-- <script src="//unpkg.pstatp.com/xgplayer/3.0.0-alpha.101-7/dist/index.min.js" charset="utf-8"></script>-->
|
||||
<!-- <script src="http://unpkg.pstatp.com/xgplayer-mp4/3.0.0-next.3/dist/index.min.js" charset="utf-8"></script>-->
|
||||
</head>
|
||||
<button id="btn" onclick="playNext()">播放下一个</button>
|
||||
<body>
|
||||
<section id="wrapper">
|
||||
<div id="vs"></div>
|
||||
</section>
|
||||
<script type="module">
|
||||
import Player from '../../packages/xgplayer/src/index'
|
||||
let player=new Player({
|
||||
id: 'vs',
|
||||
autoplay: true,
|
||||
volume: 0,
|
||||
url:'./err.mp4',
|
||||
poster: "//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/poster.jpg",
|
||||
playsinline: true,
|
||||
thumbnail: {
|
||||
pic_num: 44,
|
||||
width: 160,
|
||||
height: 90,
|
||||
col: 10,
|
||||
row: 10,
|
||||
// urls: ['//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/xgplayer-demo-thumbnail.jpg'],
|
||||
},
|
||||
TestSpeed: {
|
||||
url: '//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/xgplayer-demo-720p.mp4',
|
||||
openSpeed: true,
|
||||
loadSize: 200*1024,
|
||||
testTimeStep: 5000,
|
||||
testCnt: 3,
|
||||
},
|
||||
height: window.innerHeight,
|
||||
width: window.innerWidth,
|
||||
// plugins: [MP4Player]
|
||||
});
|
||||
import Mp4Plugin from '../../packages/xgplayer-mp4/src/index'
|
||||
var videoList = [
|
||||
{
|
||||
definition: '360p',
|
||||
//keyValue: "0ff2ccbec8ab45349ae912c89056bc62",
|
||||
bitrate: 311473,
|
||||
vheight: 360,
|
||||
vwidth: 640,
|
||||
duration:90,
|
||||
url: './video/360p.mp4'
|
||||
},
|
||||
{
|
||||
definition: '480p',
|
||||
//keyValue: "288e672cd2944bd0ab4aa0e03b4de438",
|
||||
bitrate: 437288,
|
||||
vheight: 480,
|
||||
vwidth: 860,
|
||||
duration:90,
|
||||
url: './video/480p.mp4'
|
||||
},
|
||||
{
|
||||
definition: '720p',
|
||||
//keyValue: "d0aabcd3605c4ed8962fc6819764ec90",
|
||||
bitrate: 915105,
|
||||
vheight: 720,
|
||||
vwidth: 1270,
|
||||
duration:90,
|
||||
url: './video/720p.mp4'
|
||||
},
|
||||
{
|
||||
definition: '1080p',
|
||||
//keyValue: "d0aabcd3605c4ed8962fc6819764ec90",
|
||||
bitrate: 2713749,
|
||||
vheight: 1080,
|
||||
vwidth: 1920,
|
||||
duration:90,
|
||||
url: './video/1080p.mp4'
|
||||
}
|
||||
]
|
||||
let player = new Player({
|
||||
id: 'vs',
|
||||
autoplay: true,
|
||||
volume: 0,
|
||||
url: 'http://tosv.byted.org/obj/media-fe/h265.mp4',
|
||||
//url: './video/output.m4a',//'./video/1080p.mp4',
|
||||
// url:'https://lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/xgplayer-demo-720p.mp4',
|
||||
poster: "//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/poster.jpg",
|
||||
playsinline: true,
|
||||
thumbnail: {
|
||||
pic_num: 44,
|
||||
width: 160,
|
||||
height: 90,
|
||||
col: 10,
|
||||
row: 10,
|
||||
// urls: ['//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/xgplayer-demo-thumbnail.jpg'],
|
||||
},
|
||||
definition: {
|
||||
defaultDefinition: '720p',
|
||||
list: videoList
|
||||
},
|
||||
// TestSpeed: {
|
||||
// url: '//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/xgplayer-demo-720p.mp4',
|
||||
// openSpeed: true,
|
||||
// loadSize: 200*1024,
|
||||
// testTimeStep: 5000,
|
||||
// testCnt: 3,
|
||||
// },
|
||||
height: window.innerHeight,
|
||||
width: window.innerWidth,
|
||||
plugins: [Mp4Plugin]
|
||||
});
|
||||
window.player = player
|
||||
|
||||
function playNext() {
|
||||
player.playNext({
|
||||
vid: 'v02d02g10000cag6m4gmmdqu846g76ag123',
|
||||
id: 'mse',
|
||||
isLive: false,
|
||||
autoplay: true,
|
||||
plugins: [Mp4Plugin],
|
||||
url: './video/1080p.mp4'
|
||||
})
|
||||
}
|
||||
window.playNext = playNext
|
||||
player.on('ended', () => {
|
||||
player.replay()
|
||||
})
|
||||
|
@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "fixtures",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@byted/xgplayer-encrypt-mp4": "^3.0.0-alpha.34-23",
|
||||
"@byted/xgplayer-offscreenvideo": "^0.1.8",
|
||||
"@byted/xgplayer-xgvideo": "^0.1.6-beta.14",
|
||||
"xgplayer-flv-live": "^3.0.0-alpha.131"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -1,44 +0,0 @@
|
||||
<!--
|
||||
* @Descripttion:
|
||||
* @version:
|
||||
* @Date: 2022-06-10 14:42:30
|
||||
* @LastEditors: wuranran
|
||||
* @LastEditTime: 2022-06-22 22:27:18
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<link rel="stylesheet" href="xgplayer.css">
|
||||
<script src="https://unpkg.com/vconsole/dist/vconsole.min.js"></script>
|
||||
<style>
|
||||
#video{
|
||||
position: absolute;
|
||||
/* left:200px;
|
||||
top:200px; */
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var ua = navigator.userAgent.toLowerCase()
|
||||
console.log('ua', ua, (ua.indexOf('mobile') > -1 || ua.indexOf('ipad') > -1) && location.href.indexOf('vconsole=1') > -1)
|
||||
if ((ua.indexOf('mobile') > -1 || ua.indexOf('ipad') > -1) && location.href.indexOf('vconsole=1') > -1) {
|
||||
var vConsole = new window.VConsole();
|
||||
}
|
||||
</script>
|
||||
<script src="xgplayer.js?cwecewcwe"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="video"></div>
|
||||
|
||||
|
||||
<!-- <script defer type ="module" src="index.js"></script> -->
|
||||
<script src="xgplayer-vr.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,98 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<!-- <script src="https://unpkg.com/vconsole/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
var ua = navigator.userAgent.toLowerCase()
|
||||
console.log('ua', ua, (ua.indexOf('mobile') > -1 || ua.indexOf('ipad') > -1) && location.href.indexOf('vconsole=1') > -1)
|
||||
if ((ua.indexOf('mobile') > -1 || ua.indexOf('ipad') > -1) && location.href.indexOf('vconsole=1') > -1) {
|
||||
var vConsole = new window.VConsole();
|
||||
}
|
||||
</script> -->
|
||||
<style>
|
||||
.pannel {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background: #72a0c8;
|
||||
}
|
||||
.message-pannel {
|
||||
margin-top: 20px;
|
||||
color: #333a3c;
|
||||
}
|
||||
.message-info {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #728bb8;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
h4 {
|
||||
margin-block-start: 0.2em;
|
||||
margin-block-end: 0.2em;
|
||||
}
|
||||
p {
|
||||
line-height: 18px;
|
||||
display: block;
|
||||
line-height: 16px;
|
||||
/* margin: 10px; */
|
||||
margin-block-start: 0.4em;
|
||||
margin-block-end: 0.4em;
|
||||
}
|
||||
.ext-controls0 {
|
||||
height: 80px!important;
|
||||
background-color: #000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="video0"></div>
|
||||
<div class="pannel">
|
||||
<div class="tool">
|
||||
<button type="submit" class="btn" id="js-destroy0" onclick="window.destroy(0)">销毁</button>
|
||||
<button type="submit" class="btn" id="js-reinit0" onclick="window.init(0)">重新初始化</button>
|
||||
<button type="submit" class="btn" id="js-playnext0" onclick="window.playNext(0)">播放下一个</button>
|
||||
<button type="submit" class="btn" id="js-changelang0" onclick="window.changeLang(0)">切换语言</button>
|
||||
<button type="submit" class="btn" id="js-changelang0" onclick="window.createDot(0)">添加预览点</button>
|
||||
</div>
|
||||
<div class="message-pannel">
|
||||
<div class="message-info" id="js-show-lang0">
|
||||
<h4>current lang:</h4>
|
||||
</div>
|
||||
|
||||
<div class="message-info" id="js-show-log0">
|
||||
<h4>log info:</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="xgplayer ext-controls0" id="controls0"></div>
|
||||
|
||||
<div id="video1"></div>
|
||||
<div class="pannel">
|
||||
<div class="tool">
|
||||
<button type="submit" class="btn" id="js-destroy1" onclick="window.destroy(1)">销毁</button>
|
||||
<button type="submit" class="btn" id="js-reinit1" onclick="window.init(1)">重新初始化</button>
|
||||
<button type="submit" class="btn" id="js-playnext1" onclick="window.playNext(1)">播放下一个</button>
|
||||
<button type="submit" class="btn" id="js-changelang1" onclick="window.changeLang(1)">切换语言</button>
|
||||
</div>
|
||||
<div class="message-pannel">
|
||||
<div class="message-info" id="js-show-lang1">
|
||||
<h4>current lang:</h4>
|
||||
</div>
|
||||
|
||||
<div class="message-info" id="js-show-log1">
|
||||
<h4>log info:</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
<script type="module" defer src="./index.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -25,9 +25,9 @@
|
||||
"libd": {
|
||||
"legacy": {
|
||||
"enabled": true,
|
||||
"needPolyfills": true,
|
||||
"esEnabled": true,
|
||||
"esNeedPolyfills": true
|
||||
"needPolyfills": true,
|
||||
"esNeedPolyfills": false
|
||||
},
|
||||
"closeUpdatePeerDeps": true
|
||||
},
|
||||
@ -84,6 +84,7 @@
|
||||
"jest": "^28.1.1",
|
||||
"jest-environment-jsdom": "^28.1.1",
|
||||
"klaw-sync": "^6.0.0",
|
||||
"rollup-plugin-visualizer": "^5.9.0",
|
||||
"sade": "^1.7.4",
|
||||
"sass": "^1.43.4",
|
||||
"semver": "^7.3.5",
|
||||
@ -91,6 +92,7 @@
|
||||
"ts-morph": "^14.0.0",
|
||||
"url-toolkit": "2.1.6",
|
||||
"vite": "^2.9.8",
|
||||
"vite-plugin-externals": "0.5.1",
|
||||
"zlib": "^1.0.5"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,18 @@
|
||||
|
||||
## xgplayer-flv@next.24-1
|
||||
chore: 更新 xgplayer-streaming-shared@3.0.0-next.33
|
||||
fix: getStats() 统计帧率不准确问题
|
||||
|
||||
## xgplayer-flv@next.24
|
||||
chore: 更新 xgplayer-streaming-shared@3.0.0-next.32
|
||||
fix: getStats() 统计帧率不准确问题
|
||||
|
||||
## xgplayer-flv@next.23
|
||||
chore: 更新 xgplayer-streaming-shared@3.0.0-next.31
|
||||
|
||||
## xgplayer-flv@next.20
|
||||
fix: catch for play() call
|
||||
|
||||
## xgplayer-flv@next.20
|
||||
fix: catch for play() call
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xgplayer-flv",
|
||||
"version": "3.0.0-next.20",
|
||||
"version": "3.0.1",
|
||||
"main": "dist/index.min.js",
|
||||
"module": "es/index.js",
|
||||
"typings": "es/index.d.ts",
|
||||
@ -14,8 +14,7 @@
|
||||
],
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"access": "public",
|
||||
"tag": "next"
|
||||
"access": "public"
|
||||
},
|
||||
"license": "MIT",
|
||||
"unpkgFiles": [
|
||||
@ -23,11 +22,11 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.7",
|
||||
"xgplayer-transmuxer": "3.0.0-next.25",
|
||||
"xgplayer-streaming-shared": "3.0.0-next.27"
|
||||
"xgplayer-transmuxer": "3.0.1",
|
||||
"xgplayer-streaming-shared": "3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xgplayer": ">=3.0.0-next.32",
|
||||
"xgplayer": ">=3.0.1",
|
||||
"core-js": ">=3.12.1"
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ import { searchKeyframeIndex } from './utils'
|
||||
|
||||
export const logger = new Logger('flv')
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {import("../../../xgplayer-streaming-shared/es/services/stats").StatsInfo} Stats
|
||||
*/
|
||||
export class Flv extends EventEmitter {
|
||||
/** @type {HTMLMediaElement | null} */
|
||||
media = null
|
||||
@ -16,22 +20,23 @@ export class Flv extends EventEmitter {
|
||||
/** @type {import('./options').FlvOption} */
|
||||
_opts = null
|
||||
|
||||
/** @type {BufferService | null} */
|
||||
/** @type {BufferService} */
|
||||
_bufferService = null
|
||||
|
||||
/** @type {GapService | null} */
|
||||
/** @type {GapService} */
|
||||
_gapService = null
|
||||
|
||||
/** @type {MediaStatsService} */
|
||||
_stats = null
|
||||
|
||||
/** @type {NetLoader} */
|
||||
_mediaLoader = null
|
||||
|
||||
_maxChunkWaitTimer = null
|
||||
|
||||
_tickTimer = null
|
||||
_tickInterval = 500
|
||||
|
||||
_noEndOfStreamOnDone = false
|
||||
_urlSwitching = false
|
||||
_seamlessSwitching = false
|
||||
|
||||
@ -97,6 +102,9 @@ export class Flv extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Stats}
|
||||
*/
|
||||
getStats () {
|
||||
return this._stats.getStats()
|
||||
}
|
||||
@ -128,7 +136,6 @@ export class Flv extends EventEmitter {
|
||||
async replay (seamlesslyReload = this._opts.seamlesslyReload, isPlayEmit) {
|
||||
if (!this.media) return
|
||||
if (seamlesslyReload) {
|
||||
this._noEndOfStreamOnDone = true
|
||||
await this._clear()
|
||||
|
||||
setTimeout(() => {
|
||||
@ -159,7 +166,6 @@ export class Flv extends EventEmitter {
|
||||
return this.media.play(true).catch(()=>{})
|
||||
}
|
||||
|
||||
this._noEndOfStreamOnDone = true
|
||||
await this._clear()
|
||||
|
||||
setTimeout(() => {
|
||||
@ -183,6 +189,10 @@ export class Flv extends EventEmitter {
|
||||
this._bufferService = null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {('video'|'audio')?} mediaType
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
static isSupported (mediaType) {
|
||||
if (!mediaType || mediaType === 'video' || mediaType === 'audio') {
|
||||
return MSE.isSupported()
|
||||
@ -233,7 +243,7 @@ export class Flv extends EventEmitter {
|
||||
|
||||
this.emit(EVENT.LOAD_START, { url })
|
||||
|
||||
logger.debug('load data, loding:', this._loading, url)
|
||||
logger.debug('load data, loading:', this._loading, url)
|
||||
|
||||
if (this._loading) {
|
||||
await this._mediaLoader.cancel()
|
||||
@ -301,13 +311,8 @@ export class Flv extends EventEmitter {
|
||||
|
||||
if (done && !this.media.seeking) {
|
||||
this.emit(EVENT.LOAD_COMPLETE)
|
||||
logger.debug('load done', this._noEndOfStreamOnDone)
|
||||
|
||||
if (this._noEndOfStreamOnDone) {
|
||||
this._noEndOfStreamOnDone = false
|
||||
} else {
|
||||
return this._end()
|
||||
}
|
||||
logger.debug('load done')
|
||||
return this._end()
|
||||
} else {
|
||||
const { maxReaderInterval } = this._opts
|
||||
if (maxReaderInterval) {
|
||||
|
@ -75,7 +75,6 @@ export class FlvPlugin extends BasePlugin {
|
||||
}
|
||||
|
||||
this.on(Events.URL_CHANGE, this._onSwitchURL)
|
||||
// this.on(Events.DEFINITION_CHANGE, this._onDefinitionChange)
|
||||
this.on(Events.DESTROY, this.destroy)
|
||||
|
||||
this._transError()
|
||||
@ -97,11 +96,14 @@ export class FlvPlugin extends BasePlugin {
|
||||
this._transCoreEvent(EVENT.SWITCH_URL_SUCCESS)
|
||||
this._transCoreEvent(EVENT.SWITCH_URL_FAILED)
|
||||
|
||||
this.flv.load(config.url, true)
|
||||
return this.flv.load(config.url, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {import('./flv').Stats | undefined}
|
||||
*/
|
||||
getStats = () => {
|
||||
return this.flv?.getStats() || {}
|
||||
return this.flv?.getStats()
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
@ -113,11 +115,22 @@ export class FlvPlugin extends BasePlugin {
|
||||
this.pluginExtension = null
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
/**
|
||||
* @param {string | boolean} [mediaType]
|
||||
* @param {string} [codec]
|
||||
* @returns {boolean}
|
||||
* - mediaType: 默认检测 MSE 对 H264 codec是否支持,传入 true 或者配置参数的mediaType的取值检测 WebAssembly是否支持
|
||||
* - codec: 暂无使用
|
||||
*/
|
||||
static isSupported (mediaType, codec) {
|
||||
return Flv.isSupported(mediaType, codec)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {boolean} seamless
|
||||
*/
|
||||
_onSwitchURL = (url, seamless) => {
|
||||
if (this.flv) {
|
||||
this.player.config.url = url
|
||||
|
@ -1,3 +1,21 @@
|
||||
|
||||
## xgplayer-hls@3.0.0-next.37-1
|
||||
chore: 更新 xgplayer-streaming-shared@3.0.0-next.33
|
||||
|
||||
## xgplayer-hls@3.0.0-next.37
|
||||
chore: 更新 xgplayer-streaming-shared@3.0.0-next.32
|
||||
fix: track发生变化判断影响软解播放
|
||||
|
||||
## xgplayer-hls@3.0.0-next.36
|
||||
chore: 更新 xgplayer-streaming-shared@3.0.0-next.31
|
||||
fix: (xgplayer-hls) track发生变化检测默认开启`allowedStreamTrackChange`, 兼容seek场景
|
||||
|
||||
## xgplayer-hls@3.0.0-next.35
|
||||
fix: 🐛 (xgplayer-hls) 支持无缝切换码率
|
||||
|
||||
## xgplayer-hls@3.0.0-next.34
|
||||
fix: 🐛 (xgplayer-hls) HLS直播支持显示 webvtt
|
||||
|
||||
## xgplayer-hls@3.0.0-next.33
|
||||
fix: 🐛 (xgplayer-hls) 兼容m3u8 endlist之后有冗余内容的情况
|
||||
|
||||
|
@ -5,9 +5,17 @@ jest.mock('../src/hls/playlist')
|
||||
|
||||
import { Hls } from '../src/hls'
|
||||
import { BufferService } from '../src/hls/buffer-service'
|
||||
import { Playlist } from '../src/hls/playlist'
|
||||
import { Playlist } from '../src/hls/playlist'
|
||||
import { Logger as TransmuxerLogger } from 'xgplayer-transmuxer'
|
||||
import { NetLoader, BandwidthService, getVideoPlaybackQuality, Buffer, MSE, Logger } from 'xgplayer-streaming-shared'
|
||||
import {
|
||||
NetLoader,
|
||||
BandwidthService,
|
||||
getVideoPlaybackQuality,
|
||||
Buffer,
|
||||
MSE,
|
||||
Logger,
|
||||
MediaStatsService
|
||||
} from 'xgplayer-streaming-shared'
|
||||
|
||||
describe('Hls', () => {
|
||||
const { EVENT } = jest.requireActual('xgplayer-streaming-shared')
|
||||
@ -39,12 +47,14 @@ describe('Hls', () => {
|
||||
TransmuxerLogger.disable = tLoggerDisable
|
||||
|
||||
const bufferServiceReset = jest.fn()
|
||||
const updateDuration = jest.fn()
|
||||
const endOfStream = jest.fn()
|
||||
const bufferDestroy = jest.fn()
|
||||
const seamlessSwitch = jest.fn()
|
||||
const clearAllBuffer = jest.fn()
|
||||
BufferService.mockImplementation(() => {
|
||||
return {
|
||||
updateDuration,
|
||||
reset: bufferServiceReset,
|
||||
endOfStream,
|
||||
seamlessSwitch,
|
||||
@ -60,8 +70,12 @@ describe('Hls', () => {
|
||||
reset: bandwidthServiceReset,
|
||||
appendBuffer,
|
||||
addChunkRecord: jest.fn(),
|
||||
getLatestSpeed () { return 1 },
|
||||
getAvgSpeed () { return 1 },
|
||||
getLatestSpeed() {
|
||||
return 1
|
||||
},
|
||||
getAvgSpeed() {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -107,8 +121,22 @@ describe('Hls', () => {
|
||||
|
||||
test('public properties', () => {
|
||||
const hls = new Hls({ media })
|
||||
const baseDtsType = typeof hls.baseDts
|
||||
const MediaStatsServiceMockData = {
|
||||
avgSpeed: 0,
|
||||
currentTime: 0,
|
||||
bufferEnd: 0,
|
||||
decodeFps: 1
|
||||
}
|
||||
|
||||
jest.spyOn(MediaStatsService.prototype, 'getStats').mockImplementation(() => {
|
||||
return MediaStatsServiceMockData
|
||||
})
|
||||
|
||||
expect(hls.media).toBe(media)
|
||||
expect(hls.version).toBe('test')
|
||||
expect(baseDtsType === 'undefined' || baseDtsType === 'number').toBe(true)
|
||||
expect(hls.getStats()).toEqual(MediaStatsServiceMockData)
|
||||
})
|
||||
|
||||
test('info methods', () => {
|
||||
@ -169,6 +197,27 @@ describe('Hls', () => {
|
||||
await hls.switchURL('url')
|
||||
expect(load).toHaveBeenCalled()
|
||||
expect(media.play).toHaveBeenCalled()
|
||||
|
||||
// argument `options` should not support incorrect type
|
||||
expect
|
||||
.extend({
|
||||
async switchUrlFailureByReceivedUnExpectedArgs() {
|
||||
let passed = true
|
||||
try {
|
||||
await hls.switchURL('url', function () {})
|
||||
} catch {
|
||||
passed = false
|
||||
}
|
||||
|
||||
return {
|
||||
message: () =>
|
||||
passed ? '' : `switchUrl failed while receiving unExpected args`,
|
||||
pass: passed
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await expect().not.switchUrlFailureByReceivedUnExpectedArgs()
|
||||
})
|
||||
|
||||
test('destroy', async () => {
|
||||
@ -192,6 +241,5 @@ describe('Hls', () => {
|
||||
expect(loaderCancel).toHaveBeenCalled()
|
||||
expect(clearAllBuffer).toHaveBeenCalled()
|
||||
expect(hls.currentStream).toBe(streams[1])
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
14
packages/xgplayer-hls/__tests__/index.spec.js
Normal file
14
packages/xgplayer-hls/__tests__/index.spec.js
Normal file
@ -0,0 +1,14 @@
|
||||
import HlsPluginExport, { EVENT, HlsPlugin, parseSwitchUrlArgs } from '../src/index'
|
||||
|
||||
describe('Exports', () => {
|
||||
test('default exports', () => {
|
||||
expect(HlsPluginExport).toBeDefined()
|
||||
})
|
||||
|
||||
test('named exports', () => {
|
||||
// Existed
|
||||
expect(HlsPlugin).toBeDefined()
|
||||
expect(EVENT).toBeDefined()
|
||||
expect(parseSwitchUrlArgs).toBeDefined()
|
||||
})
|
||||
})
|
@ -1,97 +1,116 @@
|
||||
jest.mock('xgplayer')
|
||||
jest.mock('../src/hls')
|
||||
|
||||
import HlsPlugin from '../src'
|
||||
import { HlsPlugin, parseSwitchUrlArgs } from '../src/plugin'
|
||||
import { Hls } from '../src/hls'
|
||||
|
||||
describe('HlsPlugin', () => {
|
||||
const { EVENT } = jest.requireActual('xgplayer-streaming-shared')
|
||||
|
||||
test('static property', () => {
|
||||
expect(HlsPlugin.pluginName).toBe('hls')
|
||||
expect(HlsPlugin.Hls).toBe(Hls)
|
||||
|
||||
const isSupported = jest.spyOn(Hls, 'isSupported')
|
||||
HlsPlugin.isSupported(true)
|
||||
expect(isSupported).toHaveBeenLastCalledWith(true, undefined)
|
||||
});
|
||||
|
||||
test('instance property', () => {
|
||||
const plugin = new HlsPlugin()
|
||||
plugin.player = {
|
||||
config: {
|
||||
url: 'mock url',
|
||||
mediaType: 'live-video'
|
||||
},
|
||||
useHooks: jest.fn()
|
||||
}
|
||||
|
||||
jest.spyOn(Hls.prototype, 'load').mockImplementation(async function(){})
|
||||
|
||||
plugin.beforePlayerInit()
|
||||
|
||||
expect(plugin.softDecode).toBe(true)
|
||||
expect(plugin.hls).toBeTruthy()
|
||||
expect(plugin.hls).toBe(plugin.core)
|
||||
expect(plugin.version).toBe(plugin.hls.version)
|
||||
describe('Plugin', () => {
|
||||
describe('Named Exports', () => {
|
||||
test('parseSwitchUrlArgs', () => {
|
||||
const plugin = {player: {}}
|
||||
expect(Object.keys(parseSwitchUrlArgs(false, plugin))).toContain('startTime')
|
||||
expect(Object.keys(parseSwitchUrlArgs(false, plugin))).toContain('seamless')
|
||||
expect(Object.keys(parseSwitchUrlArgs(undefined, plugin))).not.toContain('seamless')
|
||||
})
|
||||
})
|
||||
|
||||
test('assign method to player', () => {
|
||||
const plugin = new HlsPlugin()
|
||||
plugin.player = {
|
||||
config: {
|
||||
url: 'mock url',
|
||||
mediaType: 'live-video'
|
||||
},
|
||||
useHooks: jest.fn()
|
||||
}
|
||||
plugin.beforePlayerInit()
|
||||
describe('HlsPlugin', () => {
|
||||
const { EVENT } = jest.requireActual('xgplayer-streaming-shared')
|
||||
|
||||
expect(plugin.player.switchURL).toBeTruthy()
|
||||
test('parseSwitchUrlArgs', () => {
|
||||
expect(HlsPlugin.pluginName).toBe('hls')
|
||||
expect(HlsPlugin.Hls).toBe(Hls)
|
||||
|
||||
const isSupported = jest.spyOn(Hls, 'isSupported')
|
||||
HlsPlugin.isSupported(true)
|
||||
expect(isSupported).toHaveBeenLastCalledWith(true, undefined)
|
||||
})
|
||||
|
||||
test('static property', () => {
|
||||
expect(HlsPlugin.pluginName).toBe('hls')
|
||||
expect(HlsPlugin.Hls).toBe(Hls)
|
||||
|
||||
const isSupported = jest.spyOn(Hls, 'isSupported')
|
||||
HlsPlugin.isSupported(true)
|
||||
expect(isSupported).toHaveBeenLastCalledWith(true, undefined)
|
||||
})
|
||||
|
||||
test('instance property', () => {
|
||||
const plugin = new HlsPlugin()
|
||||
plugin.player = {
|
||||
config: {
|
||||
url: 'mock url',
|
||||
mediaType: 'live-video'
|
||||
},
|
||||
useHooks: jest.fn()
|
||||
}
|
||||
|
||||
jest.spyOn(Hls.prototype, 'load').mockImplementation(async function () {})
|
||||
|
||||
plugin.beforePlayerInit()
|
||||
|
||||
expect(plugin.softDecode).toBe(true)
|
||||
expect(plugin.hls).toBeTruthy()
|
||||
expect(plugin.hls).toBe(plugin.core)
|
||||
expect(plugin.version).toBe(plugin.hls.version)
|
||||
})
|
||||
|
||||
test('assign method to player', () => {
|
||||
const plugin = new HlsPlugin()
|
||||
plugin.player = {
|
||||
config: {
|
||||
url: 'mock url',
|
||||
mediaType: 'live-video'
|
||||
},
|
||||
useHooks: jest.fn()
|
||||
}
|
||||
plugin.beforePlayerInit()
|
||||
|
||||
expect(plugin.player.switchURL).toBeTruthy()
|
||||
})
|
||||
|
||||
test('should transfer event and error', () => {
|
||||
const plugin = new HlsPlugin()
|
||||
plugin.player = {
|
||||
config: {
|
||||
url: 'mock url',
|
||||
mediaType: 'live-video'
|
||||
},
|
||||
useHooks: jest.fn()
|
||||
}
|
||||
plugin.hls = { on: jest.fn(), destroy: jest.fn() }
|
||||
plugin.beforePlayerInit()
|
||||
const calls = plugin.hls.on.mock.calls
|
||||
|
||||
expect(calls.find(x => x[0] === EVENT.ERROR)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.TTFB)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.LOAD_START)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.LOAD_RESPONSE_HEADERS)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.LOAD_COMPLETE)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.LOAD_RETRY)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.KEYFRAME)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.METADATA_PARSED)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.SEI)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.SPEED)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.STREAM_EXCEPTION)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.SWITCH_URL_SUCCESS)).toBeTruthy()
|
||||
expect(calls.find(x => x[0] === EVENT.SWITCH_URL_FAILED)).toBeTruthy()
|
||||
})
|
||||
|
||||
test('destroy', () => {
|
||||
const plugin = new HlsPlugin()
|
||||
const hlsDestroy = jest.fn()
|
||||
plugin.player = {
|
||||
config: {
|
||||
url: 'mock url',
|
||||
mediaType: 'live-video'
|
||||
},
|
||||
useHooks: jest.fn()
|
||||
}
|
||||
plugin.hls = { destroy: hlsDestroy }
|
||||
plugin.beforePlayerInit()
|
||||
plugin.destroy()
|
||||
expect(hlsDestroy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
test('should transfer event and error', () => {
|
||||
const plugin = new HlsPlugin()
|
||||
plugin.player = {
|
||||
config: {
|
||||
url: 'mock url',
|
||||
mediaType: 'live-video'
|
||||
},
|
||||
useHooks: jest.fn()
|
||||
}
|
||||
plugin.hls = { on: jest.fn(), destroy: jest.fn() }
|
||||
plugin.beforePlayerInit()
|
||||
const calls = plugin.hls.on.mock.calls
|
||||
|
||||
expect(calls.find(x=>x[0]===EVENT.ERROR)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.TTFB)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.LOAD_START)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.LOAD_RESPONSE_HEADERS)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.LOAD_COMPLETE)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.LOAD_RETRY)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.KEYFRAME)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.METADATA_PARSED)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.SEI)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.SPEED)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.STREAM_EXCEPTION)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.SWITCH_URL_SUCCESS)).toBeTruthy()
|
||||
expect(calls.find(x=>x[0]===EVENT.SWITCH_URL_FAILED)).toBeTruthy()
|
||||
})
|
||||
|
||||
test('destroy', () => {
|
||||
const plugin = new HlsPlugin()
|
||||
const hlsDestroy = jest.fn()
|
||||
plugin.player = {
|
||||
config: {
|
||||
url: 'mock url',
|
||||
mediaType: 'live-video'
|
||||
},
|
||||
useHooks: jest.fn()
|
||||
}
|
||||
plugin.hls = { destroy: hlsDestroy }
|
||||
plugin.beforePlayerInit()
|
||||
plugin.destroy()
|
||||
expect(hlsDestroy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
})
|
||||
|
@ -126,6 +126,16 @@ describe('BufferService', () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('constructor', async () => {
|
||||
hls.config.url = ''
|
||||
new BufferService(hls)
|
||||
expect(bindMedia).not.toHaveBeenCalled()
|
||||
|
||||
hls.config.url = 'url'
|
||||
new BufferService(hls)
|
||||
expect(bindMedia).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('appendBuffer', async () => {
|
||||
const bs = new BufferService(hls)
|
||||
bs.createSource(
|
||||
@ -156,6 +166,13 @@ describe('BufferService', () => {
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('removeBuffer', async () => {
|
||||
const bs = new BufferService(hls)
|
||||
|
||||
await bs.removeBuffer(20, 0)
|
||||
expect(clearBuffer).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('evictBuffer', async () => {
|
||||
const bs = new BufferService(hls)
|
||||
|
||||
@ -223,4 +240,10 @@ describe('BufferService', () => {
|
||||
await bs.clearAllBuffer()
|
||||
expect(clearAllBuffer).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('seamlessSwitch', async () => {
|
||||
const bs = new BufferService(hls)
|
||||
bs.seamlessSwitch()
|
||||
expect(bs._needInitSegment).toBe(true)
|
||||
})
|
||||
})
|
||||
|
@ -8,6 +8,7 @@ describe('Utils', () => {
|
||||
expect(clamp(3, 0, 2)).toBe(2)
|
||||
expect(clamp(0, 0, 2)).toBe(0)
|
||||
expect(clamp(2, 0, 2)).toBe(2)
|
||||
expect(clamp(2, 0, -1)).toBe(0)
|
||||
})
|
||||
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xgplayer-hls",
|
||||
"version": "3.0.0-next.34",
|
||||
"version": "3.0.1",
|
||||
"main": "dist/index.min.js",
|
||||
"module": "es/index.js",
|
||||
"typings": "es/index.d.ts",
|
||||
@ -14,8 +14,7 @@
|
||||
],
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"access": "public",
|
||||
"tag": "next"
|
||||
"access": "public"
|
||||
},
|
||||
"license": "MIT",
|
||||
"unpkgFiles": [
|
||||
@ -23,11 +22,11 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.7",
|
||||
"xgplayer-transmuxer": "3.0.0-next.25",
|
||||
"xgplayer-streaming-shared": "3.0.0-next.27"
|
||||
"xgplayer-transmuxer": "3.0.1",
|
||||
"xgplayer-streaming-shared": "3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xgplayer": "3.0.0-next.49",
|
||||
"xgplayer": ">=3.0.1",
|
||||
"core-js": ">=3.12.1"
|
||||
}
|
||||
}
|
||||
|
@ -23,11 +23,14 @@ export class BufferService {
|
||||
this._softVideo = hls.media
|
||||
} else {
|
||||
this._mse = new MSE()
|
||||
const _ret = this._mse.bindMedia(hls.media)
|
||||
if (_ret && _ret.then) {
|
||||
_ret.then(() => {
|
||||
hls && hls.emit('sourceAttached')
|
||||
})
|
||||
|
||||
if (hls.config.url) {
|
||||
const _ret = this._mse.bindMedia(hls.media)
|
||||
if (_ret && _ret.then) {
|
||||
_ret.then(() => {
|
||||
hls && hls.emit('sourceAttached')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +44,12 @@ export class BufferService {
|
||||
return this._transmuxer?._demuxer?._fixer?._baseDts
|
||||
}
|
||||
|
||||
get nbSb () {
|
||||
if (!this._mse?._sourceBuffer) return 0
|
||||
|
||||
return Object.keys(this._mse._sourceBuffer).length
|
||||
}
|
||||
|
||||
async updateDuration (duration) {
|
||||
logger.debug('update duration', duration)
|
||||
if (this._mse) {
|
||||
@ -154,6 +163,15 @@ export class BufferService {
|
||||
}
|
||||
}
|
||||
|
||||
async removeBuffer (start = 0, end = Infinity) {
|
||||
const media = this.hls.media
|
||||
if (!this._mse || !media || start < 0 || end < start || start >= this._mse.duration) return
|
||||
|
||||
return this._mse
|
||||
.clearBuffer(start, end)
|
||||
.then(() => this.hls.emit(EVENT.REMOVE_BUFFER, { start, end, removeEnd: end }))
|
||||
}
|
||||
|
||||
async evictBuffer (bufferBehind) {
|
||||
const media = this.hls.media
|
||||
if (!this._mse || !media || !bufferBehind || bufferBehind < 0) return
|
||||
@ -162,9 +180,7 @@ export class BufferService {
|
||||
if (removeEnd <= 0) return
|
||||
const start = Buffer.start(Buffer.get(media))
|
||||
if (start + 1 >= removeEnd) return
|
||||
return this._mse
|
||||
.clearBuffer(0, removeEnd)
|
||||
.then(() => this.hls.emit(EVENT.REMOVE_BUFFER, { removeEnd }))
|
||||
return this.removeBuffer(0, removeEnd)
|
||||
}
|
||||
|
||||
async clearAllBuffer () {
|
||||
@ -177,11 +193,11 @@ export class BufferService {
|
||||
|
||||
async reset (reuseMse = false) {
|
||||
if (this._mse && !reuseMse) {
|
||||
this._sourceCreated = false
|
||||
await this._mse.unbindMedia()
|
||||
await this._mse.bindMedia(this.hls.media)
|
||||
}
|
||||
this._transmuxer = null
|
||||
this._sourceCreated = false
|
||||
this._needInitSegment = true
|
||||
this._directAppend = false
|
||||
}
|
||||
@ -251,4 +267,8 @@ export class BufferService {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
seamlessSwitch () {
|
||||
this._needInitSegment = true
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ export function getConfig (cfg) {
|
||||
maxPlaylistSize: 50,
|
||||
retryCount: 3,
|
||||
retryDelay: 1000,
|
||||
pollRetryCount: 2,
|
||||
loadTimeout: 10000,
|
||||
preloadTime: 30,
|
||||
softDecode: false,
|
||||
@ -39,7 +40,7 @@ export function getConfig (cfg) {
|
||||
startTime: 0,
|
||||
targetLatency: 10,
|
||||
maxLatency: 20,
|
||||
allowedStreamTrackChange: false,
|
||||
allowedStreamTrackChange: true,
|
||||
...cfg,
|
||||
media
|
||||
}
|
||||
|
@ -3,5 +3,8 @@ import { EVENT } from 'xgplayer-streaming-shared'
|
||||
export const Event = {
|
||||
...EVENT,
|
||||
STREAM_PARSED: 'core.streamparsed',
|
||||
NO_AUDIO_TRACK: 'core.noaudiotrack'
|
||||
NO_AUDIO_TRACK: 'core.noaudiotrack',
|
||||
SUBTITLE_SEGMENTS: 'core.subtitlesegments',
|
||||
SUBTITLE_PLAYLIST: 'core.subtitleplaylist',
|
||||
SEI_PAYLOAD_TIME: 'core.seipayloadtime'
|
||||
}
|
||||
|
@ -12,6 +12,16 @@ import { clamp } from './utils'
|
||||
/**
|
||||
* @typedef {import('./manifest-loader/parser/model').MediaSegment} MediaSegment
|
||||
*/
|
||||
/**
|
||||
* @typedef {import("../../../xgplayer-streaming-shared/es/services/stats").StatsInfo} Stats
|
||||
*/
|
||||
/**
|
||||
* @typedef {{
|
||||
* seamless?: boolean,
|
||||
* startTime?: number,
|
||||
* bitrate?: number
|
||||
* }} SwitchUrlOptions
|
||||
*/
|
||||
|
||||
const logger = new Logger('hls')
|
||||
|
||||
@ -53,6 +63,8 @@ export class Hls extends EventEmitter {
|
||||
_segmentProcessing = false
|
||||
_reloadOnPlay = false
|
||||
|
||||
_switchUrlOpts = null
|
||||
|
||||
constructor (cfg) {
|
||||
super()
|
||||
this.config = cfg = getConfig(cfg)
|
||||
@ -76,6 +88,7 @@ export class Hls extends EventEmitter {
|
||||
get isLive () { return this._playlist.isLive }
|
||||
get streams () { return this._playlist.streams }
|
||||
get currentStream () { return this._playlist.currentStream }
|
||||
get hasSubtitle () { return this._playlist.hasSubtitle}
|
||||
|
||||
get baseDts () {
|
||||
return this._bufferService?.baseDts
|
||||
@ -89,6 +102,9 @@ export class Hls extends EventEmitter {
|
||||
return Buffer.info(Buffer.get(this.media), this.media?.currentTime, maxHole)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Stats}
|
||||
*/
|
||||
getStats () {
|
||||
return this._stats.getStats()
|
||||
}
|
||||
@ -101,13 +117,70 @@ export class Hls extends EventEmitter {
|
||||
if (url) this.config.url = url
|
||||
url = this.config.url
|
||||
await this._reset(reuseMse)
|
||||
if (url) url = url.trim()
|
||||
if (!url) throw this._emitError(new StreamingError(ERR.OTHER, ERR.SUB_TYPES.OPTION, null, null, 'm3u8 url is missing'))
|
||||
await this._loadM3U8(url)
|
||||
await this._loadSegment()
|
||||
await this._loadData(url)
|
||||
this._startTick()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @private
|
||||
*/
|
||||
async _loadData (url) {
|
||||
try {
|
||||
if (url) url = url.trim()
|
||||
} catch (e) {}
|
||||
|
||||
if (!url) throw this._emitError(new StreamingError(ERR.OTHER, ERR.SUB_TYPES.OPTION, null, null, 'm3u8 url is missing'))
|
||||
|
||||
const manifest = await this._loadM3U8(url)
|
||||
const { currentStream } = this._playlist
|
||||
|
||||
if (this._urlSwitching) {
|
||||
if (currentStream.bitrate === 0 && this._switchUrlOpts?.bitrate) {
|
||||
currentStream.bitrate = this._switchUrlOpts?.bitrate
|
||||
}
|
||||
const switchTimePoint = this._getSeamlessSwitchPoint()
|
||||
this.config.startTime = switchTimePoint
|
||||
|
||||
const segIdx = this._playlist.findSegmentIndexByTime(switchTimePoint)
|
||||
const nextSeg = this._playlist.getSegmentByIndex(segIdx + 1)
|
||||
|
||||
if (nextSeg) {
|
||||
// move to next segment in case of media stall
|
||||
const bufferClearStartPoint = nextSeg.start
|
||||
await this._bufferService.removeBuffer(bufferClearStartPoint)
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest) {
|
||||
if (this.isLive) {
|
||||
this._bufferService.setLiveSeekableRange(0, 0xffffffff)
|
||||
logger.log('totalDuration first time got', this._playlist.totalDuration)
|
||||
|
||||
// 配置的目标延迟小于首次获取分片总时长
|
||||
if (this.config.targetLatency < this._playlist.totalDuration) {
|
||||
this.config.targetLatency = this._playlist.totalDuration
|
||||
this.config.maxLatency = 1.5 * this.config.targetLatency
|
||||
}
|
||||
|
||||
if (!manifest.isMaster) this._pollM3U8(url)
|
||||
} else {
|
||||
await this._bufferService.updateDuration(currentStream.totalDuration)
|
||||
|
||||
const { startTime } = this.config
|
||||
if (startTime) {
|
||||
if (!this._switchUrlOpts?.seamless) {
|
||||
this.media.currentTime = startTime
|
||||
}
|
||||
|
||||
this._playlist.setNextSegmentByIndex(this._playlist.findSegmentIndexByTime(startTime) || 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this._loadSegment()
|
||||
}
|
||||
|
||||
async replay (isPlayEmit) {
|
||||
this.config.startTime = 0
|
||||
this.config.softDecode ? this.load() : (await this.load())
|
||||
@ -115,22 +188,69 @@ export class Hls extends EventEmitter {
|
||||
return this.media.play(!isPlayEmit)
|
||||
}
|
||||
|
||||
async switchURL (url, startTime = 0) {
|
||||
this.config.startTime = startTime
|
||||
this.config.url = url
|
||||
let appended
|
||||
try {
|
||||
appended = this.config.softDecode ? this.load(url) : (await this.load(url))
|
||||
} catch (error) {
|
||||
this.emit(Event.SWITCH_URL_FAILED, error)
|
||||
throw error
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {?SwitchUrlOptions} options
|
||||
* @returns
|
||||
*/
|
||||
async switchURL (url, options = {}) {
|
||||
const defaultOpts = {
|
||||
seamless: false,
|
||||
startTime: 0,
|
||||
bitrate: 0
|
||||
}
|
||||
switch (typeof options) {
|
||||
case 'number':
|
||||
options = {startTime: options}
|
||||
break
|
||||
case 'boolean':
|
||||
options = {seamless: options}
|
||||
break
|
||||
case 'object':
|
||||
for (const key in options) {
|
||||
if (options[key] === undefined || options[key] === null) {
|
||||
delete options[key]
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw `unsupported switchURL args: ${options}`
|
||||
}
|
||||
this._reloadOnPlay = false
|
||||
|
||||
if (appended) {
|
||||
this.emit(Event.SWITCH_URL_SUCCESS, { url })
|
||||
options = Object.assign({}, defaultOpts, options)
|
||||
|
||||
const { seamless, startTime } = options
|
||||
this.config.url = url
|
||||
this.config.startTime = startTime
|
||||
this._switchUrlOpts = options
|
||||
|
||||
if (!seamless) {
|
||||
let appended
|
||||
try {
|
||||
appended = this.config.softDecode ? this.load(url) : (await this.load(url))
|
||||
} catch (error) {
|
||||
this.emit(Event.SWITCH_URL_FAILED, error)
|
||||
throw error
|
||||
}
|
||||
this._reloadOnPlay = false
|
||||
|
||||
if (appended) {
|
||||
this.emit(Event.SWITCH_URL_SUCCESS, { url })
|
||||
}
|
||||
return this.media.play(true)
|
||||
} else {
|
||||
this._urlSwitching = true
|
||||
this._prevSegSn = null
|
||||
this._prevSegCc = null
|
||||
|
||||
this._playlist.reset()
|
||||
this._bufferService.seamlessSwitch()
|
||||
await this._clear()
|
||||
await this._loadData(url)
|
||||
this._startTick()
|
||||
}
|
||||
return this.media.play(true)
|
||||
|
||||
this._switchUrlOpts = null
|
||||
}
|
||||
|
||||
async switchStream (id, force = true) {
|
||||
@ -147,6 +267,7 @@ export class Hls extends EventEmitter {
|
||||
throw this._emitError(StreamingError.create(error))
|
||||
}
|
||||
|
||||
// 同步更新
|
||||
if (curStream.currentAudioStream && toSwitch.audioStreams.length > 2) {
|
||||
const curId = curStream.currentAudioStream.id
|
||||
toSwitch.currentAudioStream = toSwitch.audioStreams.find(x => x.id === curId) || toSwitch.currentAudioStream
|
||||
@ -199,6 +320,12 @@ export class Hls extends EventEmitter {
|
||||
return toSwitch
|
||||
}
|
||||
|
||||
async switchSubtitleStream (lang) {
|
||||
this._playlist.switchSubtitle(lang)
|
||||
await this._manifestLoader.stopPoll()
|
||||
await this._refreshM3U8()
|
||||
}
|
||||
|
||||
async destroy () {
|
||||
if (!this.media) return
|
||||
this.removeAllListeners()
|
||||
@ -213,6 +340,10 @@ export class Hls extends EventEmitter {
|
||||
this.media = null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {('video'|'audio')?} mediaType
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
static isSupported (mediaType) {
|
||||
if (!mediaType || mediaType === 'video' || mediaType === 'audio') {
|
||||
return MSE.isSupported()
|
||||
@ -234,7 +365,7 @@ export class Hls extends EventEmitter {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_loadM3U8 = async (url) => {
|
||||
async _loadM3U8 (url) {
|
||||
let playlist
|
||||
try {
|
||||
[playlist] = await this._manifestLoader.load(url)
|
||||
@ -243,35 +374,33 @@ export class Hls extends EventEmitter {
|
||||
}
|
||||
if (!playlist) return
|
||||
this._playlist.upsertPlaylist(playlist)
|
||||
|
||||
if (playlist.isMaster) {
|
||||
if (this._playlist.currentStream.subtitleStreams?.length) {
|
||||
this.emit(Event.SUBTITLE_PLAYLIST, {
|
||||
list: this._playlist.currentStream.subtitleStreams
|
||||
})
|
||||
}
|
||||
await this._refreshM3U8()
|
||||
}
|
||||
this.emit(Event.STREAM_PARSED)
|
||||
if (this.isLive) {
|
||||
this._bufferService.setLiveSeekableRange(0, 0xffffffff)
|
||||
if (!playlist.isMaster) this._pollM3U8(url)
|
||||
} else {
|
||||
await this._bufferService.updateDuration(this._playlist.currentStream.totalDuration)
|
||||
const { startTime } = this.config
|
||||
if (startTime) {
|
||||
this.media.currentTime = startTime
|
||||
this._playlist.setNextSegmentByIndex(this._playlist.findSegmentIndexByTime(startTime) || 0)
|
||||
}
|
||||
}
|
||||
return playlist
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @private 首次更新 master playlist 的 media level
|
||||
*/
|
||||
_refreshM3U8 () {
|
||||
const stream = this._playlist.currentStream
|
||||
if (!stream || !stream.url) throw this._emitError(StreamingError.create(null, null, new Error('m3u8 url is not defined')))
|
||||
const url = stream.url
|
||||
const audioUrl = stream.currentAudioStream?.url
|
||||
return this._manifestLoader.load(url, audioUrl).then(([mediaPlaylist, audioPlaylist]) => {
|
||||
const subtitleUrl = stream.currentSubtitleStream?.url
|
||||
return this._manifestLoader.load(url, audioUrl, subtitleUrl).then(([mediaPlaylist, audioPlaylist, subtitlePlaylist]) => {
|
||||
if (!mediaPlaylist) return
|
||||
this._playlist.upsertPlaylist(mediaPlaylist, audioPlaylist)
|
||||
if (this.isLive) this._pollM3U8(url, audioUrl)
|
||||
this._playlist.upsertPlaylist(mediaPlaylist, audioPlaylist, subtitlePlaylist)
|
||||
if (!this.isLive) return
|
||||
this._pollM3U8(url, audioUrl, subtitleUrl)
|
||||
}).catch(err => {
|
||||
throw this._emitError(StreamingError.create(err))
|
||||
})
|
||||
@ -280,13 +409,15 @@ export class Hls extends EventEmitter {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_pollM3U8 (url, audioUrl) {
|
||||
_pollM3U8 (url, audioUrl, subtitleUrl) {
|
||||
let isEmpty = this._playlist.isEmpty
|
||||
this._manifestLoader.poll(
|
||||
url,
|
||||
audioUrl,
|
||||
(p1, p2) => {
|
||||
this._playlist.upsertPlaylist(p1, p2)
|
||||
subtitleUrl,
|
||||
(p1, p2, p3) => {
|
||||
this._playlist.upsertPlaylist(p1, p2, p3)
|
||||
this._playlist.clearOldSegment()
|
||||
if (p1 && isEmpty && !this._playlist.isEmpty) {
|
||||
this._loadSegment()
|
||||
}
|
||||
@ -294,6 +425,7 @@ export class Hls extends EventEmitter {
|
||||
}, (err) => {
|
||||
this._emitError(StreamingError.create(err))
|
||||
},
|
||||
// 刷新时间
|
||||
(this._playlist.lastSegment?.duration || 0) * 1000)
|
||||
}
|
||||
|
||||
@ -309,6 +441,7 @@ export class Hls extends EventEmitter {
|
||||
return this._loadSegmentDirect()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@ -334,6 +467,11 @@ export class Hls extends EventEmitter {
|
||||
}
|
||||
|
||||
if (appended) {
|
||||
if (this._urlSwitching) {
|
||||
this._urlSwitching = false
|
||||
this.emit(Event.SWITCH_URL_SUCCESS, { url: this.config.url })
|
||||
}
|
||||
|
||||
this._playlist.moveSegmentPointer()
|
||||
if (seg.isLast) {
|
||||
this._end()
|
||||
@ -461,21 +599,21 @@ export class Hls extends EventEmitter {
|
||||
if (!liveEdge) return
|
||||
const latency = liveEdge - this.media.currentTime
|
||||
if (latency >= cfg.maxLatency) {
|
||||
logger.debug('latency jump', latency)
|
||||
logger.debug(`latency jump, currentTime:${this.media.currentTime}, liveEdge:${liveEdge}, latency=${latency}`)
|
||||
this.media.currentTime = liveEdge - cfg.targetLatency
|
||||
}
|
||||
}
|
||||
|
||||
this._seiService.throw(this.media.currentTime)
|
||||
|
||||
if (this.config.allowedStreamTrackChange) {
|
||||
if (this.config.allowedStreamTrackChange && !this.config.softDecode) {
|
||||
this._checkStreamTrackChange(this.media.currentTime)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_checkStreamTrackChange (time) {
|
||||
const changedSeg = this._playlist.checkSegmentTrackChange(time)
|
||||
const changedSeg = this._playlist.checkSegmentTrackChange(time, this._bufferService.nbSb)
|
||||
if (!changedSeg) return
|
||||
this.switchURL(this.config.url, changedSeg.start + 0.2)
|
||||
}
|
||||
@ -500,6 +638,7 @@ export class Hls extends EventEmitter {
|
||||
this._reloadOnPlay = false
|
||||
this._prevSegSn = null
|
||||
this._prevSegCc = null
|
||||
this._switchUrlOpts = null
|
||||
this._playlist.reset()
|
||||
this._segmentLoader.reset()
|
||||
this._seiService.reset()
|
||||
@ -586,12 +725,38 @@ export class Hls extends EventEmitter {
|
||||
this.media.pause()
|
||||
}
|
||||
this._stopTick()
|
||||
if (this._urlSwitching) {
|
||||
this._urlSwitching = false
|
||||
this.emit(Event.SWITCH_URL_FAILED, error)
|
||||
}
|
||||
this.emit(Event.ERROR, error)
|
||||
if (endOfStream) this._end()
|
||||
this._seiService.reset()
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getSeamlessSwitchPoint () {
|
||||
const { media } = this
|
||||
let nextLoadPoint = media.currentTime
|
||||
if (!media.paused) {
|
||||
const segIdx = this._playlist.findSegmentIndexByTime(media.currentTime)
|
||||
const curSeg = this._playlist.getSegmentByIndex(segIdx)
|
||||
const latestKbps = this._stats?.getStats().downloadSpeed // latest download speed
|
||||
if (latestKbps && curSeg) {
|
||||
const delay = (curSeg.duration * this._playlist.currentStream.bitrate) / latestKbps + 1
|
||||
|
||||
nextLoadPoint += delay
|
||||
} else {
|
||||
nextLoadPoint += 5
|
||||
}
|
||||
}
|
||||
|
||||
return nextLoadPoint
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -24,22 +24,45 @@ export class ManifestLoader {
|
||||
timeout: loadTimeout,
|
||||
onRetryError: this._onLoaderRetry
|
||||
})
|
||||
|
||||
this._subtitleLoader = new NetLoader({
|
||||
...fetchOptions,
|
||||
responseType: 'text',
|
||||
retry: retryCount,
|
||||
retryDelay: retryDelay,
|
||||
timeout: loadTimeout,
|
||||
onRetryError: this._onLoaderRetry
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
async load (url, audioUrl) {
|
||||
async load (url, audioUrl, subtitleUrl) {
|
||||
const toLoad = [this._loader.load(url)]
|
||||
if (audioUrl) {
|
||||
toLoad.push(this._audioLoader.load(audioUrl))
|
||||
}
|
||||
|
||||
if (subtitleUrl) {
|
||||
toLoad.push(this._subtitleLoader.load(subtitleUrl))
|
||||
}
|
||||
|
||||
let videoText
|
||||
let audioText
|
||||
let subtitleText
|
||||
|
||||
try {
|
||||
const [video, audio] = await Promise.all(toLoad)
|
||||
const [video, audio, subtitle] = await Promise.all(toLoad)
|
||||
if (!video) return []
|
||||
|
||||
videoText = video.data
|
||||
audioText = audio?.data
|
||||
|
||||
if (audioUrl) {
|
||||
audioText = audio?.data
|
||||
subtitleText = subtitle?.data
|
||||
} else {
|
||||
subtitleText = audio?.data
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw StreamingError.network(error)
|
||||
}
|
||||
@ -48,10 +71,12 @@ export class ManifestLoader {
|
||||
|
||||
let playlist
|
||||
let audioPlaylist
|
||||
let subtitlePlaylist
|
||||
try {
|
||||
if (onPreM3U8Parse) {
|
||||
videoText = onPreM3U8Parse(videoText) || videoText
|
||||
if (audioText) audioText = onPreM3U8Parse(audioText, true) || audioText
|
||||
if (subtitleText) subtitleText = onPreM3U8Parse(subtitleText, true) || subtitleText
|
||||
}
|
||||
playlist = M3U8Parser.parse(videoText, url)
|
||||
if (playlist?.live === false && playlist.segments && !playlist.segments.length) {
|
||||
@ -60,6 +85,10 @@ export class ManifestLoader {
|
||||
if (audioText) {
|
||||
audioPlaylist = M3U8Parser.parse(audioText, audioUrl)
|
||||
}
|
||||
if (subtitleText) {
|
||||
subtitlePlaylist = M3U8Parser.parse(subtitleText, subtitleUrl)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new StreamingError(ERR.MANIFEST, ERR.SUB_TYPES.HLS, error)
|
||||
}
|
||||
@ -71,20 +100,25 @@ export class ManifestLoader {
|
||||
}
|
||||
}
|
||||
|
||||
return [playlist, audioPlaylist]
|
||||
return [playlist, audioPlaylist, subtitlePlaylist]
|
||||
}
|
||||
|
||||
poll (url, audioUrl, cb, errorCb, time) {
|
||||
poll (url, audioUrl, subtitleUrl, cb, errorCb, time) {
|
||||
clearTimeout(this._timer)
|
||||
time = time || 3000
|
||||
let retryCount = this.hls.config.pollRetryCount
|
||||
const fn = async () => {
|
||||
clearTimeout(this._timer)
|
||||
try {
|
||||
const res = await this.load(url, audioUrl)
|
||||
const res = await this.load(url, audioUrl, subtitleUrl)
|
||||
if (!res[0]) return
|
||||
cb(res[0], res[1])
|
||||
retryCount = this.hls.config.pollRetryCount
|
||||
cb(res[0], res[1], res[2])
|
||||
} catch (e) {
|
||||
errorCb(e)
|
||||
retryCount--
|
||||
if (retryCount <= 0) {
|
||||
errorCb(e)
|
||||
}
|
||||
}
|
||||
this._timer = setTimeout(fn, time)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MasterPlaylist, MasterStream, AudioStream } from './model'
|
||||
import { MasterPlaylist, MasterStream, AudioStream, SubTitleStream, MediaStream } from './model'
|
||||
import { parseAttr, parseTag, getAbsoluteUrl, getCodecs } from './utils'
|
||||
|
||||
/**
|
||||
@ -11,6 +11,7 @@ export function parseMasterPlaylist (lines, parentUrl) {
|
||||
let index = 0
|
||||
let line
|
||||
const audioStreams = []
|
||||
const subtitleStreams = []
|
||||
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (line = lines[index++]) {
|
||||
@ -21,19 +22,37 @@ export function parseMasterPlaylist (lines, parentUrl) {
|
||||
master.version = parseInt(data)
|
||||
} else if (name === 'MEDIA' && data) {
|
||||
const attr = parseAttr(data)
|
||||
let stream
|
||||
switch (attr.TYPE) {
|
||||
case 'AUDIO':
|
||||
stream = new AudioStream()
|
||||
break
|
||||
case 'SUBTITLES':
|
||||
stream = new SubTitleStream()
|
||||
break
|
||||
default:
|
||||
stream = new MediaStream()
|
||||
}
|
||||
|
||||
stream.url = getAbsoluteUrl(attr.URI, parentUrl)
|
||||
stream.default = attr.DEFAULT === 'YES'
|
||||
stream.autoSelect = attr.AUTOSELECT === 'YES'
|
||||
stream.group = attr['GROUP-ID']
|
||||
stream.name = attr.NAME
|
||||
stream.lang = attr.LANGUAGE
|
||||
if (attr.CHANNELS) {
|
||||
stream.channels = Number(attr.CHANNELS.split('/')[0])
|
||||
if (Number.isNaN(stream.channels)) stream.channels = 0
|
||||
}
|
||||
|
||||
if (attr.TYPE === 'AUDIO' && attr.URI) {
|
||||
const stream = new AudioStream()
|
||||
stream.url = getAbsoluteUrl(attr.URI, parentUrl)
|
||||
stream.default = attr.DEFAULT === 'YES'
|
||||
stream.group = attr['GROUP-ID']
|
||||
stream.name = attr.NAME
|
||||
stream.lang = attr.LANGUAGE
|
||||
if (attr.CHANNELS) {
|
||||
stream.channels = Number(attr.CHANNELS.split('/')[0])
|
||||
if (Number.isNaN(stream.channels)) stream.channels = 0
|
||||
}
|
||||
audioStreams.push(stream)
|
||||
}
|
||||
|
||||
if (attr.TYPE === 'SUBTITLES') {
|
||||
subtitleStreams.push(stream)
|
||||
}
|
||||
|
||||
} else if (name === 'STREAM-INF' && data) {
|
||||
const stream = new MasterStream()
|
||||
const attr = parseAttr(data)
|
||||
@ -53,12 +72,13 @@ export function parseMasterPlaylist (lines, parentUrl) {
|
||||
stream.textCodec = getCodecs('text', codecs)
|
||||
}
|
||||
stream.audioGroup = attr.AUDIO
|
||||
stream.subtitleGroup = attr.SUBTITLES
|
||||
|
||||
master.streams.push(stream)
|
||||
}
|
||||
}
|
||||
|
||||
master.streams.forEach((s, i) => { s.id = i })
|
||||
|
||||
if (audioStreams.length) {
|
||||
audioStreams.forEach((s, i) => { s.id = i })
|
||||
master.streams.forEach((stream) => {
|
||||
@ -68,5 +88,14 @@ export function parseMasterPlaylist (lines, parentUrl) {
|
||||
})
|
||||
}
|
||||
|
||||
if (subtitleStreams.length) {
|
||||
subtitleStreams.forEach((s, i) => { s.id = i })
|
||||
master.streams.forEach((stream) => {
|
||||
if (stream.subtitleGroup) {
|
||||
stream.subtitleStreams = subtitleStreams.filter(x => x.group === stream.subtitleGroup)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return master
|
||||
}
|
||||
|
@ -7,15 +7,42 @@ export class MasterPlaylist {
|
||||
isMaster = true
|
||||
}
|
||||
|
||||
export class AudioStream {
|
||||
|
||||
const MediaType = {
|
||||
Audio: 'AUDIO',
|
||||
Video: 'VIDEO',
|
||||
SubTitle: 'SUBTITLE',
|
||||
ClosedCaptions: 'CLOSED-CAPTIONS'
|
||||
}
|
||||
|
||||
export class MediaStream {
|
||||
id = 0
|
||||
url = ''
|
||||
default = false
|
||||
autoSelect = false
|
||||
forced = false
|
||||
group = ''
|
||||
name = ''
|
||||
lang = ''
|
||||
channels = 0
|
||||
segments = []
|
||||
endSN = 0
|
||||
}
|
||||
|
||||
export class AudioStream extends MediaStream {
|
||||
mediaType = MediaType.Audio
|
||||
channels = 0
|
||||
}
|
||||
|
||||
export class VideoStream extends MediaStream {
|
||||
mediaType = MediaType.Video
|
||||
}
|
||||
|
||||
export class SubTitleStream extends MediaStream {
|
||||
mediaType = MediaType.SubTitle
|
||||
}
|
||||
|
||||
export class ClosedCaptionsStream extends MediaStream {
|
||||
mediaType = MediaType.ClosedCaptions
|
||||
}
|
||||
|
||||
export class MasterStream {
|
||||
@ -32,6 +59,12 @@ export class MasterStream {
|
||||
|
||||
/** @type {AudioStream[]} */
|
||||
audioStreams = []
|
||||
|
||||
/** @type {SubTitleStream[]} */
|
||||
subtitleStreams = []
|
||||
|
||||
/** @type {ClosedCaptionsStream[]} */
|
||||
closedCaptionsStream = []
|
||||
}
|
||||
|
||||
export class MediaPlaylist {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { clamp } from '../utils'
|
||||
import { Stream } from './stream'
|
||||
import { Event } from '../constants'
|
||||
|
||||
export class Playlist {
|
||||
/** @type {import('./stream').Stream[]} */
|
||||
@ -32,10 +33,18 @@ export class Playlist {
|
||||
return this.currentStream?.segments
|
||||
}
|
||||
|
||||
get currentSubtitleEndSn () {
|
||||
return this.currentStream?.currentSubtitleEndSn
|
||||
}
|
||||
|
||||
get liveEdge () {
|
||||
return this.currentStream?.liveEdge
|
||||
}
|
||||
|
||||
get totalDuration () {
|
||||
return this.currentStream?.totalDuration || 0
|
||||
}
|
||||
|
||||
get seekRange () {
|
||||
const segments = this.currentSegments
|
||||
if (!segments || !segments.length) return
|
||||
@ -53,6 +62,10 @@ export class Playlist {
|
||||
return this.currentStream?.live
|
||||
}
|
||||
|
||||
get hasSubtitle () {
|
||||
return !!this.currentStream?.currentSubtitleStream
|
||||
}
|
||||
|
||||
getAudioSegment (seg) {
|
||||
return this.currentStream?.getAudioSegment(seg)
|
||||
}
|
||||
@ -92,7 +105,7 @@ export class Playlist {
|
||||
}
|
||||
}
|
||||
|
||||
upsertPlaylist (playlist, audioPlaylist) {
|
||||
upsertPlaylist (playlist, audioPlaylist, subtitlePlaylist) {
|
||||
if (!playlist) return
|
||||
if (playlist.isMaster) {
|
||||
this.streams.length = playlist.streams.length
|
||||
@ -104,13 +117,20 @@ export class Playlist {
|
||||
}
|
||||
})
|
||||
this.currentStream = this.streams[0]
|
||||
// update media
|
||||
} else if (Array.isArray(playlist.segments)) {
|
||||
const stream = this.currentStream
|
||||
if (stream) {
|
||||
stream.update(playlist, audioPlaylist)
|
||||
stream.update(playlist, audioPlaylist, subtitlePlaylist)
|
||||
const newSubtitleSegs = stream.updateSubtitle(subtitlePlaylist)
|
||||
if (newSubtitleSegs) {
|
||||
this.hls.emit(Event.SUBTITLE_SEGMENTS, {
|
||||
list: newSubtitleSegs
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.reset()
|
||||
this.currentStream = this.streams[0] = new Stream(playlist, audioPlaylist)
|
||||
this.currentStream = this.streams[0] = new Stream(playlist, audioPlaylist, subtitlePlaylist)
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,6 +146,10 @@ export class Playlist {
|
||||
}
|
||||
}
|
||||
|
||||
switchSubtitle (lang) {
|
||||
this.currentStream?.switchSubtitle(lang)
|
||||
}
|
||||
|
||||
clearOldSegment (maxPlaylistSize = 50) {
|
||||
const stream = this.currentStream
|
||||
if (!this.dvrWindow || !stream) return
|
||||
@ -137,7 +161,7 @@ export class Playlist {
|
||||
this._segmentPointer = stream.clearOldSegment(startTime, this._segmentPointer)
|
||||
}
|
||||
|
||||
checkSegmentTrackChange (cTime) {
|
||||
checkSegmentTrackChange (cTime, nbSb) {
|
||||
const index = this.findSegmentIndexByTime(cTime)
|
||||
const seg = this.getSegmentByIndex(index)
|
||||
|
||||
@ -145,6 +169,10 @@ export class Playlist {
|
||||
|
||||
if (!seg.hasAudio && !seg.hasVideo) return
|
||||
|
||||
// when seek
|
||||
if (nbSb !== 2 && seg.hasAudio && seg.hasVideo) return seg
|
||||
|
||||
// continuous play
|
||||
if (seg.end - cTime > 0.3) return
|
||||
|
||||
const next = this.getSegmentByIndex(index + 1)
|
||||
|
@ -24,9 +24,18 @@ export class Stream {
|
||||
/** @type {import('../../parser/model').AudioStream[]} */
|
||||
audioStreams = []
|
||||
|
||||
/** @type {import('../../parser/model').SubTitleStream[]} */
|
||||
subtitleStreams = []
|
||||
|
||||
/** @type {import('../../parser/model').ClosedCaptionsStream[]} */
|
||||
closedCaptions = []
|
||||
|
||||
/** @type {import('../../parser/model').AudioStream | null} */
|
||||
currentAudioStream = null
|
||||
|
||||
/** @type {import('../../parser/model').subtitleStreams | null} */
|
||||
currentSubtitleStream = null
|
||||
|
||||
/**
|
||||
* asdasd {@link AudioStream}
|
||||
*/
|
||||
@ -49,16 +58,20 @@ export class Stream {
|
||||
return this.lastSegment?.end || 0
|
||||
}
|
||||
|
||||
constructor (playlist, audioPlaylist) {
|
||||
this.update(playlist, audioPlaylist)
|
||||
get currentSubtitleEndSn () {
|
||||
return this.currentSubtitleStream?.endSN || 0
|
||||
}
|
||||
|
||||
constructor (playlist, audioPlaylist, subtitlePlaylist) {
|
||||
this.update(playlist, audioPlaylist, subtitlePlaylist)
|
||||
}
|
||||
|
||||
clearOldSegment (startTime, pointer) {
|
||||
if (this.currentAudioStream) {
|
||||
this._clearSegments(this.currentAudioStream, startTime, pointer)
|
||||
this._clearSegments(startTime, pointer)
|
||||
}
|
||||
|
||||
return this._clearSegments(this, startTime, pointer)
|
||||
return this._clearSegments(startTime, pointer)
|
||||
}
|
||||
|
||||
getAudioSegment (seg) {
|
||||
@ -87,6 +100,7 @@ export class Stream {
|
||||
this.snDiff = playlist.segments[0].sn - audioPlaylist.segments[0].sn
|
||||
}
|
||||
}
|
||||
|
||||
} else { // master stream
|
||||
this.id = playlist.id
|
||||
this.bitrate = playlist.bitrate
|
||||
@ -97,10 +111,47 @@ export class Stream {
|
||||
this.videoCodec = playlist.videoCodec
|
||||
this.textCodec = playlist.textCodec
|
||||
this.audioStreams = playlist.audioStreams
|
||||
|
||||
this.subtitleStreams = playlist.subtitleStreams
|
||||
if (!this.currentAudioStream && this.audioStreams.length) {
|
||||
this.currentAudioStream = this.audioStreams.find(x => x.default) || this.audioStreams[0]
|
||||
}
|
||||
|
||||
if (!this.currentSubtitleStream && this.subtitleStreams.length) {
|
||||
this.currentSubtitleStream = this.subtitleStreams.find(x => x.default) || this.subtitleStreams[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSubtitle (subtitlePlaylist) {
|
||||
if (!(subtitlePlaylist && this.currentSubtitleStream && Array.isArray(subtitlePlaylist.segments))) return
|
||||
|
||||
const newSegs = this._updateSegments(subtitlePlaylist, this.currentSubtitleStream)
|
||||
const segs = this.currentSubtitleStream.segments
|
||||
if (segs.length > 100 ) {
|
||||
this.currentSubtitleStream.segments = segs.slice(100)
|
||||
}
|
||||
|
||||
if (!newSegs) return
|
||||
|
||||
return newSegs.map(x => {
|
||||
return {
|
||||
sn: x.sn,
|
||||
url: x.url,
|
||||
duration: x.duration,
|
||||
start: x.start,
|
||||
end: x.end,
|
||||
lang: this.currentSubtitleStream.lang
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
switchSubtitle (lang) {
|
||||
const toSwitch = this.subtitleStreams.find(x => x.lang === lang)
|
||||
const origin = this.currentSubtitleStream
|
||||
if (toSwitch) {
|
||||
this.currentSubtitleStream = toSwitch
|
||||
origin.segments = []
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,11 +200,14 @@ export class Stream {
|
||||
toAppend.forEach(seg => (seg.cc += lastCC))
|
||||
}
|
||||
}
|
||||
|
||||
segObj.endSN = playlist.endSN
|
||||
segObj.segments = segments.concat(toAppend)
|
||||
|
||||
return toAppend
|
||||
}
|
||||
} else {
|
||||
segObj.segments = playlist.segments
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
/* c8 ignore next 12 */
|
||||
import { HlsPlugin } from './plugin'
|
||||
import { HlsPlugin, parseSwitchUrlArgs } from './plugin'
|
||||
|
||||
/**
|
||||
* @typedef { import ('./hls/buffer-service/decrypt/index').IExternalDecryptor } IExternalDecryptor
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef { import ('./hls').SwitchUrlOptions } SwitchUrlOptions
|
||||
*/
|
||||
|
||||
export {
|
||||
ERR,
|
||||
ERR_CODE,
|
||||
@ -12,6 +16,6 @@ export {
|
||||
} from 'xgplayer-streaming-shared'
|
||||
export * from './hls'
|
||||
export { Event as EVENT } from './hls/constants'
|
||||
export { HlsPlugin }
|
||||
export { HlsPlugin, parseSwitchUrlArgs }
|
||||
|
||||
export default HlsPlugin
|
||||
|
@ -4,6 +4,26 @@ import { Hls } from './hls'
|
||||
import { Event } from './hls/constants'
|
||||
import PluginExtension from './plugin-extension'
|
||||
|
||||
export function parseSwitchUrlArgs (args, plugin) {
|
||||
const { player } = plugin
|
||||
const curTime = player.currentTime
|
||||
const options = {
|
||||
startTime: curTime
|
||||
}
|
||||
|
||||
switch (typeof args) {
|
||||
case 'boolean':
|
||||
options.seamless = args
|
||||
break
|
||||
case 'object':
|
||||
Object.assign(options, args)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
export class HlsPlugin extends BasePlugin {
|
||||
static Hls = Hls
|
||||
|
||||
@ -57,7 +77,7 @@ export class HlsPlugin extends BasePlugin {
|
||||
this.hls = new Hls({
|
||||
softDecode: this.softDecode,
|
||||
isLive: config.isLive,
|
||||
media: this.player.video,
|
||||
media: this.player.media || this.player.video,
|
||||
startTime: config.startTime,
|
||||
url: config.url,
|
||||
...hlsOpts
|
||||
@ -70,9 +90,13 @@ export class HlsPlugin extends BasePlugin {
|
||||
configurable: true
|
||||
}
|
||||
})
|
||||
this.hls.on('sourceAttached', () => {
|
||||
if (this.hls.media?.src) {
|
||||
this.hls.on('sourceAttached', () => {
|
||||
_resolve(true)
|
||||
})
|
||||
} else {
|
||||
_resolve(true)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
_resolve(true)
|
||||
}
|
||||
@ -91,8 +115,8 @@ export class HlsPlugin extends BasePlugin {
|
||||
this.player?.useHooks('replay', () => this.hls?.replay())
|
||||
}
|
||||
|
||||
this.on(Events.SWITCH_SUBTITLE || 'switch_subtitle', this._onSwitchSubtitle)
|
||||
this.on(Events.URL_CHANGE, this._onSwitchURL)
|
||||
// this.on(Events.DEFINITION_CHANGE, this._onDefinitionChange)
|
||||
this.on(Events.DESTROY, this.destroy)
|
||||
|
||||
this._transError()
|
||||
@ -110,12 +134,14 @@ export class HlsPlugin extends BasePlugin {
|
||||
this._transCoreEvent(EVENT.SEI_IN_TIME)
|
||||
this._transCoreEvent(EVENT.SPEED)
|
||||
this._transCoreEvent(EVENT.HLS_MANIFEST_LOADED)
|
||||
this._transCoreEvent(Event.STREAM_PARSED)
|
||||
this._transCoreEvent(Event.NO_AUDIO_TRACK)
|
||||
this._transCoreEvent(EVENT.HLS_LEVEL_LOADED)
|
||||
this._transCoreEvent(EVENT.STREAM_EXCEPTION)
|
||||
this._transCoreEvent(EVENT.SWITCH_URL_SUCCESS)
|
||||
this._transCoreEvent(EVENT.SWITCH_URL_FAILED)
|
||||
this._transCoreEvent(Event.NO_AUDIO_TRACK)
|
||||
this._transCoreEvent(Event.STREAM_PARSED)
|
||||
this._transCoreEvent(Event.SUBTITLE_SEGMENTS)
|
||||
this._transCoreEvent(Event.SUBTITLE_PLAYLIST)
|
||||
|
||||
if (config.url) {
|
||||
this.hls.load(config.url, true).catch(e => {})
|
||||
@ -124,8 +150,11 @@ export class HlsPlugin extends BasePlugin {
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('./hls').Stats | undefined}
|
||||
*/
|
||||
getStats = () => {
|
||||
return this.hls?.getStats() || {}
|
||||
return this.hls?.getStats()
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
@ -138,22 +167,28 @@ export class HlsPlugin extends BasePlugin {
|
||||
this.pluginExtension = null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | boolean} [mediaType]
|
||||
* @param {string} [codec]
|
||||
* @returns {boolean}
|
||||
* - mediaType: 默认检测 MSE 对 H264 codec是否支持,传入 true 或者配置参数的mediaType的取值检测 WebAssembly是否支持
|
||||
* - codec: 暂无使用
|
||||
*/
|
||||
static isSupported (mediaType, codec) {
|
||||
return Hls.isSupported(mediaType, codec)
|
||||
}
|
||||
|
||||
_onSwitchURL = (url) => {
|
||||
const { player, hls } = this
|
||||
if (hls) {
|
||||
const curTime = player.currentTime
|
||||
|
||||
player.config.url = url
|
||||
hls.switchURL(url, curTime).catch(e => {})
|
||||
}
|
||||
_onSwitchSubtitle = ({lang}) => {
|
||||
this.hls?.switchSubtitleStream(lang)
|
||||
}
|
||||
|
||||
_onDefinitionChange = ({ to }) => {
|
||||
if (this.hls) this.hls.switchURL(to).catch(e => {})
|
||||
_onSwitchURL = (url, args) => {
|
||||
const { player, hls } = this
|
||||
if (hls) {
|
||||
const options = parseSwitchUrlArgs(args, this)
|
||||
player.config.url = url
|
||||
hls.switchURL(url, options).catch(e => {})
|
||||
}
|
||||
}
|
||||
|
||||
_transError () {
|
||||
@ -171,7 +206,23 @@ export class HlsPlugin extends BasePlugin {
|
||||
...e,
|
||||
eventName
|
||||
})
|
||||
|
||||
if (eventName === EVENT.SEI_IN_TIME && this.hls.hasSubtitle) {
|
||||
this._emitSeiPaylodTime(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_emitSeiPaylodTime (e) {
|
||||
try {
|
||||
const seiJson = JSON.parse(Array.from(e.data.payload).map(x=>String.fromCharCode(x)).join('').slice(0,-1))
|
||||
if (!seiJson['rtmp_dts']) return
|
||||
this.player.emit('core_event', {
|
||||
eventName: Event.SEI_PAYLOAD_TIME,
|
||||
time: seiJson['rtmp_dts']
|
||||
})
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "xgplayer-m4a",
|
||||
"version": "1.1.5",
|
||||
"description": "xgplayer plugin for m4a transform to fmp4",
|
||||
"main": "dist/index.min.js",
|
||||
"module": "es/index.js",
|
||||
"typings": "es/index.d.ts",
|
||||
"libd": {
|
||||
"umdName": "M4aPlayer"
|
||||
},
|
||||
"sideEffects": [
|
||||
"*.css"
|
||||
],
|
||||
"files": [
|
||||
"dist",
|
||||
"es"
|
||||
],
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"access": "public"
|
||||
},
|
||||
"unpkgFiles": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:bytedance/xgplayer.git"
|
||||
},
|
||||
"keywords": [
|
||||
"mp4",
|
||||
"fmp4",
|
||||
"player",
|
||||
"audio"
|
||||
],
|
||||
"author": "yinguohui@bytedance.com",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"concat-typed-array": "^1.0.2",
|
||||
"deepmerge": "^2.0.1",
|
||||
"event-emitter": "^0.3.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xgplayer": ">=3.0.0-next.0",
|
||||
"core-js": ">=3.12.1"
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { Errors } from 'xgplayer'
|
||||
import { version } from '../package.json'
|
||||
|
||||
class _Errors extends Errors {
|
||||
constructor (type, vid, errd = {}, url = '') {
|
||||
errd.version = version
|
||||
super(type, vid, errd)
|
||||
this.url = url
|
||||
}
|
||||
}
|
||||
|
||||
export default _Errors
|
@ -1,29 +0,0 @@
|
||||
import Concat from 'concat-typed-array'
|
||||
|
||||
class Buffer {
|
||||
constructor () {
|
||||
this.buffer = new Uint8Array(0)
|
||||
}
|
||||
|
||||
write (...buffer) {
|
||||
const self = this
|
||||
buffer.forEach(item => {
|
||||
if (item) {
|
||||
self.buffer = Concat(Uint8Array, self.buffer, item)
|
||||
} else {
|
||||
window.console.error(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static writeUint32 (value) {
|
||||
return new Uint8Array([
|
||||
value >> 24,
|
||||
(value >> 16) & 0xff,
|
||||
(value >> 8) & 0xff,
|
||||
value & 0xff
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
export default Buffer
|
@ -1,655 +0,0 @@
|
||||
import Buffer from './buffer'
|
||||
const UINT32_MAX = Math.pow(2, 32) - 1
|
||||
class FMP4 {
|
||||
static type (name) {
|
||||
return new Uint8Array([name.charCodeAt(0), name.charCodeAt(1), name.charCodeAt(2), name.charCodeAt(3)])
|
||||
}
|
||||
|
||||
static size (value) {
|
||||
return Buffer.writeUint32(value)
|
||||
}
|
||||
|
||||
static extension (version, flag) {
|
||||
return new Uint8Array([
|
||||
version,
|
||||
(flag >> 16) & 0xff,
|
||||
(flag >> 8) & 0xff,
|
||||
flag & 0xff
|
||||
])
|
||||
}
|
||||
|
||||
static ftyp () {
|
||||
const buffer = new Buffer()
|
||||
buffer.write(FMP4.size(28), FMP4.type('ftyp'), new Uint8Array([
|
||||
0x69, 0x73, 0x6F, 0x35, // iso5,
|
||||
0x00, 0x00, 0x00, 0x01, // minor_version: 0x00
|
||||
0x4D, 0x34, 0x41, 0x20, // M4A,
|
||||
0x69, 0x73, 0x6F, 0x35, // iso5,
|
||||
0x64, 0x61, 0x73, 0x68 // dash
|
||||
]))
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static moov (data) {
|
||||
const buffer = new Buffer(); let size = 8
|
||||
|
||||
const mvhd = FMP4.mvhd(data.duration, data.timeScale)
|
||||
const trak = FMP4.audioTrak(data)
|
||||
const mvex = FMP4.mvex(data.duration, data.timeScale)
|
||||
const udtaBuffer = new Buffer()
|
||||
const bytes = new Uint8Array([
|
||||
0x00, 0x00, 0x00, 0x55, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x4D, 0x6D, 0x65, 0x74, 0x61,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
|
||||
0x0C, 0x2D, 0x2D, 0x2D, 0x2D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x2D, 0x2D, 0x2D,
|
||||
0x2D, 0x00, 0x00, 0x00, 0x00
|
||||
])
|
||||
udtaBuffer.write(new Uint8Array(bytes))
|
||||
const udta = udtaBuffer.buffer;
|
||||
[mvhd, trak, mvex, udta].forEach(item => {
|
||||
size += item.byteLength
|
||||
})
|
||||
buffer.write(FMP4.size(size), FMP4.type('moov'), mvhd, mvex, trak, udta)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static mvhd (duration, timeScale) {
|
||||
const buffer = new Buffer()
|
||||
duration *= timeScale
|
||||
duration = 0
|
||||
const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1))
|
||||
const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1))
|
||||
const bytes = new Uint8Array([
|
||||
0x01, // version 1
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // creation_time
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // modification_time
|
||||
(timeScale >> 24) & 0xff,
|
||||
(timeScale >> 16) & 0xff,
|
||||
(timeScale >> 8) & 0xff,
|
||||
timeScale & 0xff, // timeScale
|
||||
(upperWordDuration >> 24),
|
||||
(upperWordDuration >> 16) & 0xff,
|
||||
(upperWordDuration >> 8) & 0xff,
|
||||
upperWordDuration & 0xff,
|
||||
(lowerWordDuration >> 24),
|
||||
(lowerWordDuration >> 16) & 0xff,
|
||||
(lowerWordDuration >> 8) & 0xff,
|
||||
lowerWordDuration & 0xff,
|
||||
0x00, 0x01, 0x00, 0x00, // 1.0 rate
|
||||
0x01, 0x00, // 1.0 volume
|
||||
0x00, 0x00, // reserved
|
||||
0x00, 0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x40, 0x00, 0x00, 0x00, // transformation: unity matrix
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, // pre_defined
|
||||
0xff, 0xff, 0xff, 0xff // next_track_ID
|
||||
])
|
||||
buffer.write(FMP4.size(8 + bytes.length), FMP4.type('mvhd'), new Uint8Array(bytes))
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static audioTrak (data) {
|
||||
const buffer = new Buffer(); let size = 8
|
||||
const tkhd = FMP4.tkhd({
|
||||
id: 1,
|
||||
duration: data.audioDuration,
|
||||
timeScale: data.audioTimeScale,
|
||||
width: 0,
|
||||
height: 0,
|
||||
type: 'audio'
|
||||
})
|
||||
const mdia = FMP4.mdia({
|
||||
type: 'audio',
|
||||
timeScale: data.audioTimeScale,
|
||||
duration: data.audioDuration,
|
||||
channelCount: data.channelCount,
|
||||
sampleRate: data.sampleRate,
|
||||
audioConfig: data.audioConfig
|
||||
})
|
||||
const udtaBuffer = new Buffer()
|
||||
const bytes = new Uint8Array([
|
||||
0x00, 0x00, 0x00, 0x2C,
|
||||
0x6C, 0x75, 0x64, 0x74,
|
||||
0x00, 0x00, 0x00, 0x24,
|
||||
0x74, 0x6C, 0x6F, 0x75,
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
0x48, 0x54, 0x84, 0x23,
|
||||
0x05, 0x01, 0x72, 0x23,
|
||||
0x03, 0x72, 0x13, 0x04,
|
||||
0x7D, 0x13, 0x05, 0x73,
|
||||
0x13, 0x06, 0x04, 0x13
|
||||
])
|
||||
udtaBuffer.write(FMP4.size(52), FMP4.type('udta'), new Uint8Array(bytes))
|
||||
const udta = udtaBuffer.buffer;
|
||||
[tkhd, mdia, udta].forEach(item => {
|
||||
size += item.byteLength
|
||||
})
|
||||
buffer.write(FMP4.size(size), FMP4.type('trak'), tkhd, mdia, udta)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static tkhd (data) {
|
||||
const buffer = new Buffer()
|
||||
const id = data.id
|
||||
let duration = data.duration * data.timeScale
|
||||
duration = 0
|
||||
const width = 0
|
||||
|
||||
const height = 0
|
||||
|
||||
const type = data.type
|
||||
|
||||
const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1))
|
||||
|
||||
const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1))
|
||||
const content = new Uint8Array([
|
||||
0x01, // version 1
|
||||
0x00, 0x00, 0x07, // flags
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // creation_time
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // modification_time
|
||||
(id >> 24) & 0xff,
|
||||
(id >> 16) & 0xff,
|
||||
(id >> 8) & 0xff,
|
||||
id & 0xff, // track_ID
|
||||
0x00, 0x00, 0x00, 0x00, // reserved
|
||||
(upperWordDuration >> 24),
|
||||
(upperWordDuration >> 16) & 0xff,
|
||||
(upperWordDuration >> 8) & 0xff,
|
||||
upperWordDuration & 0xff,
|
||||
(lowerWordDuration >> 24),
|
||||
(lowerWordDuration >> 16) & 0xff,
|
||||
(lowerWordDuration >> 8) & 0xff,
|
||||
lowerWordDuration & 0xff,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x00, // layer
|
||||
0x00, type === 'video' ? 0x01 : 0x00, // alternate_group
|
||||
type === 'audio' ? 0x01 : 0x00, 0x00, // non-audio track volume
|
||||
0x00, 0x00, // reserved
|
||||
0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x40, 0x00, 0x00, 0x00, // transformation: unity matrix
|
||||
(width >> 8) & 0xff,
|
||||
width & 0xff,
|
||||
0x00, 0x00, // width
|
||||
(height >> 8) & 0xff,
|
||||
height & 0xff,
|
||||
0x00, 0x00 // height
|
||||
])
|
||||
buffer.write(FMP4.size(8 + content.byteLength), FMP4.type('tkhd'), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static edts (data) {
|
||||
const buffer = new Buffer(); const duration = data.duration; const mediaTime = data.mediaTime
|
||||
buffer.write(FMP4.size(36), FMP4.type('edts'))
|
||||
// elst
|
||||
buffer.write(FMP4.size(28), FMP4.type('elst'))
|
||||
buffer.write(new Uint8Array([
|
||||
0x00, 0x00, 0x00, 0x01, // entry count
|
||||
(duration >> 24) & 0xff, (duration >> 16) & 0xff, (duration >> 8) & 0xff, duration & 0xff,
|
||||
(mediaTime >> 24) & 0xff, (mediaTime >> 16) & 0xff, (mediaTime >> 8) & 0xff, mediaTime & 0xff,
|
||||
0x00, 0x00, 0x00, 0x01 // media rate
|
||||
]))
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static mdia (data) {
|
||||
const buffer = new Buffer(); let size = 8
|
||||
const mdhd = FMP4.mdhd(data.timeScale)
|
||||
const hdlr = FMP4.hdlr(data.type)
|
||||
const minf = FMP4.minf(data);
|
||||
[mdhd, hdlr, minf].forEach(item => {
|
||||
size += item.byteLength
|
||||
})
|
||||
buffer.write(FMP4.size(size), FMP4.type('mdia'), mdhd, hdlr, minf)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static mdhd (timeScale, duration = 0) {
|
||||
const buffer = new Buffer()
|
||||
duration *= timeScale
|
||||
const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1))
|
||||
const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1))
|
||||
const content = new Uint8Array([
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // creation_time
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // modification_time
|
||||
(timeScale >> 24) & 0xff, (timeScale >> 16) & 0xff, (timeScale >> 8) & 0xff, timeScale & 0xff,
|
||||
(upperWordDuration >> 24),
|
||||
(upperWordDuration >> 16) & 0xff,
|
||||
(upperWordDuration >> 8) & 0xff,
|
||||
upperWordDuration & 0xff,
|
||||
(lowerWordDuration >> 24),
|
||||
(lowerWordDuration >> 16) & 0xff,
|
||||
(lowerWordDuration >> 8) & 0xff,
|
||||
lowerWordDuration & 0xff,
|
||||
0x55, 0xc4, // 'und' language
|
||||
0x00, 0x00
|
||||
])
|
||||
buffer.write(FMP4.size(12 + content.byteLength), FMP4.type('mdhd'), FMP4.extension(1, 0), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static hdlr (type) {
|
||||
const buffer = new Buffer()
|
||||
const value = [0x00, // version 0
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x00, // pre_defined
|
||||
0x76, 0x69, 0x64, 0x65, // handler_type: 'vide'
|
||||
0x00, 0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x00, 0x00, 0x00, // reserved
|
||||
0x56, 0x69, 0x64, 0x65,
|
||||
0x6f, 0x48, 0x61, 0x6e,
|
||||
0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler'
|
||||
]
|
||||
if (type === 'audio') {
|
||||
value.splice(8, 4, ...[0x73, 0x6f, 0x75, 0x6e])
|
||||
value.splice(24, 13, ...[0x53, 0x6f, 0x75, 0x6e,
|
||||
0x64, 0x48, 0x61, 0x6e,
|
||||
0x64, 0x6c, 0x65, 0x72, 0x00])
|
||||
}
|
||||
buffer.write(FMP4.size(8 + value.length), FMP4.type('hdlr'), new Uint8Array(value))
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static minf (data) {
|
||||
const buffer = new Buffer(); let size = 8
|
||||
const vmhd = data.type === 'video' ? FMP4.vmhd() : FMP4.smhd()
|
||||
const dinf = FMP4.dinf()
|
||||
const stbl = FMP4.stbl(data);
|
||||
[vmhd, dinf, stbl].forEach(item => {
|
||||
size += item.byteLength
|
||||
})
|
||||
buffer.write(FMP4.size(size), FMP4.type('minf'), vmhd, dinf, stbl)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static vmhd () {
|
||||
const buffer = new Buffer()
|
||||
buffer.write(FMP4.size(20), FMP4.type('vmhd'), new Uint8Array([
|
||||
0x00, // version
|
||||
0x00, 0x00, 0x01, // flags
|
||||
0x00, 0x00, // graphicsmode
|
||||
0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00 // opcolor
|
||||
]))
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static smhd () {
|
||||
const buffer = new Buffer()
|
||||
buffer.write(FMP4.size(16), FMP4.type('smhd'), new Uint8Array([
|
||||
0x00, // version
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, // balance
|
||||
0x00, 0x00 // reserved
|
||||
]))
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static dinf () {
|
||||
const buffer = new Buffer()
|
||||
const dref = [0x00, // version 0
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x01, // entry_count
|
||||
0x00, 0x00, 0x00, 0x0c, // entry_size
|
||||
0x75, 0x72, 0x6c, 0x20, // 'url' type
|
||||
0x00, // version 0
|
||||
0x00, 0x00, 0x01 // entry_flags
|
||||
]
|
||||
buffer.write(FMP4.size(36), FMP4.type('dinf'), FMP4.size(28), FMP4.type('dref'), new Uint8Array(dref))
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static stbl (data) {
|
||||
const buffer = new Buffer(); let size = 8
|
||||
const stsd = FMP4.stsd(data)
|
||||
const stts = FMP4.stts()
|
||||
const stsc = FMP4.stsc()
|
||||
const stsz = FMP4.stsz()
|
||||
const stco = FMP4.stco();
|
||||
[stsd, stts, stsc, stsz, stco].forEach(item => {
|
||||
size += item.byteLength
|
||||
})
|
||||
buffer.write(FMP4.size(size), FMP4.type('stbl'), stsd, stts, stsc, stsz, stco)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static stsd (data) {
|
||||
const buffer = new Buffer(); let content
|
||||
if (data.type === 'audio') {
|
||||
// if (!data.isAAC && data.codec === 'mp4') {
|
||||
// content = FMP4.mp3(data);
|
||||
// } else {
|
||||
//
|
||||
// }
|
||||
// 支持mp4a
|
||||
content = FMP4.mp4a(data)
|
||||
} else {
|
||||
content = FMP4.avc1(data)
|
||||
}
|
||||
buffer.write(FMP4.size(16 + content.byteLength), FMP4.type('stsd'), FMP4.extension(0, 0), new Uint8Array([0x00, 0x00, 0x00, 0x01]), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static mp4a (data) {
|
||||
const buffer = new Buffer()
|
||||
const content = new Uint8Array([
|
||||
0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x01, // data_reference_index
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, // reserved
|
||||
0x00, data.channelCount, // channelcount
|
||||
0x00, 0x10, // sampleSize:16bits
|
||||
0x00, 0x00, 0x00, 0x00, // reserved2
|
||||
(data.sampleRate >> 8) & 0xff,
|
||||
data.sampleRate & 0xff, //
|
||||
0x00, 0x00
|
||||
])
|
||||
const esds = FMP4.esds(data.audioConfig)
|
||||
buffer.write(FMP4.size(8 + content.byteLength + esds.byteLength), FMP4.type('mp4a'), content, esds)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static esds (config = [43, 146, 8, 0]) {
|
||||
const configlen = config.length
|
||||
const buffer = new Buffer()
|
||||
const content = new Uint8Array([
|
||||
0x00, // version 0
|
||||
0x00, 0x00, 0x00, // flags
|
||||
|
||||
0x03, // descriptor_type
|
||||
0x17 + configlen, // length
|
||||
0x00, 0x00, // es_id
|
||||
0x00, // stream_priority
|
||||
|
||||
0x04, // descriptor_type
|
||||
0x0f + configlen, // length
|
||||
0x40, // codec : mpeg4_audio
|
||||
0x15, // stream_type
|
||||
0x00, 0x00, 0x00, // buffer_size
|
||||
0x00, 0x00, 0x00, 0x00, // maxBitrate
|
||||
0x00, 0x00, 0x00, 0x00, // avgBitrate
|
||||
|
||||
0x05 // descriptor_type
|
||||
].concat([configlen]).concat(config).concat([0x06, 0x01, 0x02]))
|
||||
buffer.write(FMP4.size(8 + content.byteLength), FMP4.type('esds'), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static avc1 (data) {
|
||||
const buffer = new Buffer(); const size = 40// 8(avc1)+8(avcc)+8(btrt)+16(pasp)
|
||||
const sps = data.sps; const pps = data.pps; const width = data.width; const height = data.height; const hSpacing = data.pixelRatio[0]; const vSpacing = data.pixelRatio[1]
|
||||
const avcc = new Uint8Array([
|
||||
0x01, // version
|
||||
sps[1], // profile
|
||||
sps[2], // profile compatible
|
||||
sps[3], // level
|
||||
0xfc | 3,
|
||||
0xE0 | 1 // 目前只处理一个sps
|
||||
].concat([sps.length >>> 8 & 0xff, sps.length & 0xff]).concat(sps).concat(1).concat([pps.length >>> 8 & 0xff, pps.length & 0xff]).concat(pps))
|
||||
const avc1 = new Uint8Array([
|
||||
0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x01, // data_reference_index
|
||||
0x00, 0x00, // pre_defined
|
||||
0x00, 0x00, // reserved
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, // pre_defined
|
||||
(width >> 8) & 0xff,
|
||||
width & 0xff, // width
|
||||
(height >> 8) & 0xff,
|
||||
height & 0xff, // height
|
||||
0x00, 0x48, 0x00, 0x00, // horizresolution
|
||||
0x00, 0x48, 0x00, 0x00, // vertresolution
|
||||
0x00, 0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x01, // frame_count
|
||||
0x12,
|
||||
0x64, 0x61, 0x69, 0x6C, // dailymotion/hls.js
|
||||
0x79, 0x6D, 0x6F, 0x74,
|
||||
0x69, 0x6F, 0x6E, 0x2F,
|
||||
0x68, 0x6C, 0x73, 0x2E,
|
||||
0x6A, 0x73, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, // compressorname
|
||||
0x00, 0x18, // depth = 24
|
||||
0x11, 0x11]) // pre_defined = -1
|
||||
const btrt = new Uint8Array([
|
||||
0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB
|
||||
0x00, 0x2d, 0xc6, 0xc0, // maxBitrate
|
||||
0x00, 0x2d, 0xc6, 0xc0 // avgBitrate
|
||||
])
|
||||
const pasp = new Uint8Array([
|
||||
(hSpacing >> 24), // hSpacing
|
||||
(hSpacing >> 16) & 0xff,
|
||||
(hSpacing >> 8) & 0xff,
|
||||
hSpacing & 0xff,
|
||||
(vSpacing >> 24), // vSpacing
|
||||
(vSpacing >> 16) & 0xff,
|
||||
(vSpacing >> 8) & 0xff,
|
||||
vSpacing & 0xff
|
||||
])
|
||||
|
||||
buffer.write(
|
||||
FMP4.size(size + avc1.byteLength + avcc.byteLength + btrt.byteLength), FMP4.type('avc1'), avc1,
|
||||
FMP4.size(8 + avcc.byteLength), FMP4.type('avcC'), avcc,
|
||||
FMP4.size(20), FMP4.type('btrt'), btrt,
|
||||
FMP4.size(16), FMP4.type('pasp'), pasp
|
||||
)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static stts () {
|
||||
const buffer = new Buffer()
|
||||
const content = new Uint8Array([
|
||||
0x00, // version
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x00 // entry_count
|
||||
])
|
||||
buffer.write(FMP4.size(16), FMP4.type('stts'), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static stsc () {
|
||||
const buffer = new Buffer()
|
||||
const content = new Uint8Array([
|
||||
0x00, // version
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x00 // entry_count
|
||||
])
|
||||
buffer.write(FMP4.size(16), FMP4.type('stsc'), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static stco () {
|
||||
const buffer = new Buffer()
|
||||
const content = new Uint8Array([
|
||||
0x00, // version
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x00 // entry_count
|
||||
])
|
||||
buffer.write(FMP4.size(16), FMP4.type('stco'), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static stsz () {
|
||||
const buffer = new Buffer()
|
||||
const content = new Uint8Array([
|
||||
0x00, // version
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x00, // sample_size
|
||||
0x00, 0x00, 0x00, 0x00 // sample_count
|
||||
])
|
||||
buffer.write(FMP4.size(20), FMP4.type('stsz'), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static mvex (duration, timeScale) {
|
||||
const buffer = new Buffer(); let size = 8
|
||||
const mehd = FMP4.mehd(duration * timeScale)
|
||||
const trex = FMP4.trex(1)
|
||||
const trep = FMP4.trep(1);
|
||||
|
||||
[mehd, trex, trep].forEach(item => {
|
||||
size += item.byteLength
|
||||
})
|
||||
buffer.write(FMP4.size(size), FMP4.type('mvex'), mehd, trex, trep)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static mehd (duration) {
|
||||
const buffer = new Buffer()
|
||||
const content = new Uint8Array([
|
||||
(duration >> 24),
|
||||
(duration >> 16) & 0xff,
|
||||
(duration >> 8) & 0xff,
|
||||
(duration & 0xff)
|
||||
])
|
||||
buffer.write(FMP4.size(16), FMP4.type('mehd'), FMP4.extension(0, 0), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static trex (id) {
|
||||
const buffer = new Buffer()
|
||||
const content = new Uint8Array([
|
||||
0x00, // version 0
|
||||
0x00, 0x00, 0x00, // flags
|
||||
(id >> 24),
|
||||
(id >> 16) & 0xff,
|
||||
(id >> 8) & 0xff,
|
||||
(id & 0xff), // track_ID
|
||||
0x00, 0x00, 0x00, 0x01, // default_sample_description_index
|
||||
0x00, 0x00, 0x04, 0x00, // default_sample_duration
|
||||
0x00, 0x00, 0x00, 0x00, // default_sample_size
|
||||
0x00, 0x1e, 0x84, 0x80 // default_sample_flags
|
||||
])
|
||||
buffer.write(FMP4.size(8 + content.byteLength), FMP4.type('trex'), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static trep (id) {
|
||||
const buffer = new Buffer()
|
||||
const content = new Uint8Array([
|
||||
(id >> 24),
|
||||
(id >> 16) & 0xff,
|
||||
(id >> 8) & 0xff,
|
||||
(id & 0xff) // track_ID
|
||||
])
|
||||
buffer.write(FMP4.size(16), FMP4.type('trep'), FMP4.extension(0, 0), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static moof (data) {
|
||||
const buffer = new Buffer(); let size = 8
|
||||
const mfhd = FMP4.mfhd()
|
||||
const traf = FMP4.traf(data);
|
||||
[mfhd, traf].forEach(item => {
|
||||
size += item.byteLength
|
||||
})
|
||||
buffer.write(FMP4.size(size), FMP4.type('moof'), mfhd, traf)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static mfhd () {
|
||||
const buffer = new Buffer()
|
||||
const content = Buffer.writeUint32(FMP4.sequence)
|
||||
FMP4.sequence += 1
|
||||
buffer.write(FMP4.size(16), FMP4.type('mfhd'), FMP4.extension(0, 0), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static traf (data) {
|
||||
const buffer = new Buffer(); let size = 8
|
||||
const tfhd = FMP4.tfhd(1)
|
||||
const tfdt = FMP4.tfdt(data.time)
|
||||
const trun = FMP4.trun(data);
|
||||
[tfhd, tfdt, trun].forEach(item => {
|
||||
size += item.byteLength
|
||||
})
|
||||
buffer.write(FMP4.size(size), FMP4.type('traf'), tfhd, tfdt, trun)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static tfhd (id) {
|
||||
const buffer = new Buffer()
|
||||
const content = Buffer.writeUint32(id)
|
||||
buffer.write(FMP4.size(16), FMP4.type('tfhd'), FMP4.extension(0, 131072), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static tfdt (time) {
|
||||
const buffer = new Buffer()
|
||||
const content = new Uint8Array([
|
||||
(time >> 24),
|
||||
(time >> 16) & 0xff,
|
||||
(time >> 8) & 0xff,
|
||||
(time & 0xff)
|
||||
])
|
||||
buffer.write(FMP4.size(16), FMP4.type('tfdt'), FMP4.extension(0, 0), content)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static trun (data) {
|
||||
const buffer = new Buffer()
|
||||
const sampleCount = Buffer.writeUint32(data.samples.length)
|
||||
// mdat-header 8
|
||||
// moof-header 8
|
||||
// mfhd 16
|
||||
// traf-header 8
|
||||
// thhd 16
|
||||
// tfdt 20
|
||||
// trun-header 12
|
||||
// sampleCount 4
|
||||
// data-offset 4
|
||||
// samples.length
|
||||
const offset = Buffer.writeUint32(92 + 4 * data.samples.length)
|
||||
buffer.write(FMP4.size(20 + 4 * data.samples.length), FMP4.type('trun'), FMP4.extension(0, 513), sampleCount, offset)
|
||||
data.samples.forEach((item, idx) => {
|
||||
buffer.write(Buffer.writeUint32(item.size))
|
||||
})
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
static mdat (data) {
|
||||
const buffer = new Buffer(); let size = 8
|
||||
data.samples.forEach(item => {
|
||||
size += item.size
|
||||
})
|
||||
buffer.write(FMP4.size(size), FMP4.type('mdat'))
|
||||
data.samples.forEach(item => {
|
||||
buffer.write(item.buffer)
|
||||
})
|
||||
return buffer.buffer
|
||||
}
|
||||
}
|
||||
|
||||
FMP4.sequence = 1
|
||||
|
||||
export default FMP4
|
@ -1,397 +0,0 @@
|
||||
import { BasePlugin, Sniffer, Util, Events, Errors } from 'xgplayer'
|
||||
import MP4 from './mp4'
|
||||
import MSE from './media/mse'
|
||||
import Task from './media/task'
|
||||
import Buffer from './fmp4/buffer'
|
||||
import FMP4 from './fmp4/mp4'
|
||||
|
||||
let MIME_TYPE = 'audio/mp4; codecs="mp4a.40.5"'
|
||||
|
||||
const isEnded = (player, mp4) => {
|
||||
if (mp4.meta.endTime - player.currentTime < 2) {
|
||||
const range = player.getBufferedRange()
|
||||
if (player.currentTime - range[1] < 0.1) {
|
||||
player.mse.endOfStream()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const errorHandle = (player, err) => {
|
||||
console.log('errorHandle')
|
||||
}
|
||||
class M4APlayer extends BasePlugin {
|
||||
static set MIME_TYPE (value) {
|
||||
MIME_TYPE = value
|
||||
}
|
||||
|
||||
static get MIME_TYPE () {
|
||||
return MIME_TYPE
|
||||
}
|
||||
|
||||
static get pluginName () {
|
||||
return 'm4aPlayer'
|
||||
}
|
||||
|
||||
static get defaultConfig () {
|
||||
return {
|
||||
withCredentials: true,
|
||||
videoOnly: false,
|
||||
headers: {
|
||||
},
|
||||
chunkSize: Math.pow(25, 4),
|
||||
preloadTime: 15, // 预加载时长
|
||||
reqTimeLength: 5,
|
||||
segPlay: false,
|
||||
pluginRule: () => {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static isSupported () {
|
||||
return ['chrome', 'firfox', 'safari'].some(item => item === Sniffer.browser) && MSE.isSupported(MIME_TYPE)
|
||||
}
|
||||
|
||||
constructor (options) {
|
||||
super(options)
|
||||
this.mp4 = null
|
||||
this.mse = null
|
||||
this.waiterTimer = null
|
||||
this.name = ''
|
||||
this.vid = ''
|
||||
// this.attachEvents();
|
||||
}
|
||||
|
||||
afterCreate () {
|
||||
console.log('afterCreate')
|
||||
if (!M4APlayer.isSupported()) {
|
||||
return
|
||||
}
|
||||
// BasePlugin.defineGetterOrSetter(player, {
|
||||
// '__url': {
|
||||
// get: () => {
|
||||
// return this.mse ? this.mse.url : player.config.url
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
this.attachEvents()
|
||||
}
|
||||
|
||||
beforePlayerInit () {
|
||||
if (!M4APlayer.isSupported()) {
|
||||
return
|
||||
}
|
||||
const { playerConfig, player } = this
|
||||
let urlInfo = {}
|
||||
if (Util.typeOf(playerConfig.url) === 'Array') {
|
||||
urlInfo = playerConfig.url
|
||||
} else {
|
||||
urlInfo = [{
|
||||
src: playerConfig.url,
|
||||
name: playerConfig.name,
|
||||
vid: playerConfig.vid,
|
||||
poster: playerConfig.poster
|
||||
}]
|
||||
}
|
||||
if (!urlInfo[0].src) {
|
||||
player.emit('error', new Errors('other', player.config.vid))
|
||||
return
|
||||
}
|
||||
this.urlInfo = urlInfo
|
||||
this.url = urlInfo[0].src
|
||||
// if (config.segPlay) {
|
||||
// let sp = config.segPlay
|
||||
// player.cut(sp.start, sp.end).then(blob => {
|
||||
// if (blob) {
|
||||
// player.config.url = URL.createObjectURL(blob)
|
||||
// player.start(player.config.url)
|
||||
// }
|
||||
// }, () => {
|
||||
// console.log('error')
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
|
||||
return this.initM4a(this.url).then(result => {
|
||||
const mp4 = result[0]; const mse = result[1]
|
||||
player.mp4 = mp4
|
||||
player.mse = mse
|
||||
player.url = this.mse.url
|
||||
mp4.on('error', err => {
|
||||
errorHandle(player, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
attachEvents () {
|
||||
const { player } = this
|
||||
// player.once('canplay', () => {
|
||||
// // safari decoder time offset
|
||||
// if (Sniffer.browser === 'safari' && player.buffered.length) {
|
||||
// let start = player.buffered.start(0);
|
||||
// player.currentTime = start + 0.1;
|
||||
// }
|
||||
// });
|
||||
player.on(Events.TIME_UPDATE, this.onTimeupdate)
|
||||
player.on(Events.SEEKING, this.onSeeking)
|
||||
player.on(Events.WAITING, this.onWaiting)
|
||||
player.on(Events.REPLAY, this.replay)
|
||||
}
|
||||
|
||||
detachEvents () {
|
||||
const { player } = this
|
||||
player.off(Events.TIME_UPDATE, this.onTimeupdate)
|
||||
player.off(Events.SEEKING, this.onSeeking)
|
||||
player.off(Events.WAITING, this.onWaiting)
|
||||
player.off(Events.REPLAY, this.replay)
|
||||
}
|
||||
|
||||
loadData (i = 0, time = 0, order = null, nextOrder = null) {
|
||||
const { player } = this
|
||||
if (player.timer) {
|
||||
clearTimeout(player.timer)
|
||||
}
|
||||
time = Math.max(time, player.currentTime)
|
||||
player.timer = setTimeout(function () {
|
||||
player.mp4.seek(time, order, nextOrder).then(buffer => {
|
||||
if (buffer) {
|
||||
const mse = player.mse
|
||||
mse.updating = true
|
||||
mse.appendBuffer(buffer)
|
||||
mse.once('updateend', () => {
|
||||
mse.updating = false
|
||||
})
|
||||
}
|
||||
}, () => {
|
||||
if (i < 10) {
|
||||
setTimeout(function () {
|
||||
this.loadData(i + 1)
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}, 50)
|
||||
}
|
||||
|
||||
initM4a (url, replaying = false) {
|
||||
const { config } = this
|
||||
this.mp4 = new MP4(url)
|
||||
this.mp4.reqTimeLength = config.reqTimeLength
|
||||
return new Promise((resolve, reject) => {
|
||||
this.mp4.once('mdatReady', () => {
|
||||
this.mse = new MSE()
|
||||
if (replaying) {
|
||||
this.mse.replaying = true
|
||||
}
|
||||
this.mse.on('sourceopen', () => {
|
||||
this.mse.appendBuffer(this.mp4.packMeta(this.mp4.meta))
|
||||
this.mse.once('updateend', () => {
|
||||
this.loadData()
|
||||
})
|
||||
})
|
||||
this.mse.on('error', function (e) {
|
||||
reject(e)
|
||||
})
|
||||
resolve([this.mp4, this.mse])
|
||||
})
|
||||
this.mp4.on('error', (e) => {
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
cut (url, start = 0, end) {
|
||||
const segment = new Buffer()
|
||||
return new Promise((resolve, reject) => {
|
||||
const mp4 = new MP4(url)
|
||||
mp4.once('mdatReady', () => {
|
||||
if (!end || end <= start) {
|
||||
end = start + 15
|
||||
}
|
||||
if (end > mp4.meta.audioDuration) {
|
||||
start = mp4.meta.audioDuration - (end - start)
|
||||
end = mp4.meta.audioDuration
|
||||
}
|
||||
mp4.reqTimeLength = end - start
|
||||
mp4.cut = true
|
||||
mp4.seek(start).then(buffer => {
|
||||
if (buffer) {
|
||||
const meta = Util.deepCopy({
|
||||
duration: mp4.reqTimeLength,
|
||||
audioDuration: mp4.reqTimeLength,
|
||||
endTime: mp4.reqTimeLength
|
||||
}, mp4.meta)
|
||||
meta.duration = mp4.reqTimeLength
|
||||
meta.audioDuration = mp4.reqTimeLength
|
||||
meta.endTime = mp4.reqTimeLength
|
||||
segment.write(mp4.packMeta(meta), buffer)
|
||||
resolve(new window.Blob([segment.buffer], { type: MIME_TYPE }))
|
||||
}
|
||||
})
|
||||
})
|
||||
mp4.on('error', (e) => {
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
switchURL (url) {
|
||||
|
||||
}
|
||||
|
||||
onTimeupdate = (e) => {
|
||||
const { config, player, mse, mp4 } = this
|
||||
// let mse = player.mse; let mp4 = player.mp4
|
||||
if (mse && !mse.updating && mp4.canDownload) {
|
||||
const timeRage = mp4.timeRage
|
||||
const range = player.getBufferedRange()
|
||||
const cacheMaxTime = player.currentTime + config.preloadTime
|
||||
if (range[1] - cacheMaxTime > 0) {
|
||||
return
|
||||
}
|
||||
timeRage.every((item, idx) => {
|
||||
if (range[1] === 0) {
|
||||
this.loadData(5)
|
||||
return false
|
||||
} else {
|
||||
if (item[0].time >= range[1] && !mp4.bufferCache.has(idx)) {
|
||||
this.loadData(0, item[0].time, item[0].order, item[1].order)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
isEnded(player, mp4)// hack for older webkit
|
||||
}
|
||||
}
|
||||
|
||||
onSeeking = (e) => {
|
||||
const { player } = this
|
||||
const buffered = player.buffered
|
||||
let hasBuffered = false
|
||||
const curTime = player.currentTime
|
||||
Task.clear()
|
||||
const timeRage = player.mp4.timeRage
|
||||
if (buffered.length) {
|
||||
for (let i = 0, len = buffered.length; i < len; i++) {
|
||||
if (curTime >= buffered.start(i) && curTime <= buffered.end(i)) {
|
||||
hasBuffered = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hasBuffered) {
|
||||
timeRage.every((item, idx) => {
|
||||
if (item[0].time <= curTime && item[1].time > curTime) {
|
||||
this.loadData(0, item[0].time, item[0].order, item[1].order)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
timeRage.every((item, idx) => {
|
||||
if (item[0].time <= curTime && item[1].time > curTime) {
|
||||
this.loadData(0, item[0].time, item[0].order, item[1].order)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onPause = (e) => {
|
||||
Task.clear()
|
||||
}
|
||||
|
||||
onPlaying = (e) => {
|
||||
if (this.waiterTimer) {
|
||||
clearTimeout(this.waiterTimer)
|
||||
this.waiterTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
onWaiting = (e) => {
|
||||
const { player } = this
|
||||
const buffered = player.buffered
|
||||
let hasBuffered = false
|
||||
const curTime = player.currentTime
|
||||
Task.clear()
|
||||
const timeRage = player.mp4.timeRage
|
||||
if (buffered.length) {
|
||||
for (let i = 0, len = buffered.length; i < len; i++) {
|
||||
if (curTime >= buffered.start(i) && curTime <= buffered.end(i)) {
|
||||
hasBuffered = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hasBuffered) {
|
||||
timeRage.every((item, idx) => {
|
||||
if (item[0].time <= curTime && item[1].time > curTime) {
|
||||
this.loadData(0, item[0].time, item[0].order, item[1].order)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
timeRage.every((item, idx) => {
|
||||
if (item[0].time <= curTime && item[1].time > curTime) {
|
||||
this.loadData(0, item[0].time, item[0].order, item[1].order)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onEnded = (e) => {
|
||||
const { player } = this
|
||||
// player.hasEnded = true
|
||||
if (player.config.offline) {
|
||||
const mdatCache = new Buffer()
|
||||
mdatCache.write(FMP4.size(player.mp4.mdatBox.size), FMP4.type('mdat'))
|
||||
player.mp4.mdatCache.sort((a, b) => {
|
||||
return a.start - b.start
|
||||
})
|
||||
let end = player.mp4.mdatCache[0].start - 1
|
||||
player.mp4.mdatCache.forEach((item, index) => {
|
||||
if (item.start === end + 1) {
|
||||
mdatCache.write(item.buffer)
|
||||
end = item.end
|
||||
}
|
||||
})
|
||||
if (end !== player.mp4.mdatCache[player.mp4.mdatCache.length - 1].end) {
|
||||
return
|
||||
}
|
||||
const m4aCache = new Buffer()
|
||||
if (player.mp4.freeBuffer) {
|
||||
m4aCache.write(new Uint8Array(player.mp4.ftypBuffer), new Uint8Array(player.mp4.moovBuffer), new Uint8Array(player.mp4.freeBuffer), mdatCache.buffer)
|
||||
} else {
|
||||
m4aCache.write(new Uint8Array(player.mp4.ftypBuffer), new Uint8Array(player.mp4.moovBuffer), mdatCache.buffer)
|
||||
}
|
||||
const offlineVid = this.vid || this.name
|
||||
player.database.openDB(() => {
|
||||
player.database.addData(player.database.myDB.ojstore.name, [{ vid: offlineVid, blob: new window.Blob([m4aCache.buffer], { type: MIME_TYPE }) }])
|
||||
setTimeout(() => {
|
||||
player.database.closeDB()
|
||||
}, 5000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
replay = () => {}
|
||||
|
||||
destroy () {
|
||||
const { player } = this
|
||||
Task.clear()
|
||||
this.detachEvents()
|
||||
if (player.timer) {
|
||||
clearTimeout(player.timer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default M4APlayer
|
@ -1,75 +0,0 @@
|
||||
import EventEmitter from 'event-emitter'
|
||||
import Errors from '../error'
|
||||
|
||||
class MSE {
|
||||
constructor (codecs = 'audio/mp4; codecs="mp4a.40.5"') {
|
||||
const self = this
|
||||
EventEmitter(this)
|
||||
this.codecs = codecs
|
||||
this.replaying = false
|
||||
this.mediaSource = new window.MediaSource()
|
||||
this.url = window.URL.createObjectURL(this.mediaSource)
|
||||
this.queue = []
|
||||
this.updating = false
|
||||
this.mediaSource.addEventListener('sourceopen', function () {
|
||||
self.sourceBuffer = self.mediaSource.addSourceBuffer(self.codecs)
|
||||
self.sourceBuffer.addEventListener('error', function (e) {
|
||||
self.emit('error', new Errors('mse', '', { line: 16, handle: '[MSE] constructor sourceopen', msg: e.message }))
|
||||
})
|
||||
self.sourceBuffer.addEventListener('updateend', function (e) {
|
||||
self.emit('updateend')
|
||||
const buffer = self.queue.shift()
|
||||
if (buffer) {
|
||||
self.sourceBuffer.appendBuffer(buffer)
|
||||
}
|
||||
})
|
||||
self.emit('sourceopen')
|
||||
})
|
||||
this.mediaSource.addEventListener('sourceclose', function () {
|
||||
self.emit('sourceclose')
|
||||
})
|
||||
}
|
||||
|
||||
get state () {
|
||||
if (this.replaying) {
|
||||
return 'open'
|
||||
} else {
|
||||
return this.mediaSource.readyState
|
||||
}
|
||||
}
|
||||
|
||||
get duration () {
|
||||
return this.mediaSource.duration
|
||||
}
|
||||
|
||||
set duration (value) {
|
||||
this.mediaSource.duration = value
|
||||
}
|
||||
|
||||
appendBuffer (buffer) {
|
||||
const sourceBuffer = this.sourceBuffer
|
||||
if (sourceBuffer.updating === false && this.state === 'open') {
|
||||
sourceBuffer.appendBuffer(buffer)
|
||||
return true
|
||||
} else {
|
||||
this.queue.push(buffer)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
removeBuffer (start, end) {
|
||||
this.sourceBuffer.remove(start, end)
|
||||
}
|
||||
|
||||
endOfStream () {
|
||||
if (this.mediaSource.readyState === 'open') {
|
||||
this.mediaSource.endOfStream()
|
||||
}
|
||||
}
|
||||
|
||||
static isSupported (codecs) {
|
||||
return window.MediaSource && window.MediaSource.isTypeSupported(codecs)
|
||||
}
|
||||
}
|
||||
|
||||
export default MSE
|
@ -1,87 +0,0 @@
|
||||
import EventEmitter from 'event-emitter'
|
||||
import Errors from '../error'
|
||||
|
||||
class Task {
|
||||
constructor (url, range, callback) {
|
||||
EventEmitter(this)
|
||||
this.url = url
|
||||
this.range = range
|
||||
this.id = range.join('-')
|
||||
this.on = false
|
||||
const xhr = new window.XMLHttpRequest()
|
||||
xhr.target = this
|
||||
xhr.responseType = 'arraybuffer'
|
||||
xhr.open('get', url)
|
||||
xhr.setRequestHeader('Range', `bytes=${range[0]}-${range[1]}`)
|
||||
xhr.onload = function () {
|
||||
if (xhr.status === 200 || xhr.status === 206) {
|
||||
if (callback && callback instanceof Function) {
|
||||
callback(xhr.response)
|
||||
}
|
||||
}
|
||||
xhr.target.remove()
|
||||
}
|
||||
xhr.onerror = function (e) {
|
||||
xhr.target.emit('error', new Errors('network', '', { line: 25, handle: '[Task] constructor', msg: e.message, url }))
|
||||
xhr.target.remove()
|
||||
}
|
||||
xhr.onabort = function () {
|
||||
xhr.target.remove()
|
||||
}
|
||||
this.xhr = xhr
|
||||
Task.queue.push(this)
|
||||
this.update()
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this.xhr.abort()
|
||||
}
|
||||
|
||||
remove () {
|
||||
Task.queue.filter((item, idx) => {
|
||||
if (item.url === this.url && item.id === this.id) {
|
||||
Task.queue.splice(idx, 1)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
this.update()
|
||||
}
|
||||
|
||||
update () {
|
||||
const Queue = Task.queue
|
||||
const sended = Queue.filter((item) => item.on)
|
||||
const wait = Queue.filter(item => !item.on)
|
||||
const max = Task.limit - sended.length
|
||||
wait.forEach((item, idx) => {
|
||||
if (idx < max) {
|
||||
item.run()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
run () {
|
||||
if (this.xhr.readyState === 1) {
|
||||
this.on = true
|
||||
this.xhr.send()
|
||||
} else {
|
||||
this.remove()
|
||||
}
|
||||
}
|
||||
|
||||
static clear () {
|
||||
Task.queue.forEach(item => {
|
||||
if (item.on) {
|
||||
item.cancel()
|
||||
}
|
||||
})
|
||||
Task.queue.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
Task.queue = []
|
||||
Task.limit = 2
|
||||
window.Task = Task
|
||||
|
||||
export default Task
|
@ -1,459 +0,0 @@
|
||||
import EventEmitter from 'event-emitter'
|
||||
import Merge from 'deepmerge'
|
||||
import Parser from './parse'
|
||||
import Buffer from './fmp4/buffer'
|
||||
import FMP4 from './fmp4/mp4'
|
||||
import Task from './media/task'
|
||||
// import Download from './util/download'
|
||||
import util from './util'
|
||||
import { Util } from 'xgplayer'
|
||||
import Errors from './error'
|
||||
|
||||
class MP4 {
|
||||
/**
|
||||
* [constructor 构造函数]
|
||||
* @param {String} url [视频地址]
|
||||
* @param {Number} [chunk_size=Math.pow(25, 4)] [请求的数据块大小,对于长视频设置的较大些可以避免二次请求]
|
||||
*/
|
||||
constructor (url, chunkSize = 1024) {
|
||||
EventEmitter(this)
|
||||
this.url = url
|
||||
this.CHUNK_SIZE = chunkSize
|
||||
this.reqTimeLength = 5
|
||||
this.init(url)
|
||||
this.once('mdatReady', this.moovParse.bind(this))
|
||||
this.cache = new Buffer()
|
||||
this.bufferCache = new Set()
|
||||
this.mdatCache = []
|
||||
this.timeRage = []
|
||||
this.canDownload = true
|
||||
this.cut = false
|
||||
}
|
||||
|
||||
/**
|
||||
* [getData 根据字节区间下载二进制数据]
|
||||
* @param {Number} [start=0] [起始字节]
|
||||
* @param {Number} [end=start + this.CHUNK_SIZE] [截止字节]
|
||||
*/
|
||||
getData (start = 0, end = start + this.CHUNK_SIZE) {
|
||||
const self = this
|
||||
return new Promise((resolve, reject) => {
|
||||
const task = new Task(this.url, [
|
||||
start, end
|
||||
], resolve)
|
||||
task.once('error', err => {
|
||||
self.emit('error', err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* [moovParse 解析视频信息]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
moovParse () {
|
||||
const self = this
|
||||
const moov = this.moovBox
|
||||
const mvhd = util.findBox(moov, 'mvhd')
|
||||
let traks = util.findBox(moov, 'trak')
|
||||
let audioTrak
|
||||
let audioCodec
|
||||
let audioTimeScale
|
||||
let channelCount,
|
||||
sampleRate,
|
||||
decoderConfig
|
||||
if (Util.typeOf(traks) !== 'Array') {
|
||||
traks = [traks]
|
||||
}
|
||||
traks.forEach(trak => {
|
||||
const hdlr = util.findBox(trak, 'hdlr')
|
||||
const mdhd = util.findBox(trak, 'mdhd')
|
||||
if (!hdlr || !mdhd) {
|
||||
self.emit('error', new Errors('parse', '', { line: 72, handle: '[MP4] moovParse', url: self.url }))
|
||||
return
|
||||
}
|
||||
const stsd = util.findBox(trak, 'stsd')
|
||||
const codecBox = stsd.subBox[0]
|
||||
if (hdlr.handleType === 'soun') {
|
||||
audioTrak = trak
|
||||
const esds = util.findBox(trak, 'esds')
|
||||
const mp4a = util.findBox(trak, 'mp4a')
|
||||
const ESDescriptor = util.findBox(trak, 5)
|
||||
audioTimeScale = mdhd.timescale
|
||||
if (esds) {
|
||||
audioCodec = `${codecBox.type}.` + util.toHex(esds.subBox[0].subBox[0].typeID) + `.${esds.subBox[0].subBox[0].subBox[0].type}`
|
||||
} else {
|
||||
audioCodec = `${codecBox.type}`
|
||||
}
|
||||
if (ESDescriptor && ESDescriptor.EScode) {
|
||||
decoderConfig = ESDescriptor.EScode.map((item) => Number(`0x${item}`))
|
||||
}
|
||||
if (mp4a) {
|
||||
channelCount = mp4a.channelCount
|
||||
sampleRate = mp4a.sampleRate
|
||||
}
|
||||
}
|
||||
})
|
||||
this.audioTrak = Merge({}, audioTrak)
|
||||
const mdat = this._boxes.find(item => item.type === 'mdat')
|
||||
const audioDuration = parseFloat(util.seekTrakDuration(audioTrak, audioTimeScale))
|
||||
this.mdatStart = mdat.start
|
||||
this.sampleCount = util.sampleCount(util.findBox(this.audioTrak, 'stts').entry)
|
||||
|
||||
let audioFrame, audioNextFrame
|
||||
const stts = util.findBox(this.audioTrak, 'stts').entry
|
||||
for (let i = 0; i < audioDuration; i += this.reqTimeLength) {
|
||||
audioFrame = util.seekOrderSampleByTime(stts, audioTimeScale, i)
|
||||
if (i + this.reqTimeLength < audioDuration) {
|
||||
audioNextFrame = util.seekOrderSampleByTime(stts, audioTimeScale, i + this.reqTimeLength)
|
||||
this.timeRage.push([
|
||||
{ time: audioFrame.startTime, order: audioFrame.order },
|
||||
{ time: audioNextFrame.startTime, order: audioNextFrame.order }
|
||||
])
|
||||
} else {
|
||||
this.timeRage.push([
|
||||
{ time: audioFrame.startTime, order: audioFrame.order },
|
||||
{ time: audioDuration, order: this.sampleCount - 1 }
|
||||
])
|
||||
}
|
||||
}
|
||||
// console.log('this.timeRage')
|
||||
// console.log(this.timeRage)
|
||||
this.meta = {
|
||||
audioCodec,
|
||||
createTime: mvhd.createTime,
|
||||
modifyTime: mvhd.modifyTime,
|
||||
duration: mvhd.duration / mvhd.timeScale,
|
||||
timeScale: mvhd.timeScale,
|
||||
audioDuration,
|
||||
audioTimeScale,
|
||||
endTime: audioDuration,
|
||||
channelCount,
|
||||
sampleRate,
|
||||
audioConfig: decoderConfig
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [init 实例的初始化,主要是获取视频的MOOV元信息]
|
||||
*/
|
||||
init () {
|
||||
const self = this
|
||||
self.getData().then((resFir) => {
|
||||
let parsedFir
|
||||
let mdatStart = 0
|
||||
|
||||
let mdat, moov, ftyp
|
||||
|
||||
let boxes
|
||||
try {
|
||||
parsedFir = new Parser(resFir)
|
||||
} catch (e) {
|
||||
self.emit('error', e.type ? e : new Errors('parse', '', { line: 176, handle: '[MP4] init', msg: e.message }))
|
||||
return false
|
||||
}
|
||||
self._boxes = boxes = parsedFir.boxes
|
||||
boxes.every(item => {
|
||||
if (item.type === 'ftyp') {
|
||||
mdatStart += item.size
|
||||
ftyp = item
|
||||
self.ftypBox = ftyp
|
||||
self.ftypBuffer = resFir.slice(0, ftyp.size)
|
||||
} else if (item.type === 'mdat') {
|
||||
mdat = item
|
||||
mdat.start = mdatStart
|
||||
mdatStart += item.size
|
||||
self.mdatBox = mdat
|
||||
// self.emit('mdatReady', moov)
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (!mdat) {
|
||||
const nextBox = parsedFir.nextBox
|
||||
if (nextBox) {
|
||||
if (nextBox.type === 'moov' || nextBox.type === 'free') {
|
||||
self.getData(mdatStart, mdatStart + nextBox.size + 1024).then(resSec => {
|
||||
const parsedSec = new Parser(resSec)
|
||||
self._boxes = self._boxes.concat(parsedSec.boxes)
|
||||
parsedSec.boxes.every(item => {
|
||||
if (item.type === 'moov') {
|
||||
mdatStart += item.size
|
||||
moov = item
|
||||
self.moovBox = moov
|
||||
self.moovBuffer = resSec.slice(0, moov.size)
|
||||
return true
|
||||
} else if (item.type === 'mdat') {
|
||||
mdat = item
|
||||
mdat.start = mdatStart
|
||||
mdatStart += item.size
|
||||
self.mdatBox = mdat
|
||||
self.emit('mdatReady', moov)
|
||||
return false
|
||||
} else {
|
||||
mdatStart += item.size
|
||||
return true
|
||||
}
|
||||
})
|
||||
if (!mdat) {
|
||||
const nextBoxSec = parsedSec.nextBox
|
||||
if (nextBoxSec) {
|
||||
if (nextBoxSec.type === 'free') {
|
||||
self.getData(mdatStart, mdatStart + nextBoxSec.size + 1024).then(resThi => {
|
||||
const parsedThi = new Parser(resThi)
|
||||
self._boxes = self._boxes.concat(parsedThi.boxes)
|
||||
parsedThi.boxes.every(item => {
|
||||
if (item.type === 'mdat') {
|
||||
mdat = item
|
||||
mdat.start = mdatStart
|
||||
self.mdatBox = mdat
|
||||
self.emit('mdatReady', moov)
|
||||
return false
|
||||
} else if (item.type === 'free') {
|
||||
self.freeBuffer = resThi.slice(0, item.size)
|
||||
mdatStart += item.size
|
||||
return true
|
||||
} else {
|
||||
mdatStart += item.size
|
||||
return true
|
||||
}
|
||||
})
|
||||
if (!mdat) {
|
||||
self.emit('error', new Errors('parse', '', { line: 207, handle: '[MP4] init', msg: 'not find mdat box' }))
|
||||
}
|
||||
}).catch(() => {
|
||||
self.emit('error', new Errors('network', '', { line: 210, handle: '[MP4] getData', msg: 'getData failed' }))
|
||||
})
|
||||
} else {
|
||||
self.emit('error', new Errors('parse', '', { line: 213, handle: '[MP4] init', msg: 'not find mdat box' }))
|
||||
}
|
||||
} else {
|
||||
self.emit('error', new Errors('parse', '', { line: 216, handle: '[MP4] init', msg: 'not find mdat box' }))
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
self.emit('error', new Errors('network', '', { line: 220, handle: '[MP4] getData', msg: 'getData failed' }))
|
||||
})
|
||||
} else {
|
||||
self.emit('error', new Errors('parse', '', { line: 223, handle: '[MP4] init', msg: 'not find mdat box' }))
|
||||
}
|
||||
} else {
|
||||
self.emit('error', new Errors('parse', '', { line: 226, handle: '[MP4] init', msg: 'not find mdat box' }))
|
||||
}
|
||||
} else {
|
||||
const nextBox = parsedFir.nextBox
|
||||
if (nextBox) {
|
||||
self.emit('error', new Errors('parse', '', { line: 223, handle: '[MP4] init', msg: 'not find moov box' }))
|
||||
} else {
|
||||
self.getData(mdatStart, mdatStart + 1024).then(resSec => {
|
||||
const parsedSec = new Parser(resSec)
|
||||
self._boxes = self._boxes.concat(parsedSec.boxes)
|
||||
parsedSec.boxes.every(item => {
|
||||
if (item.type === 'moov') {
|
||||
mdatStart += item.size
|
||||
moov = item
|
||||
self.moovBox = moov
|
||||
self.moovBuffer = resSec.slice(0, moov.size)
|
||||
self.emit('mdatReady', moov)
|
||||
return false
|
||||
} else {
|
||||
mdatStart += item.size
|
||||
return true
|
||||
}
|
||||
})
|
||||
if (!moov) {
|
||||
const nextBoxSec = parsedSec.nextBox
|
||||
if (nextBoxSec) {
|
||||
if (nextBoxSec.type === 'moov') {
|
||||
self.getData(mdatStart, mdatStart + nextBoxSec.size - 1).then(resThi => {
|
||||
const parsedThi = new Parser(resThi)
|
||||
self._boxes = self._boxes.concat(parsedThi.boxes)
|
||||
parsedThi.boxes.every(item => {
|
||||
if (item.type === 'moov') {
|
||||
mdatStart += item.size
|
||||
moov = item
|
||||
self.moovBox = moov
|
||||
self.moovBuffer = resSec.slice(0, moov.size)
|
||||
self.emit('mdatReady', moov)
|
||||
return false
|
||||
} else {
|
||||
mdatStart += item.size
|
||||
return true
|
||||
}
|
||||
})
|
||||
if (!moov) {
|
||||
self.emit('error', new Errors('parse', '', { line: 207, handle: '[MP4] init', msg: 'not find moov box' }))
|
||||
}
|
||||
}).catch(() => {
|
||||
self.emit('error', new Errors('network', '', { line: 210, handle: '[MP4] getData', msg: 'getData failed' }))
|
||||
})
|
||||
} else {
|
||||
self.emit('error', new Errors('parse', '', { line: 213, handle: '[MP4] init', msg: 'not find moov box' }))
|
||||
}
|
||||
} else {
|
||||
self.emit('error', new Errors('parse', '', { line: 216, handle: '[MP4] init', msg: 'not find moov box' }))
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
self.emit('error', new Errors('network', '', { line: 220, handle: '[MP4] getData', msg: 'getData failed' }))
|
||||
})
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
self.emit('error', new Errors('network', '', { line: 230, handle: '[MP4] getData', msg: 'getData failed' }))
|
||||
})
|
||||
}
|
||||
|
||||
getSamplesByOrders (type = 'audio', start, end) {
|
||||
const trak = this.audioTrak
|
||||
const stsc = util.findBox(trak, 'stsc') // chunk~samples
|
||||
const stsz = util.findBox(trak, 'stsz') // sample-size
|
||||
const stts = util.findBox(trak, 'stts') // sample-time
|
||||
const stco = util.findBox(trak, 'stco') // chunk-offset
|
||||
const ctts = util.findBox(trak, 'ctts') // offset-compositime
|
||||
const mdatStart = this.mdatStart
|
||||
let samples = []
|
||||
end = end !== undefined
|
||||
? end
|
||||
: stsz.entries.length
|
||||
if (start instanceof Array) {
|
||||
start.forEach((item, idx) => {
|
||||
samples.push({
|
||||
idx: item,
|
||||
size: stsz.entries[item],
|
||||
time: util.seekSampleTime(stts, ctts, item),
|
||||
offset: util.seekSampleOffset(stsc, stco, stsz, item, mdatStart)
|
||||
})
|
||||
})
|
||||
} else if (end !== 0) {
|
||||
for (let i = start; i < end; i++) {
|
||||
samples.push({
|
||||
idx: i,
|
||||
size: stsz.entries[i],
|
||||
time: util.seekSampleTime(stts, ctts, i),
|
||||
offset: util.seekSampleOffset(stsc, stco, stsz, i, mdatStart)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
samples = {
|
||||
idx: start,
|
||||
size: stsz.entries[start],
|
||||
time: util.seekSampleTime(stts, ctts, start),
|
||||
offset: util.seekSampleOffset(stsc, stco, stsz, start, mdatStart)
|
||||
}
|
||||
}
|
||||
return samples
|
||||
}
|
||||
|
||||
packMeta (meta) {
|
||||
if (!meta) {
|
||||
return
|
||||
}
|
||||
const buffer = new Buffer()
|
||||
buffer.write(FMP4.ftyp())
|
||||
buffer.write(FMP4.moov(meta))
|
||||
this.cache.write(buffer.buffer)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
seek (time, audioIndexOrder = null, audioNextIndexOrder = null) {
|
||||
const audioStts = util.findBox(this.audioTrak, 'stts').entry
|
||||
if (!audioIndexOrder) {
|
||||
audioIndexOrder = util.seekOrderSampleByTime(audioStts, this.meta.audioTimeScale, time).order
|
||||
}
|
||||
if (!audioNextIndexOrder) {
|
||||
if (time + this.reqTimeLength < this.meta.audioDuration) {
|
||||
audioNextIndexOrder = util.seekOrderSampleByTime(audioStts, this.meta.audioTimeScale, time + this.reqTimeLength).order
|
||||
}
|
||||
}
|
||||
if (this.bufferCache.has(audioIndexOrder)) {
|
||||
return Promise.resolve(null)
|
||||
} else {
|
||||
return this.loadFragment(audioIndexOrder, audioNextIndexOrder)
|
||||
}
|
||||
}
|
||||
|
||||
loadFragment (audioIndexOrder, audioNextIndexOrder) {
|
||||
let end
|
||||
const self = this
|
||||
const audioFrame = this.getSamplesByOrders('audio', audioIndexOrder, 0)
|
||||
const start = audioFrame.offset
|
||||
let audioNextFrame
|
||||
if (audioNextIndexOrder) {
|
||||
audioNextFrame = this.getSamplesByOrders('audio', audioNextIndexOrder, 0)
|
||||
end = audioNextFrame.offset - 1
|
||||
} else {
|
||||
audioNextFrame = this.getSamplesByOrders('audio', this.sampleCount - 1, 0)
|
||||
end = audioNextFrame.offset + audioNextFrame.size - 1
|
||||
}
|
||||
// console.log('start order')
|
||||
// console.log(audioIndexOrder)
|
||||
// console.log('start')
|
||||
// console.log(start + self.mdatStart)
|
||||
// console.log('end order')
|
||||
// console.log(audioNextIndexOrder)
|
||||
// console.log('end')
|
||||
// console.log(self.mdatStart + end)
|
||||
if (window.isNaN(start) || (end !== undefined && window.isNaN(end))) {
|
||||
self.emit('error', new Errors('parse', '', { line: 366, handle: '[MP4] loadFragment', url: self.url }))
|
||||
return false
|
||||
}
|
||||
return this.getData(
|
||||
start + self.mdatStart, end
|
||||
? self.mdatStart + end
|
||||
: '').then((dat) => {
|
||||
if (end) {
|
||||
this.mdatCache.push({
|
||||
start: start + self.mdatStart,
|
||||
end: self.mdatStart + end,
|
||||
buffer: new Uint8Array(dat)
|
||||
})
|
||||
}
|
||||
return self.createFragment(new Uint8Array(dat), start, audioIndexOrder, audioNextIndexOrder)
|
||||
})
|
||||
}
|
||||
|
||||
addFragment (data) {
|
||||
const buffer = new Buffer()
|
||||
buffer.write(FMP4.moof(data))
|
||||
buffer.write(FMP4.mdat(data))
|
||||
this.cache.write(buffer.buffer)
|
||||
return buffer.buffer
|
||||
}
|
||||
|
||||
createFragment (mdatData, start, audioIndexOrder, audioNextIndexOrder) {
|
||||
const resBuffers = []
|
||||
this.bufferCache.add(audioIndexOrder)
|
||||
const _samples = this.getSamplesByOrders(
|
||||
'audio', audioIndexOrder, audioNextIndexOrder)
|
||||
const samples = _samples.map((item, idx) => {
|
||||
return {
|
||||
size: item.size,
|
||||
duration: item.time.duration,
|
||||
offset: item.time.offset,
|
||||
buffer: new Uint8Array(mdatData.slice(item.offset - start, item.offset - start + item.size)),
|
||||
key: idx === 0
|
||||
}
|
||||
})
|
||||
resBuffers.push(this.addFragment({ id: 2, time: this.cut ? 0 : _samples[0].time.time, firstFlags: 0x00, flags: 0x701, samples: samples }))
|
||||
let bufferSize = 0
|
||||
resBuffers.every(item => {
|
||||
bufferSize += item.byteLength
|
||||
return true
|
||||
})
|
||||
const buffer = new Uint8Array(bufferSize)
|
||||
|
||||
let offset = 0
|
||||
resBuffers.every(item => {
|
||||
buffer.set(item, offset)
|
||||
offset += item.byteLength
|
||||
return true
|
||||
})
|
||||
return Promise.resolve(buffer)
|
||||
}
|
||||
|
||||
download () {
|
||||
// new Download('fmp4.mp4', this.cache.buffer)
|
||||
}
|
||||
}
|
||||
|
||||
export default MP4
|
@ -1,71 +0,0 @@
|
||||
import Stream from './stream'
|
||||
import Errors from '../error'
|
||||
|
||||
class Box {
|
||||
static boxParse = {}
|
||||
constructor () {
|
||||
this.headSize = 8
|
||||
this.size = 0
|
||||
this.type = ''
|
||||
this.subBox = []
|
||||
this.start = -1
|
||||
}
|
||||
|
||||
readHeader (stream) {
|
||||
this.start = stream.position
|
||||
this.size = stream.readUint32()
|
||||
this.type = String.fromCharCode(stream.readUint8(), stream.readUint8(), stream.readUint8(), stream.readUint8())
|
||||
if (this.size === 1) {
|
||||
this.size = stream.readUint64()
|
||||
} else if (this.size === 0) {
|
||||
if (this.type !== 'mdat') {
|
||||
throw new Errors('parse', '', { line: 19, handle: '[Box] readHeader', msg: 'parse mp4 mdat box failed' })
|
||||
}
|
||||
}
|
||||
if (this.type === 'uuid') {
|
||||
const uuid = []
|
||||
for (let i = 0; i < 16; i++) {
|
||||
uuid.push(stream.readUint8())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readBody (stream) {
|
||||
const end = this.size - stream.position + this.start
|
||||
const type = this.type
|
||||
this.data = stream.buffer.slice(stream.position, stream.position + end)
|
||||
stream.position += this.data.byteLength
|
||||
let parser
|
||||
if (Box.containerBox.find(item => item === type)) {
|
||||
parser = Box.containerParser
|
||||
} else {
|
||||
parser = Box.boxParse[type]
|
||||
}
|
||||
if (parser && parser instanceof Function) {
|
||||
parser.call(this)
|
||||
}
|
||||
}
|
||||
|
||||
read (stream) {
|
||||
this.readHeader(stream)
|
||||
this.readBody(stream)
|
||||
}
|
||||
|
||||
static containerParser () {
|
||||
let stream = new Stream(this.data)
|
||||
const size = stream.buffer.byteLength
|
||||
const self = this
|
||||
while (stream.position < size) {
|
||||
const box = new Box()
|
||||
box.readHeader(stream)
|
||||
self.subBox.push(box)
|
||||
box.readBody(stream)
|
||||
}
|
||||
delete self.data
|
||||
stream = null
|
||||
}
|
||||
}
|
||||
|
||||
Box.containerBox = ['moov', 'trak', 'edts', 'mdia', 'minf', 'dinf', 'stbl', 'mvex', 'moof', 'traf', 'mfra']
|
||||
|
||||
export default Box
|
@ -1,26 +0,0 @@
|
||||
import Box from '../box'
|
||||
import Stream from '../stream'
|
||||
import MP4DecSpecificDescrTag from './MP4DecSpecificDescrTag'
|
||||
|
||||
export default function MP4DecConfigDescrTag (stream) {
|
||||
const box = new Box()
|
||||
let size
|
||||
box.type = stream.readUint8()
|
||||
size = stream.readUint8()
|
||||
if (size === 0x80) {
|
||||
box.extend = true
|
||||
stream.skip(2)
|
||||
size = stream.readUint8() + 5
|
||||
} else {
|
||||
size += 2
|
||||
}
|
||||
box.size = size
|
||||
box.typeID = stream.readUint8()
|
||||
// 6 bits stream type,1 bit upstream flag,1 bit reserved flag
|
||||
box.streamUint = stream.readUint8()
|
||||
box.bufferSize = Stream.readByte(stream.dataview, 3)
|
||||
box.maximum = stream.readUint32()
|
||||
box.average = stream.readUint32()
|
||||
box.subBox.push(MP4DecSpecificDescrTag(stream))
|
||||
return box
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import Box from '../box'
|
||||
|
||||
export default function MP4DecSpecificDescrTag (stream) {
|
||||
const box = new Box()
|
||||
let size, dataSize
|
||||
box.type = stream.readUint8()
|
||||
size = stream.readUint8()
|
||||
if (size === 0x80) {
|
||||
box.extend = true
|
||||
stream.skip(2)
|
||||
size = stream.readUint8() + 5
|
||||
dataSize = size - 5
|
||||
} else {
|
||||
dataSize = size
|
||||
size += 2
|
||||
}
|
||||
box.size = size
|
||||
const EScode = []
|
||||
for (let i = 0; i < dataSize; i++) {
|
||||
EScode.push(Number(stream.readUint8()).toString(16).padStart(2, '0'))
|
||||
}
|
||||
box.EScode = EScode
|
||||
delete box.subBox
|
||||
return box
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import Box from '../box'
|
||||
import MP4DecConfigDescrTag from './MP4DecConfigDescrTag'
|
||||
import SLConfigDescriptor from './SLConfigDescriptor'
|
||||
|
||||
export default function MP4ESDescrTag (stream) {
|
||||
const box = new Box()
|
||||
let size
|
||||
box.type = stream.readUint8()
|
||||
size = stream.readUint8()
|
||||
if (size === 0x80) {
|
||||
box.extend = true
|
||||
stream.skip(2)
|
||||
size = stream.readUint8() + 5
|
||||
} else {
|
||||
size += 2
|
||||
}
|
||||
box.size = size
|
||||
box.esID = stream.readUint16()
|
||||
box.priority = stream.readUint8()
|
||||
box.subBox.push(MP4DecConfigDescrTag(stream))
|
||||
box.subBox.push(SLConfigDescriptor(stream))
|
||||
return box
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import Box from '../box'
|
||||
|
||||
export default function SLConfigDescriptor (stream) {
|
||||
const box = new Box()
|
||||
let size
|
||||
box.type = stream.readUint8()
|
||||
size = stream.readUint8()
|
||||
if (size === 0x80) {
|
||||
box.extend = true
|
||||
stream.skip(2)
|
||||
size = stream.readUint8() + 5
|
||||
} else {
|
||||
size += 2
|
||||
}
|
||||
box.size = size
|
||||
box.SL = stream.readUint8()
|
||||
delete box.subBox
|
||||
return box
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import Box from '../box'
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function avc1 () {
|
||||
let stream = new Stream(this.data)
|
||||
const self = this
|
||||
stream.skip(6)
|
||||
this.dataReferenceIndex = stream.readUint16()
|
||||
stream.skip(16)
|
||||
this.width = stream.readUint16()
|
||||
this.height = stream.readUint16()
|
||||
this.horizresolution = stream.readUint32()
|
||||
this.vertresolution = stream.readUint32()
|
||||
stream.skip(4)
|
||||
this.frameCount = stream.readUint16()
|
||||
stream.skip(1)
|
||||
for (let i = 0; i < 31; i++) {
|
||||
String.fromCharCode(stream.readUint8())
|
||||
}
|
||||
this.depth = stream.readUint16()
|
||||
stream.skip(2)
|
||||
while (stream.buffer.byteLength - stream.position >= 8) {
|
||||
const box = new Box()
|
||||
box.readHeader(stream)
|
||||
self.subBox.push(box)
|
||||
box.readBody(stream)
|
||||
}
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function avcC () {
|
||||
let stream = new Stream(this.data)
|
||||
this.configVersion = stream.readUint8()
|
||||
this.profile = stream.readUint8()
|
||||
this.profileCompatibility = stream.readUint8()
|
||||
this.AVCLevelIndication = stream.readUint8()
|
||||
this.lengthSizeMinusOne = (stream.readUint8() & 3) + 1
|
||||
this.numOfSequenceParameterSets = stream.readUint8() & 31
|
||||
const sequenceLength = stream.readUint16()
|
||||
this.sequenceLength = sequenceLength
|
||||
const sequence = []
|
||||
for (let i = 0; i < sequenceLength; i++) {
|
||||
sequence.push(Number(stream.readUint8()).toString(16))
|
||||
}
|
||||
this.ppsCount = stream.readUint8()
|
||||
const ppsLength = stream.readUint16()
|
||||
this.ppsLength = ppsLength
|
||||
const pps = []
|
||||
for (let i = 0; i < ppsLength; i++) {
|
||||
pps.push(Number(stream.readUint8()).toString(16))
|
||||
}
|
||||
this.pps = pps
|
||||
this.sequence = sequence
|
||||
const last = []; const dataviewLength = stream.dataview.byteLength
|
||||
while (stream.position < dataviewLength) {
|
||||
last.push(stream.readUint8())
|
||||
}
|
||||
this.last = last
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function btrt () {
|
||||
let stream = new Stream(this.data)
|
||||
this.bufferSizeDB = stream.readUint32()
|
||||
this.maxBitrate = stream.readUint32()
|
||||
this.avgBitrate = stream.readUint32()
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function co64 () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
this.count = stream.readUint32()
|
||||
const entries = []
|
||||
this.entries = entries
|
||||
for (let i = 0, count = this.count; i < count; i++) {
|
||||
entries.push(stream.readUint64())
|
||||
}
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function ctts () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
|
||||
this.entryCount = stream.readUint32()
|
||||
const entry = []
|
||||
this.entry = entry
|
||||
for (let i = 0, count = this.entryCount; i < count; i++) {
|
||||
entry.push({
|
||||
count: stream.readUint32(),
|
||||
offset: stream.readUint32()
|
||||
})
|
||||
}
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import Box from '../box'
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function dref () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
const entryCount = stream.readUint32()
|
||||
this.entryCount = entryCount
|
||||
const self = this
|
||||
// 暂时不支持离散视频,视频的部分内容由url指定
|
||||
for (let i = 0; i < entryCount; i++) {
|
||||
const box = new Box()
|
||||
self.subBox.push(box)
|
||||
box.read(stream)
|
||||
}
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function elst () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
const entries = []
|
||||
const entry_count = stream.readUint32()
|
||||
this.empty_duration = 0 // empty duration of the first edit list entry
|
||||
this.start_time = 0 // start time of the media
|
||||
let edit_start_index = 0
|
||||
this.entries = entries
|
||||
for (let i = 0; i < entry_count; i++) {
|
||||
const entry = {}
|
||||
entries.push(entry)
|
||||
if (this.version === 1) {
|
||||
entry.segment_duration = stream.readUint64()
|
||||
entry.media_time = stream.readUint64()
|
||||
} else {
|
||||
entry.segment_duration = stream.readUint32()
|
||||
entry.media_time = stream.readInt32()
|
||||
}
|
||||
entry.media_rate_integer = stream.readInt16()
|
||||
entry.media_rate_fraction = stream.readInt16()
|
||||
|
||||
if (i === 0 && entry.media_time === -1) {
|
||||
/* if empty, the first entry is the start time of the stream
|
||||
* relative to the presentation itself */
|
||||
this.empty_duration = entry.segment_duration
|
||||
edit_start_index = 1
|
||||
} else if (i === edit_start_index && entry.media_time >= 0) {
|
||||
this.start_time = entry.media_time
|
||||
}
|
||||
}
|
||||
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
// import Box from '../box'
|
||||
import Stream from '../stream'
|
||||
import MP4ESDescrTag from './MP4ESDescrTag'
|
||||
|
||||
export default function esds () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
const box = MP4ESDescrTag(stream)
|
||||
this.subBox.push(box)
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function ftyp () {
|
||||
let stream = new Stream(this.data)
|
||||
this.major_brand = String.fromCharCode(stream.readUint8(), stream.readUint8(), stream.readUint8(), stream.readUint8())
|
||||
this.minor_version = stream.readUint32()
|
||||
const compatibleBrands = []
|
||||
for (let i = 0, len = Math.floor((stream.buffer.byteLength - 8) / 4); i < len; i++) {
|
||||
compatibleBrands.push(String.fromCharCode(stream.readUint8(), stream.readUint8(), stream.readUint8(), stream.readUint8()))
|
||||
}
|
||||
this.compatible_brands = compatibleBrands
|
||||
stream = null
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function hdlr () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
stream.skip(4)
|
||||
this.handleType = `${String.fromCharCode(stream.readUint8())}${String.fromCharCode(stream.readUint8())}${String.fromCharCode(stream.readUint8())}${String.fromCharCode(stream.readUint8())}`
|
||||
stream.skip(12)
|
||||
const name = []
|
||||
while (stream.position < this.size - 8) {
|
||||
name.push(String.fromCharCode(stream.readUint8()))
|
||||
}
|
||||
this.name = name.join('')
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
import avc1 from './avc1.js'
|
||||
import avcC from './avcC.js'
|
||||
import btrt from './btrt.js'
|
||||
import co64 from './co64.js'
|
||||
import ctts from './ctts.js'
|
||||
import dref from './dref.js'
|
||||
import elst from './elst.js'
|
||||
import esds from './esds.js'
|
||||
import ftyp from './ftyp.js'
|
||||
import hdlr from './hdlr.js'
|
||||
// import hmhd from './hmhd.js'
|
||||
import iods from './iods.js'
|
||||
import mdat from './mdat.js'
|
||||
import mdhd from './mdhd.js'
|
||||
// import mfhd from './mfhd.js'
|
||||
import mp4a from './mp4a.js'
|
||||
import MP4DecConfigDescrTag from './MP4DecConfigDescrTag.js'
|
||||
import MP4DecSpecificDescrTag from './MP4DecSpecificDescrTag.js'
|
||||
import MP4ESDescrTag from './MP4ESDescrTag.js'
|
||||
import mvhd from './mvhd.js'
|
||||
// import nmhd from './nmhd.js'
|
||||
import pasp from './pasp.js'
|
||||
// import sbgp from './sbgp.js'
|
||||
// import sdtp from './sdtp.js'
|
||||
import SLConfigDescriptor from './SLConfigDescriptor.js'
|
||||
import smhd from './smhd.js'
|
||||
import stco from './stco.js'
|
||||
import stsc from './stsc.js'
|
||||
import stsd from './stsd.js'
|
||||
// import stsh from './stsh.js'
|
||||
import stss from './stss.js'
|
||||
import stsz from './stsz.js'
|
||||
import stts from './stts.js'
|
||||
// import stz2 from './stz2.js'
|
||||
// import tfhd from './tfhd.js'
|
||||
import tkhd from './tkhd.js'
|
||||
// import traf from './traf.js'
|
||||
// import trun from './trun.js'
|
||||
import udta from './udta.js'
|
||||
import url from './url.js'
|
||||
import vmhd from './vmhd.js'
|
||||
|
||||
export {
|
||||
avc1,
|
||||
avcC,
|
||||
btrt,
|
||||
co64,
|
||||
ctts,
|
||||
dref,
|
||||
elst,
|
||||
esds,
|
||||
ftyp,
|
||||
hdlr,
|
||||
// hmhd,
|
||||
iods,
|
||||
mdat,
|
||||
mdhd,
|
||||
// mfhd,
|
||||
mp4a,
|
||||
MP4DecConfigDescrTag,
|
||||
MP4DecSpecificDescrTag,
|
||||
MP4ESDescrTag,
|
||||
mvhd,
|
||||
// nmhd,
|
||||
pasp,
|
||||
// sbgp,
|
||||
// sdtp,
|
||||
SLConfigDescriptor,
|
||||
smhd,
|
||||
stco,
|
||||
stsc,
|
||||
stsd,
|
||||
// stsh,
|
||||
stss,
|
||||
stsz,
|
||||
stts,
|
||||
// stz2,
|
||||
// tfhd,
|
||||
tkhd,
|
||||
// traf,
|
||||
// trun,
|
||||
udta,
|
||||
url,
|
||||
vmhd
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function iods () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
const content = []
|
||||
const length = stream.buffer.byteLength
|
||||
while (stream.position < length) {
|
||||
content.push(stream.readUint8())
|
||||
}
|
||||
this.content = content
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export default function mdat () {
|
||||
delete this.subBox
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
import UTC from '../date'
|
||||
|
||||
export default function mdhd () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
if (this.version === 1) {
|
||||
this.create = stream.readUint64()
|
||||
this.modify = stream.readUint64()
|
||||
this.createTime = new UTC().setTime(this.create * 1000)
|
||||
this.modifyTime = new UTC().setTime(this.modify * 1000)
|
||||
this.timescale = stream.readUint32()
|
||||
this.duration = stream.readUint64()
|
||||
} else {
|
||||
this.create = stream.readUint32()
|
||||
this.modify = stream.readUint32()
|
||||
this.createTime = new UTC().setTime(this.create * 1000)
|
||||
this.modifyTime = new UTC().setTime(this.modify * 1000)
|
||||
this.timescale = stream.readUint32()
|
||||
this.duration = stream.readUint32()
|
||||
}
|
||||
this.language = stream.readUint16()
|
||||
stream.readUint16()
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import Box from '../box'
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function mp4a () {
|
||||
let stream = new Stream(this.data)
|
||||
stream.skip(6)
|
||||
this.dataReferenceIndex = stream.readUint16()
|
||||
stream.skip(8)
|
||||
this.channelCount = stream.readUint16()
|
||||
this.sampleSize = stream.readUint16()
|
||||
stream.skip(4)
|
||||
this.sampleRate = stream.readUint32() >> 16
|
||||
const box = new Box()
|
||||
box.readHeader(stream)
|
||||
this.subBox.push(box)
|
||||
box.readBody(stream)
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
import UTC from '../date'
|
||||
|
||||
export default function mvhd () {
|
||||
const stream = new Stream(this.data)
|
||||
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
this.create = stream.readUint32()
|
||||
this.modify = stream.readUint32()
|
||||
this.createTime = new UTC().setTime(this.create * 1000)
|
||||
this.modifyTime = new UTC().setTime(this.modify * 1000)
|
||||
this.timeScale = stream.readUint32()
|
||||
this.duration = stream.readUint32()
|
||||
this.rate = stream.readUint16() + '.' + stream.readUint16()
|
||||
this.volume = stream.readUint8() + '.' + stream.readUint8()
|
||||
// 越过保留的10字节
|
||||
Stream.readByte(stream.dataview, 8)
|
||||
Stream.readByte(stream.dataview, 2)
|
||||
// 视频转换矩阵
|
||||
const matrix = []
|
||||
for (let i = 0; i < 9; i++) {
|
||||
matrix.push(stream.readUint16() + '.' + stream.readUint16())
|
||||
}
|
||||
this.matrix = matrix
|
||||
Stream.readByte(stream.dataview, 24)
|
||||
this.nextTrackID = stream.readUint32()
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function pasp () {
|
||||
let stream = new Stream(this.data)
|
||||
this.content = stream.buffer.slice(0, this.size - 8)
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function smhd () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
this.balance = stream.readInt8() + '.' + stream.readInt8()
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function stco () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
this.count = stream.readUint32()
|
||||
const entries = []
|
||||
this.entries = entries
|
||||
for (let i = 0, count = this.count; i < count; i++) {
|
||||
entries.push(stream.readUint32())
|
||||
}
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function stsc () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
this.count = stream.readUint32()
|
||||
const entries = []
|
||||
this.entries = entries
|
||||
for (let i = 0, count = this.count; i < count; i++) {
|
||||
entries.push({
|
||||
first_chunk: stream.readUint32(),
|
||||
samples_per_chunk: stream.readUint32(),
|
||||
sample_desc_index: stream.readUint32()
|
||||
})
|
||||
}
|
||||
for (let i = 0, count = this.count, entry, preEntry; i < count - 1; i++) {
|
||||
entry = entries[i]
|
||||
preEntry = entries[i - 1]
|
||||
entry.chunk_count = entries[i + 1].first_chunk - entry.first_chunk
|
||||
entry.first_sample = i === 0 ? 1 : preEntry.first_sample + preEntry.chunk_count * preEntry.samples_per_chunk
|
||||
}
|
||||
if (this.count === 1) {
|
||||
const entry = entries[0]
|
||||
entry.first_sample = 1
|
||||
entry.chunk_count = 0
|
||||
} else if (this.count > 1) {
|
||||
const last = entries[this.count - 1]; const pre = entries[this.count - 2]
|
||||
last.first_sample = pre.first_sample + pre.chunk_count * pre.samples_per_chunk
|
||||
last.chunk_count = 0
|
||||
}
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import Box from '../box'
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function stsd () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
this.entryCount = stream.readUint32()
|
||||
const box = new Box()
|
||||
box.readHeader(stream)
|
||||
this.subBox.push(box)
|
||||
box.readBody(stream)
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function stss () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
this.count = stream.readUint32()
|
||||
const entries = []
|
||||
this.entries = entries
|
||||
for (let i = 0, count = this.count; i < count; i++) {
|
||||
entries.push(stream.readUint32())
|
||||
}
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function stsz () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
this.sampleSize = stream.readUint32()
|
||||
this.count = stream.readUint32()
|
||||
const entries = []
|
||||
this.entries = entries
|
||||
for (let i = 0, count = this.count; i < count; i++) {
|
||||
entries.push(stream.readUint32())
|
||||
}
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function stts () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3)
|
||||
this.count = stream.readUint32()
|
||||
const entry = []
|
||||
for (let i = 0, count = this.count; i < count; i++) {
|
||||
entry.push({
|
||||
sampleCount: stream.readUint32(),
|
||||
sampleDuration: stream.readUint32()
|
||||
})
|
||||
}
|
||||
this.entry = entry
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
import UTC from '../date'
|
||||
|
||||
export default function tkhd () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = Stream.readByte(stream.dataview, 3, 0)
|
||||
if (this.version === 1) {
|
||||
this.create = stream.readUint64()
|
||||
this.modify = stream.readUint64()
|
||||
this.createTime = new UTC().setTime(this.create * 1000)
|
||||
this.modifyTime = new UTC().setTime(this.modify * 1000)
|
||||
this.trackID = stream.readUint32()
|
||||
this.reserverd = stream.readUint32()
|
||||
this.duration = stream.readUint64()
|
||||
} else {
|
||||
this.create = stream.readUint32()
|
||||
this.modify = stream.readUint32()
|
||||
this.createTime = new UTC().setTime(this.create * 1000)
|
||||
this.modifyTime = new UTC().setTime(this.modify * 1000)
|
||||
this.trackID = stream.readUint32()
|
||||
this.reserverd = stream.readUint32()
|
||||
this.duration = stream.readUint32()
|
||||
}
|
||||
stream.readUint64()
|
||||
this.layer = stream.readInt16()
|
||||
this.alternate_group = stream.readInt16()
|
||||
this.volume = stream.readInt16() >> 8
|
||||
stream.readUint16()
|
||||
// 视频转换矩阵
|
||||
const matrix = []
|
||||
for (let i = 0; i < 9; i++) {
|
||||
matrix.push(stream.readUint16() + '.' + stream.readUint16())
|
||||
}
|
||||
this.matrix = matrix
|
||||
this.width = stream.readUint16() + '.' + stream.readUint16()
|
||||
this.height = stream.readUint16() + '.' + stream.readUint16()
|
||||
delete this.data
|
||||
delete this.subBox
|
||||
stream = null
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export default function udta () {
|
||||
delete this.subBox
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function url () {
|
||||
// Box['url '] = function () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = [stream.readUint8(), stream.readUint8(), stream.readUint8()]
|
||||
const location = []; const length = stream.buffer.byteLength
|
||||
while (stream.position < length) {
|
||||
location.push(stream.readUint8())
|
||||
}
|
||||
this.location = location
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import Stream from '../stream'
|
||||
|
||||
export default function vmhd () {
|
||||
let stream = new Stream(this.data)
|
||||
this.version = stream.readUint8()
|
||||
this.flag = [stream.readUint8(), stream.readUint8(), stream.readUint8()]
|
||||
this.graphicsmode = stream.readUint16()
|
||||
this.opcolor = [stream.readUint16(), stream.readUint16(), stream.readUint16()]
|
||||
delete this.subBox
|
||||
delete this.data
|
||||
stream = null
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
class UTC {
|
||||
constructor () {
|
||||
const time = new Date()
|
||||
time.setFullYear(1904)
|
||||
time.setMonth(0)
|
||||
time.setDate(1)
|
||||
time.setHours(0)
|
||||
time.setMinutes(0)
|
||||
time.setSeconds(0)
|
||||
this.time = time
|
||||
}
|
||||
|
||||
setTime (value) {
|
||||
this.time.setTime(this.time.getTime() + value * 1)
|
||||
return this.time.toLocaleString()
|
||||
}
|
||||
}
|
||||
|
||||
export default UTC
|
@ -1,44 +0,0 @@
|
||||
import Box from './box'
|
||||
import Concat from 'concat-typed-array'
|
||||
import Stream from './stream'
|
||||
import * as BoxParse from './boxParse'
|
||||
|
||||
Box.boxParse = BoxParse
|
||||
|
||||
class Parse {
|
||||
constructor (buffer) {
|
||||
this.buffer = null
|
||||
this.boxes = []
|
||||
this.nextBox = null
|
||||
this.start = 0
|
||||
const self = this
|
||||
if (self.buffer) {
|
||||
Concat(Uint8Array, self.buffer, buffer)
|
||||
} else {
|
||||
self.buffer = buffer
|
||||
}
|
||||
const bufferLength = buffer.byteLength
|
||||
buffer.position = 0
|
||||
const stream = new Stream(buffer)
|
||||
while (bufferLength - stream.position >= 8) {
|
||||
const box = new Box()
|
||||
box.readHeader(stream)
|
||||
if (box.size - 8 <= (bufferLength - stream.position)) {
|
||||
box.readBody(stream)
|
||||
self.boxes.push(box)
|
||||
} else {
|
||||
if (box.type === 'mdat') {
|
||||
box.readBody(stream)
|
||||
self.boxes.push(box)
|
||||
} else {
|
||||
self.nextBox = box
|
||||
stream.position -= 8
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
self.buffer = new Uint8Array(self.buffer.slice(stream.position))
|
||||
}
|
||||
}
|
||||
|
||||
export default Parse
|
@ -1,115 +0,0 @@
|
||||
import Errors from '../error'
|
||||
class Stream {
|
||||
constructor (buffer) {
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
this.buffer = buffer
|
||||
this.dataview = new DataView(buffer)
|
||||
this.dataview.position = 0
|
||||
} else {
|
||||
throw new Errors('parse', '', { line: 9, handle: '[Stream] constructor', msg: 'data is valid' })
|
||||
}
|
||||
}
|
||||
|
||||
set position (value) {
|
||||
this.dataview.position = value
|
||||
}
|
||||
|
||||
get position () {
|
||||
return this.dataview.position
|
||||
}
|
||||
|
||||
skip (count) {
|
||||
const loop = Math.floor(count / 4)
|
||||
const last = count % 4
|
||||
for (let i = 0; i < loop; i++) {
|
||||
Stream.readByte(this.dataview, 4)
|
||||
}
|
||||
if (last > 0) {
|
||||
Stream.readByte(this.dataview, last)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [readByte 从DataView中读取数据]
|
||||
* @param {DataView} buffer [DataView实例]
|
||||
* @param {Number} size [读取字节数]
|
||||
* @return {Number} [整数]
|
||||
*/
|
||||
static readByte (buffer, size, sign) {
|
||||
let res
|
||||
switch (size) {
|
||||
case 1:
|
||||
if (sign) {
|
||||
res = buffer.getInt8(buffer.position)
|
||||
} else {
|
||||
res = buffer.getUint8(buffer.position)
|
||||
}
|
||||
break
|
||||
case 2:
|
||||
if (sign) {
|
||||
res = buffer.getInt16(buffer.position)
|
||||
} else {
|
||||
res = buffer.getUint16(buffer.position)
|
||||
}
|
||||
break
|
||||
case 3:
|
||||
if (sign) {
|
||||
throw new Error('not supported for readByte 3')
|
||||
} else {
|
||||
res = buffer.getUint8(buffer.position) << 16
|
||||
res |= buffer.getUint8(buffer.position + 1) << 8
|
||||
res |= buffer.getUint8(buffer.position + 2)
|
||||
}
|
||||
break
|
||||
case 4:
|
||||
if (sign) {
|
||||
res = buffer.getInt32(buffer.position)
|
||||
} else {
|
||||
res = buffer.getUint32(buffer.position)
|
||||
}
|
||||
break
|
||||
case 8:
|
||||
if (sign) {
|
||||
throw new Errors('parse', '', { line: 73, handle: '[Stream] readByte', msg: 'not supported for readBody 8' })
|
||||
} else {
|
||||
res = buffer.getUint32(buffer.position) << 32
|
||||
res |= buffer.getUint32(buffer.position + 4)
|
||||
}
|
||||
break
|
||||
default:
|
||||
res = ''
|
||||
}
|
||||
buffer.position += size
|
||||
return res
|
||||
}
|
||||
|
||||
readUint8 () {
|
||||
return Stream.readByte(this.dataview, 1)
|
||||
}
|
||||
|
||||
readUint16 () {
|
||||
return Stream.readByte(this.dataview, 2)
|
||||
}
|
||||
|
||||
readUint32 () {
|
||||
return Stream.readByte(this.dataview, 4)
|
||||
}
|
||||
|
||||
readUint64 () {
|
||||
return Stream.readByte(this.dataview, 8)
|
||||
}
|
||||
|
||||
readInt8 () {
|
||||
return Stream.readByte(this.dataview, 1, true)
|
||||
}
|
||||
|
||||
readInt16 () {
|
||||
return Stream.readByte(this.dataview, 2, true)
|
||||
}
|
||||
|
||||
readInt32 () {
|
||||
return Stream.readByte(this.dataview, 4, true)
|
||||
}
|
||||
}
|
||||
|
||||
export default Stream
|
@ -1,13 +0,0 @@
|
||||
class Download {
|
||||
constructor (filename, content) {
|
||||
const aLink = document.createElement('a')
|
||||
const blob = new Blob([content])
|
||||
const evt = document.createEvent('MouseEvents')
|
||||
evt.initEvent('click', false, false)
|
||||
aLink.download = filename
|
||||
aLink.href = URL.createObjectURL(blob)
|
||||
aLink.dispatchEvent(evt)
|
||||
}
|
||||
}
|
||||
|
||||
export default Download
|
@ -1,170 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
const util = {}
|
||||
|
||||
/**
|
||||
* [使用递归查询指定type的box]
|
||||
* var mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
|
||||
* @param {Object} root [JSON对象]
|
||||
* @param {String} type [box的类型]
|
||||
* @return {Object} [box]
|
||||
*/
|
||||
util.findBox = function (root, type, result = []) {
|
||||
if (root.type !== type) {
|
||||
if (root && root.subBox) {
|
||||
const box = root.subBox.filter(item => item.type === type)
|
||||
if (box.length) {
|
||||
box.forEach(item => result.push(item))
|
||||
} else {
|
||||
root.subBox.forEach(item => util.findBox(item, type, result))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push(root)
|
||||
}
|
||||
result = [].concat(result)
|
||||
return result.length > 1 ? result : result[0]
|
||||
}
|
||||
|
||||
util.padStart = function (str, length, pad) {
|
||||
const charstr = String(pad); const len = length >> 0; let maxlen = Math.ceil(len / charstr.length)
|
||||
const chars = []; const r = String(str)
|
||||
while (maxlen--) {
|
||||
chars.push(charstr)
|
||||
}
|
||||
return chars.join('').substring(0, len - r.length) + r
|
||||
}
|
||||
|
||||
/**
|
||||
* [十进制转十六进制]
|
||||
* @param {Number} value [要转换的十进制数字]
|
||||
* @return {String} [十六进制]
|
||||
*/
|
||||
util.toHex = function (...value) {
|
||||
const hex = []
|
||||
value.forEach(item => {
|
||||
hex.push(util.padStart(Number(item).toString(16), 2, 0))
|
||||
})
|
||||
return hex
|
||||
}
|
||||
|
||||
/**
|
||||
* [求和计算]
|
||||
* @param {[type]} rst [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
util.sum = function (...rst) {
|
||||
let count = 0
|
||||
rst.forEach(item => { count += item })
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* [计算音视频数据在Mdat中的偏移量]
|
||||
* @param {Array} stsc [块偏移量]
|
||||
* @param {Number} sample_order [帧次序]
|
||||
* @return {Object} [块的位置和当前帧的偏移数]
|
||||
*/
|
||||
util.stscOffset = function (stsc, sample_order) {
|
||||
let chunk_index; let samples_offset = ''
|
||||
const chunk_start = stsc.entries.filter((item) => {
|
||||
return item.first_sample <= sample_order && sample_order < item.first_sample + item.chunk_count * item.samples_per_chunk
|
||||
})[0]
|
||||
if (!chunk_start) {
|
||||
const last_chunk = stsc.entries.pop()
|
||||
stsc.entries.push(last_chunk)
|
||||
const chunk_offset = Math.floor((sample_order - last_chunk.first_sample) / last_chunk.samples_per_chunk)
|
||||
const last_chunk_index = last_chunk.first_chunk + chunk_offset
|
||||
const last_chunk_first_sample = last_chunk.first_sample + last_chunk.samples_per_chunk * chunk_offset
|
||||
return {
|
||||
chunk_index: last_chunk_index,
|
||||
samples_offset: [last_chunk_first_sample, sample_order]
|
||||
}
|
||||
} else {
|
||||
const chunk_offset = Math.floor((sample_order - chunk_start.first_sample) / chunk_start.samples_per_chunk)
|
||||
const chunk_offset_sample = chunk_start.first_sample + chunk_offset * chunk_start.samples_per_chunk
|
||||
chunk_index = chunk_start.first_chunk + chunk_offset
|
||||
samples_offset = [chunk_offset_sample, sample_order]
|
||||
return {
|
||||
chunk_index: chunk_index,
|
||||
samples_offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
util.seekSampleOffset = function (stsc, stco, stsz, order, mdatStart) {
|
||||
const chunkOffset = util.stscOffset(stsc, order + 1)
|
||||
const result = stco.entries[chunkOffset.chunk_index - 1] + util.sum.apply(null, stsz.entries.slice(chunkOffset.samples_offset[0] - 1, chunkOffset.samples_offset[1] - 1)) - mdatStart
|
||||
if (result === undefined) {
|
||||
throw new Error(`result=${result},stco.length=${stco.entries.length},sum=${util.sum.apply(null, stsz.entries.slice(0, order))}`)
|
||||
} else if (result < 0) {
|
||||
throw new Error(`result=${result},stco.length=${stco.entries.length},sum=${util.sum.apply(null, stsz.entries.slice(0, order))}`)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
util.seekSampleTime = function (stts, ctts, order) {
|
||||
let time; let duration; let count = 0; let startTime = 0; let offset = 0
|
||||
stts.entry.every(item => {
|
||||
duration = item.sampleDuration
|
||||
if (order < count + item.sampleCount) {
|
||||
time = startTime + (order - count) * item.sampleDuration
|
||||
return false
|
||||
} else {
|
||||
count += item.sampleCount
|
||||
startTime += item.sampleCount * duration
|
||||
return true
|
||||
}
|
||||
})
|
||||
if (ctts) {
|
||||
let ct = 0
|
||||
ctts.entry.every(item => {
|
||||
ct += item.count
|
||||
if (order < ct) {
|
||||
offset = item.offset
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!time) {
|
||||
time = startTime + (order - count) * duration
|
||||
}
|
||||
return { time, duration, offset }
|
||||
}
|
||||
|
||||
util.seekOrderSampleByTime = function (stts, timeScale, time) {
|
||||
let startTime = 0; let order = 0; let count = 0; let itemDuration; let sampleDuration
|
||||
stts.every((item, idx) => {
|
||||
itemDuration = item.sampleCount * item.sampleDuration / timeScale
|
||||
if (time <= startTime + itemDuration) {
|
||||
order = count + Math.ceil((time - startTime) * timeScale / item.sampleDuration)
|
||||
startTime = startTime + Math.ceil((time - startTime) * timeScale / item.sampleDuration) * item.sampleDuration / timeScale
|
||||
sampleDuration = item.sampleCount
|
||||
return false
|
||||
} else {
|
||||
startTime += itemDuration
|
||||
count += item.sampleCount
|
||||
return true
|
||||
}
|
||||
})
|
||||
return { order, startTime, sampleDuration }
|
||||
}
|
||||
|
||||
util.sampleCount = function (stts) {
|
||||
let count = 0
|
||||
stts.forEach((item, idx) => {
|
||||
count += item.sampleCount
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
util.seekTrakDuration = function (trak, timeScale) {
|
||||
const stts = util.findBox(trak, 'stts'); let duration = 0
|
||||
stts.entry.forEach(item => {
|
||||
duration += item.sampleCount * item.sampleDuration
|
||||
})
|
||||
return Number(duration / timeScale).toFixed(4)
|
||||
}
|
||||
|
||||
export default util
|
@ -1,38 +0,0 @@
|
||||
import Stream from './stream'
|
||||
class Box {
|
||||
constructor (obj, output) {
|
||||
this.size = obj.size
|
||||
this.type = obj.type
|
||||
this.stream = new Stream(new Uint8Array(this.size - 8).buffer)
|
||||
this.data = obj
|
||||
this.output = output
|
||||
this.subBox = []
|
||||
const header = new Stream(new Uint8Array(8).buffer)
|
||||
header.writeUint32(obj.size)
|
||||
header.writeStr(obj.type)
|
||||
output.write(new Uint8Array(header.buffer))
|
||||
}
|
||||
|
||||
writeBody () {
|
||||
const self = this
|
||||
const data = this.data
|
||||
if (Box.containerBox.find(item => item === self.type)) {
|
||||
data.subBox.forEach(item => {
|
||||
const box = new Box(item, self.output)
|
||||
self.subBox.push(box)
|
||||
box.writeBody()
|
||||
})
|
||||
} else {
|
||||
const run = Box[self.type]
|
||||
if (run && run instanceof Function) {
|
||||
run.call(self, data, self.output)
|
||||
} else {
|
||||
throw new Error(`write:error,${self.type} write nothing`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box.containerBox = ['moov', 'trak', 'edts', 'mdia', 'minf', 'dinf', 'stbl', 'mvex', 'moof', 'mvex', 'traf', 'mfra']
|
||||
|
||||
export default Box
|
@ -1,21 +0,0 @@
|
||||
import Box from '../box'
|
||||
import Stream from '../stream'
|
||||
Box.MP4DecConfigDescrTag = function (data, output) {
|
||||
const stream = new Stream(new Uint8Array(data.size).buffer)
|
||||
stream.writeUint8(data.type)
|
||||
if (data.extend) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
stream.writeUint8(0x80)
|
||||
}
|
||||
stream.writeUint8(data.size - 5)
|
||||
} else {
|
||||
stream.writeUint8(data.size - 2)
|
||||
}
|
||||
stream.writeUint8(data.typeID)
|
||||
stream.writeUint8(data.streamUint)
|
||||
stream.writeUint24(data.bufferSize)
|
||||
stream.writeUint32(data.maximum)
|
||||
stream.writeUint32(data.average)
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
Box.MP4DecSpecificDescrTag(data.subBox[0], output)
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import Box from '../box'
|
||||
import Stream from '../stream'
|
||||
|
||||
Box.MP4DecSpecificDescrTag = function (data, output) {
|
||||
const stream = new Stream(new Uint8Array(data.size).buffer)
|
||||
stream.writeUint8(data.type)
|
||||
if (data.extend) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
stream.writeUint8(0x80)
|
||||
}
|
||||
stream.writeUint8(data.size - 5)
|
||||
} else {
|
||||
stream.writeUint8(data.size - 2)
|
||||
}
|
||||
data.EScode.forEach(item => {
|
||||
stream.writeUint8(Number(`0x${item}`))
|
||||
})
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import Box from '../box'
|
||||
import Stream from '../stream'
|
||||
Box.MP4ESDescrTag = function (data, output) {
|
||||
const stream = new Stream(new Uint8Array(data.size).buffer)
|
||||
stream.writeUint8(data.type)
|
||||
if (data.extend) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
stream.writeUint8(0x80)
|
||||
}
|
||||
stream.writeUint8(data.size - 5)
|
||||
} else {
|
||||
stream.writeUint8(data.size - 2)
|
||||
}
|
||||
stream.writeUint16(data.esID)
|
||||
stream.writeUint8(data.priority)
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
Box.MP4DecConfigDescrTag(data.subBox[0], output)
|
||||
Box.SLConfigDescriptor(data.subBox[1], output)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import Box from '../box'
|
||||
import Stream from '../stream'
|
||||
Box.SLConfigDescriptor = function (data, output) {
|
||||
const stream = new Stream(new Uint8Array(data.size).buffer)
|
||||
stream.writeUint8(data.type)
|
||||
if (data.extend) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
stream.writeUint8(0x80)
|
||||
}
|
||||
stream.writeUint8(data.size - 5)
|
||||
} else {
|
||||
stream.writeUint8(data.size - 2)
|
||||
}
|
||||
stream.writeUint8(data.SL)
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import Box from '../box'
|
||||
Box.avc1 = function (data, output) {
|
||||
const stream = this.stream
|
||||
stream.fill(6)
|
||||
stream.writeUint16(data.dataReferenceIndex)
|
||||
stream.fill(16)
|
||||
stream.writeUint16(data.width)
|
||||
stream.writeUint16(data.height)
|
||||
stream.writeUint32(data.horizresolution)
|
||||
stream.writeUint32(data.vertresolution)
|
||||
stream.fill(4)
|
||||
stream.writeUint16(data.frameCount)
|
||||
stream.fill(32)
|
||||
stream.writeUint16(data.depth)
|
||||
stream.fill(2)
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
const self = this
|
||||
data.subBox.forEach(item => {
|
||||
const box = new Box(item, self.output)
|
||||
self.subBox.push(box)
|
||||
box.writeBody()
|
||||
})
|
||||
let writeSize = stream.position
|
||||
self.subBox.forEach(item => {
|
||||
writeSize += item.stream.position + 8
|
||||
})
|
||||
if (writeSize !== data.size - 8) {
|
||||
throw new Error(`${data.type} box incomplete`)
|
||||
} else {
|
||||
self.outputSize = writeSize
|
||||
}
|
||||
delete this.data
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import Box from '../box'
|
||||
Box.avcC = function (data, output) {
|
||||
const stream = this.stream
|
||||
stream.writeUint8(data.configVersion)
|
||||
stream.writeUint8(data.profile)
|
||||
stream.writeUint8(data.profileCompatibility)
|
||||
stream.writeUint8(data.AVCLevelIndication)
|
||||
stream.writeUint8(data.lengthSizeMinusOne - 1)
|
||||
stream.writeUint8(data.numOfSequenceParameterSets)
|
||||
stream.writeUint16(data.sequenceLength)
|
||||
data.sequence.forEach(item => {
|
||||
stream.writeUint8(Number('0x' + item))
|
||||
})
|
||||
stream.writeUint8(data.ppsCount)
|
||||
stream.writeUint16(data.ppsLength)
|
||||
data.pps.forEach(item => {
|
||||
stream.writeUint8(Number('0x' + item))
|
||||
})
|
||||
if (data.last.length) {
|
||||
data.last.forEach(item => {
|
||||
stream.writeUint8(item)
|
||||
})
|
||||
}
|
||||
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
if (stream.position !== data.size - 8) {
|
||||
throw new Error(`${data.type} box incomplete`)
|
||||
} else {
|
||||
this.outputSize = stream.position
|
||||
}
|
||||
delete this.data
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import Box from '../box'
|
||||
Box.btrt = function (data, output) {
|
||||
const stream = this.stream
|
||||
stream.writeUint32(this.bufferSizeDB)
|
||||
stream.writeUint32(this.maxBitrate)
|
||||
stream.writeUint32(this.avgBitrate)
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
if (stream.position !== data.size - 8) {
|
||||
throw `${data.type} box incomplete`
|
||||
} else {
|
||||
this.outputSize = stream.position
|
||||
}
|
||||
delete this.data
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import Box from '../box'
|
||||
Box.co64 = function (data, output) {
|
||||
const stream = this.stream
|
||||
stream.writeUint8(data.version)
|
||||
stream.writeUint24(data.flag)
|
||||
stream.writeUint32(data.count)
|
||||
data.entries.forEach(item => {
|
||||
stream.writeUint64(item)
|
||||
})
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
if (stream.position !== data.size - 8) {
|
||||
throw new Error(`${data.type} box incomplete`)
|
||||
} else {
|
||||
this.outputSize = stream.position
|
||||
}
|
||||
delete this.data
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import Box from '../box'
|
||||
|
||||
Box.ctts = function (data, output) {
|
||||
const stream = this.stream
|
||||
stream.writeUint8(data.version)
|
||||
stream.writeUint24(data.flag)
|
||||
stream.writeUint32(data.entryCount)
|
||||
data.entry.forEach(item => {
|
||||
stream.writeUint32(item.count)
|
||||
stream.writeUint32(item.offset)
|
||||
})
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
if (stream.position !== data.size - 8) {
|
||||
throw new Error(`${data.type} box incomplete`)
|
||||
} else {
|
||||
this.outputSize = stream.position
|
||||
}
|
||||
delete this.data
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import Box from '../box'
|
||||
|
||||
Box.dref = function (data, output) {
|
||||
const stream = this.stream
|
||||
stream.writeUint8(data.version)
|
||||
stream.writeUint24(data.flag)
|
||||
stream.writeUint32(data.entryCount)
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
const self = this
|
||||
data.subBox.forEach(item => {
|
||||
const box = new Box(item, self.output)
|
||||
self.subBox.push(box)
|
||||
box.writeBody()
|
||||
})
|
||||
let writeSize = stream.position
|
||||
self.subBox.forEach(item => {
|
||||
writeSize += item.stream.position + 8
|
||||
})
|
||||
if (writeSize !== data.size - 8) {
|
||||
throw new Error(`${data.type} box incomplete`)
|
||||
} else {
|
||||
this.outputSize = stream.position
|
||||
}
|
||||
delete this.data
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import Box from '../box'
|
||||
|
||||
Box.elst = function (data, output) {
|
||||
const stream = this.stream
|
||||
stream.writeUint8(data.version)
|
||||
stream.writeUint24(data.flag)
|
||||
stream.writeUint32(data.entries.length)
|
||||
const version = data.version
|
||||
data.entries.forEach(item => {
|
||||
if (version === 1) {
|
||||
stream.writeUint64(item.segment_duration)
|
||||
stream.writeUint64(item.media_time)
|
||||
} else {
|
||||
stream.writeUint32(item.segment_duration)
|
||||
stream.writeInt32(item.media_time)
|
||||
}
|
||||
stream.writeInt16(item.media_rate_integer)
|
||||
stream.writeInt16(item.media_rate_fraction)
|
||||
})
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
if (stream.position !== data.size - 8) {
|
||||
throw new Error(`${data.type} box incomplete`)
|
||||
} else {
|
||||
this.outputSize = stream.position
|
||||
}
|
||||
delete this.data
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import Box from '../box'
|
||||
|
||||
Box.esds = function (data, output) {
|
||||
const stream = this.stream
|
||||
stream.writeUint8(data.version)
|
||||
stream.writeUint24(data.flag)
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
Box.MP4ESDescrTag(data.subBox[0], output)
|
||||
this.outputSize = data.size - 8
|
||||
delete this.data
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import Box from '../box'
|
||||
|
||||
Box.ftyp = function (data, output) {
|
||||
const stream = this.stream
|
||||
stream.writeStr(data.major_brand)
|
||||
stream.writeUint32(data.minor_version)
|
||||
data.compatible_brands.forEach(item => {
|
||||
stream.writeStr(item)
|
||||
})
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
if (stream.position !== data.size - 8) {
|
||||
throw `${data.type} box incomplete`
|
||||
} else {
|
||||
this.outputSize = stream.position
|
||||
}
|
||||
delete this.data
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import Box from '../box'
|
||||
Box.hdlr = function (data, output) {
|
||||
const stream = this.stream
|
||||
stream.writeUint8(data.version)
|
||||
stream.writeUint24(data.flag)
|
||||
stream.fill(4)
|
||||
stream.writeStr(data.handleType)
|
||||
stream.fill(12)
|
||||
stream.writeStr(data.name)
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
if (stream.position !== data.size - 8) {
|
||||
throw new Error(`${data.type} box incomplete`)
|
||||
}
|
||||
delete this.data
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import Box from '../box'
|
||||
Box.iods = function (data, output) {
|
||||
const stream = this.stream
|
||||
stream.writeUint8(data.version)
|
||||
stream.writeUint24(data.flag)
|
||||
data.content.forEach(item => {
|
||||
stream.writeUint8(item)
|
||||
})
|
||||
output.write(new Uint8Array(stream.buffer.slice(0, stream.position)))
|
||||
if (stream.position !== data.size - 8) {
|
||||
throw new Error(`${data.type} box incomplete`)
|
||||
}
|
||||
delete this.data
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user