├── .babelrc ├── plugins ├── clappr │ ├── .babelrc │ ├── example │ │ ├── style.css │ │ └── index.html │ ├── rollup.config.js │ ├── package.json │ └── src │ │ └── streamedian.clappr.js ├── videojs │ ├── .babelrc │ ├── example │ │ ├── style.css │ │ └── index.html │ ├── rollup.config.js │ ├── package.json │ └── src │ │ └── videojs.streamedian.js └── flowplayer │ ├── .babelrc │ ├── ws_rtsp_proxy.png │ ├── example │ ├── style.css │ └── index.html │ ├── rollup.config.js │ ├── package.json │ ├── LICENSE │ └── Readme.md ├── src ├── deps │ ├── bp_event.js │ ├── bp_logger.js │ ├── jsencrypt.js │ └── bp_statemachine.js ├── core │ ├── util │ │ ├── browser.js │ │ ├── url.js │ │ ├── exp-golomb.js │ │ ├── binary.js │ │ └── md5.js │ ├── elementary │ │ ├── AACFrame.js │ │ ├── NALU.js │ │ ├── AACAsm.js │ │ └── NALUAsm.js │ ├── defs.js │ ├── base_transport.js │ ├── parsers │ │ ├── aac.js │ │ ├── ts.js │ │ ├── m3u8.js │ │ └── pes.js │ ├── base_client.js │ └── remuxer │ │ ├── base.js │ │ ├── aac.js │ │ ├── h264.js │ │ └── remuxer.js ├── client │ ├── rtsp │ │ ├── rtp │ │ │ ├── payload │ │ │ │ └── parser.js │ │ │ ├── factory.js │ │ │ └── rtp.js │ │ ├── message.js │ │ ├── session.js │ │ └── stream.js │ └── hls │ │ ├── pes_avc.js │ │ ├── pes_aac.js │ │ ├── id3.js │ │ ├── adts.js │ │ └── client.js ├── media_error.js └── recorder.js ├── frameworks └── react │ ├── public │ ├── favicon.png │ ├── manifest.json │ └── index.html │ ├── .gitignore │ ├── src │ ├── index.js │ ├── ViewInfo.jsx │ ├── index.css │ ├── ViewTunnelClient.jsx │ ├── InputSource.jsx │ ├── InputBufferDuration.jsx │ ├── Player.jsx │ ├── VideoRateControl.jsx │ ├── StreamedianPlayer.jsx │ └── serviceWorker.js │ └── package.json ├── .npmignore ├── .editorconfig ├── example ├── style.css └── test.js ├── test.js ├── rollup.videojs.config.js ├── rollup.flow.config.js ├── rollup.clappr.config.js ├── test.clappr.js ├── package.json ├── streamedian.js ├── example.js ├── .gitignore ├── player.js ├── rollup.config.js └── Server(NodeJS) /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-rollup"] 3 | } 4 | -------------------------------------------------------------------------------- /plugins/clappr/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-rollup"] 3 | } 4 | -------------------------------------------------------------------------------- /plugins/videojs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-rollup"] 3 | } 4 | -------------------------------------------------------------------------------- /plugins/flowplayer/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-rollup", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /src/deps/bp_event.js: -------------------------------------------------------------------------------- 1 | // export * from 'bp_event'; 2 | export * from '../../node_modules/bp_event/event.js'; -------------------------------------------------------------------------------- /src/deps/bp_logger.js: -------------------------------------------------------------------------------- 1 | // export * from 'bp_logger'; 2 | export * from '../../node_modules/bp_logger/logger.js'; -------------------------------------------------------------------------------- /src/deps/jsencrypt.js: -------------------------------------------------------------------------------- 1 | // export * from 'jsencrypt'; 2 | export * from '../../node_modules/jsencrypt/src/jsencrypt.js'; -------------------------------------------------------------------------------- /frameworks/react/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Streamedian/html5_rtsp_player/HEAD/frameworks/react/public/favicon.png -------------------------------------------------------------------------------- /plugins/flowplayer/ws_rtsp_proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Streamedian/html5_rtsp_player/HEAD/plugins/flowplayer/ws_rtsp_proxy.png -------------------------------------------------------------------------------- /src/deps/bp_statemachine.js: -------------------------------------------------------------------------------- 1 | // export * from 'bp_statemachine'; 2 | export * from '../../node_modules/bp_statemachine/statemachine.js'; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | examples/test.bundle.js 4 | src/client/hls 5 | src/core/parsers/pes.js 6 | src/core/parsers/ts.js 7 | webpack.config.js -------------------------------------------------------------------------------- /src/core/util/browser.js: -------------------------------------------------------------------------------- 1 | export const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 2 | 3 | export const CPU_CORES = 1;//navigator.hardwareConcurrency || 3; -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/core/elementary/AACFrame.js: -------------------------------------------------------------------------------- 1 | export class AACFrame { 2 | 3 | constructor(data, dts, pts) { 4 | this.dts = dts; 5 | this.pts = pts ? pts : this.dts; 6 | 7 | this.data=data;//.subarray(offset); 8 | } 9 | 10 | getData() { 11 | return this.data; 12 | } 13 | 14 | getSize() { 15 | return this.data.byteLength; 16 | } 17 | } -------------------------------------------------------------------------------- /frameworks/react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Streamedian player", 3 | "name": "Streamedian RTSP player example", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frameworks/react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frameworks/react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import StreamedianPlayer from './StreamedianPlayer'; 4 | import './index.css'; 5 | 6 | const App = () => ( 7 |
8 | 9 | {/* */} 10 | 11 |
12 | ); 13 | 14 | render(, document.getElementById('root')); 15 | -------------------------------------------------------------------------------- /frameworks/react/src/ViewInfo.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ViewTunnelClient from "./ViewTunnelClient"; 3 | 4 | export default class ViewInfo extends React.Component { 5 | render() { 6 | return ( 7 |
8 | { Object.values(this.props.info.clients || []).map((client, index) => 9 | 10 | )} 11 |
12 | ); 13 | } 14 | } -------------------------------------------------------------------------------- /example/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | max-width: 720px; 3 | margin: 50px auto; 4 | } 5 | 6 | #test_video { 7 | width: 720px; 8 | } 9 | 10 | .controls { 11 | display: flex; 12 | justify-content: space-around; 13 | align-items: center; 14 | } 15 | input.input, .form-inline .input-group>.form-control { 16 | width: 300px; 17 | } 18 | .logs { 19 | overflow: auto; 20 | width: 720px; 21 | height: 150px; 22 | padding: 5px; 23 | border-top: solid 1px gray; 24 | border-bottom: solid 1px gray; 25 | } 26 | button { 27 | margin: 5px 28 | } -------------------------------------------------------------------------------- /frameworks/react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | max-width: 720px; 3 | margin: 50px auto; 4 | } 5 | 6 | video { 7 | width: 720px; 8 | } 9 | 10 | .controls { 11 | display: flex; 12 | justify-content: space-around; 13 | align-items: center; 14 | } 15 | 16 | input.input, .form-inline .input-group>.form-control { 17 | width: 300px; 18 | } 19 | 20 | .logs { 21 | overflow: auto; 22 | width: 720px; 23 | height: 150px; 24 | padding: 5px; 25 | border-top: solid 1px gray; 26 | border-bottom: solid 1px gray; 27 | } 28 | 29 | button { 30 | margin: 5px 31 | } -------------------------------------------------------------------------------- /plugins/clappr/example/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | max-width: 720px; 3 | margin: 50px auto; 4 | } 5 | 6 | #test_video { 7 | width: 720px; 8 | } 9 | 10 | .controls { 11 | display: flex; 12 | justify-content: space-around; 13 | align-items: center; 14 | } 15 | input.input, .form-inline .input-group>.form-control { 16 | width: 300px; 17 | } 18 | .logs { 19 | overflow: auto; 20 | width: 720px; 21 | height: 150px; 22 | padding: 5px; 23 | border-top: solid 1px gray; 24 | border-bottom: solid 1px gray; 25 | } 26 | button { 27 | margin: 5px 28 | } -------------------------------------------------------------------------------- /plugins/videojs/example/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | max-width: 720px; 3 | margin: 50px auto; 4 | } 5 | 6 | #test_video { 7 | width: 720px; 8 | } 9 | 10 | .controls { 11 | display: flex; 12 | justify-content: space-around; 13 | align-items: center; 14 | } 15 | input.input, .form-inline .input-group>.form-control { 16 | width: 300px; 17 | } 18 | .logs { 19 | overflow: auto; 20 | width: 720px; 21 | height: 150px; 22 | padding: 5px; 23 | border-top: solid 1px gray; 24 | border-bottom: solid 1px gray; 25 | } 26 | button { 27 | margin: 5px 28 | } -------------------------------------------------------------------------------- /plugins/flowplayer/example/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | max-width: 720px; 3 | margin: 50px auto; 4 | } 5 | 6 | #test_video { 7 | width: 720px; 8 | } 9 | 10 | .controls { 11 | display: flex; 12 | justify-content: space-around; 13 | align-items: center; 14 | } 15 | input.input, .form-inline .input-group>.form-control { 16 | width: 300px; 17 | } 18 | .logs { 19 | overflow: auto; 20 | width: 720px; 21 | height: 150px; 22 | padding: 5px; 23 | border-top: solid 1px gray; 24 | border-bottom: solid 1px gray; 25 | } 26 | button { 27 | margin: 5px 28 | } -------------------------------------------------------------------------------- /frameworks/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Streamedian-RTSP-player-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.3", 7 | "react-dom": "^16.8.3", 8 | "react-scripts": "2.1.5" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": [ 20 | ">0.2%", 21 | "not dead", 22 | "not ie <= 11", 23 | "not op_mini all" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/core/defs.js: -------------------------------------------------------------------------------- 1 | export class StreamType { 2 | static get VIDEO() {return 1;} 3 | static get AUDIO() {return 2;} 4 | 5 | static get map() {return { 6 | [StreamType.VIDEO]: 'video', 7 | [StreamType.AUDIO]: 'audio' 8 | }}; 9 | } 10 | 11 | export class PayloadType { 12 | static get H264() {return 1;} 13 | static get AAC() {return 2;} 14 | 15 | static get map() {return { 16 | [PayloadType.H264]: 'video', 17 | [PayloadType.AAC]: 'audio' 18 | }}; 19 | 20 | static get string_map() {return { 21 | H264: PayloadType.H264, 22 | AAC: PayloadType.AAC, 23 | 'MP4A-LATM': PayloadType.AAC, 24 | 'MPEG4-GENERIC': PayloadType.AAC 25 | }} 26 | } -------------------------------------------------------------------------------- /frameworks/react/src/ViewTunnelClient.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class ViewTunnelClient extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.setSource = this.setSource.bind(this); 8 | } 9 | 10 | setSource(event) { 11 | let source = event.target.dataset['src']; 12 | this.props.onClick(source); 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 | { this.props.client.map((source, index) => 19 | 25 | )} 26 |
27 | ); 28 | } 29 | } -------------------------------------------------------------------------------- /plugins/clappr/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import alias from 'rollup-plugin-alias'; 3 | import uglify from 'rollup-plugin-uglify'; 4 | 5 | const path = require('path'); 6 | 7 | export default { 8 | entry: path.join(__dirname, 'src/streamedian.clappr.js'), 9 | targets: [ 10 | { 11 | dest: path.join(__dirname, 'example/streamedian.clappr.min.js'), 12 | format: 'iife', 13 | name: 'streamedian.clappr' 14 | } 15 | ], 16 | sourceMap: true, 17 | plugins: [ 18 | babel({ 19 | // exclude: 'node_modules/**', 20 | }), 21 | alias({ 22 | streamedian: path.join(__dirname,'../../src') 23 | }) 24 | ] 25 | } -------------------------------------------------------------------------------- /plugins/videojs/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import alias from 'rollup-plugin-alias'; 3 | import uglify from 'rollup-plugin-uglify'; 4 | 5 | const path = require('path'); 6 | 7 | export default { 8 | entry: path.join(__dirname, 'src/videojs.streamedian.js'), 9 | targets: [ 10 | { 11 | dest: path.join(__dirname, 'example/streamedian.videojs.min.js'), 12 | format: 'iife', 13 | name: 'streamedian.videojs' 14 | } 15 | ], 16 | sourceMap: true, 17 | plugins: [ 18 | babel({ 19 | // exclude: 'node_modules/**', 20 | }), 21 | alias({ 22 | streamedian: path.join(__dirname,'../../src') 23 | }) 24 | ] 25 | } -------------------------------------------------------------------------------- /plugins/flowplayer/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import buble from 'rollup-plugin-buble'; 3 | import alias from 'rollup-plugin-alias'; 4 | 5 | const path = require('path'); 6 | 7 | export default { 8 | entry: path.join(__dirname, '/src/flowplayer.streamedian.js'), 9 | targets: [ 10 | {dest: path.join(__dirname, 'example/streamedian.flowplayer.min.js'), format: 'umd'} 11 | ], 12 | sourceMap: true, 13 | plugins: [ 14 | // babel({ 15 | //exclude: 'node_modules/**' 16 | // }), 17 | alias({ 18 | bp_logger: path.join(__dirname, 'node_modules/bp_logger/logger'), 19 | streamedian: path.join(__dirname, '../../src') 20 | }) 21 | ] 22 | 23 | } 24 | -------------------------------------------------------------------------------- /frameworks/react/src/InputSource.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class InputSource extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.changeSource = this.changeSource.bind(this); 8 | this.setSource = this.setSource.bind(this); 9 | } 10 | 11 | changeSource(event) { 12 | this.setState({source: event.target.value}); 13 | } 14 | 15 | setSource(event) { 16 | if (this.state) { 17 | this.props.onClick(this.state.source); 18 | } 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 | 25 | 26 |

Enter your rtsp link to the stream, for example: "rtsp://192.168.1.1:554/h264"

27 |
28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /example/test.js: -------------------------------------------------------------------------------- 1 | import {LogLevel, getTagged, setDefaultLogLevel} from 'bp_logger'; 2 | import * as wsp from 'wsp/player'; 3 | import WebsocketTransport from 'wsp/transport/websocket'; 4 | import RTSPClient from 'wsp/client/rtsp/client'; 5 | import {isSafari} from "wsp/core/util/browser"; 6 | 7 | setDefaultLogLevel(LogLevel.Debug); 8 | getTagged("transport:ws").setLevel(LogLevel.Error); 9 | getTagged("client:rtsp").setLevel(LogLevel.Error); 10 | 11 | let wsTransport = { 12 | constructor: WebsocketTransport, 13 | options: { 14 | socket: "ws://127.0.0.1:8080/ws/" 15 | } 16 | }; 17 | 18 | let p = new wsp.WSPlayer('test_video', { 19 | // url: `${STREAM_UNIX}${STREAM_URL}`, 20 | // type: wsp.StreamType.HLS, 21 | modules: [ 22 | { 23 | client: RTSPClient, 24 | transport: wsTransport 25 | }, 26 | ] 27 | }); 28 | -------------------------------------------------------------------------------- /frameworks/react/src/InputBufferDuration.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class InputBufferDuration extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.changeDuration = this.changeDuration.bind(this); 7 | } 8 | 9 | changeDuration(event) { 10 | this.props.onChange(event.target.value); 11 | } 12 | 13 | render() { 14 | return ( 15 |
16 |
17 | 25 | {this.props.duration}sec. 26 |

Change buffer duration

27 |
28 |
29 | ); 30 | } 31 | } -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import {LogLevel, getTagged, setDefaultLogLevel} from './node_modules/bp_logger/logger'; 2 | import * as streamedian from './src/player.js'; 3 | import WebsocketTransport from './src/transport/websocket.js'; 4 | import RTSPClient from './src/client/rtsp/client.js'; 5 | import HLSClient from './src/client/hls/client.js'; 6 | import {isSafari} from "./src/core/util/browser.js"; 7 | 8 | setDefaultLogLevel(LogLevel.Debug); 9 | getTagged("transport:ws").setLevel(LogLevel.Error); 10 | getTagged("client:rtsp").setLevel(LogLevel.Error); 11 | 12 | let wsTransport = { 13 | constructor: WebsocketTransport, 14 | options: { 15 | socket: "wss://specforge.com/ws/" 16 | } 17 | }; 18 | 19 | let p = new streamedian.WSPlayer('test_video', { 20 | // url: `${STREAM_UNIX}${STREAM_URL}`, 21 | // type: wsp.StreamType.HLS, 22 | modules: [ 23 | { 24 | client: RTSPClient, 25 | transport: wsTransport 26 | }, 27 | { 28 | client: HLSClient, 29 | transport: wsTransport 30 | } 31 | ] 32 | }); -------------------------------------------------------------------------------- /rollup.videojs.config.js: -------------------------------------------------------------------------------- 1 | // import babel from 'rollup-plugin-babel'; 2 | import buble from 'rollup-plugin-buble'; 3 | import alias from 'rollup-plugin-alias'; 4 | 5 | const path = require('path'); 6 | 7 | export default { 8 | entry: path.join(__dirname, '/plugins/videojs/src/videojs.streamedian.js'), 9 | targets: [ 10 | {dest: path.join(__dirname, 'dist/test.videojs.bundle.js'), format: 'umd'} 11 | ], 12 | sourceMap: true, 13 | plugins: [ 14 | //buble({ 15 | //exclude: 'node_modules/**' 16 | //}), 17 | alias({ 18 | bp_logger: path.join(__dirname,'node_modules/bp_logger/logger'), 19 | bp_event: path.join(__dirname,'node_modules/bp_event/event'), 20 | bp_statemachine: path.join(__dirname,'node_modules/bp_statemachine/statemachine'), 21 | jsencrypt: path.join(__dirname,'node_modules/jsencrypt/src/jsencrypt'), 22 | rtsp: path.join(__dirname,'node_modules/html5_rtsp_player/src'), 23 | streamedian: path.join(__dirname,'src') 24 | }) 25 | ] 26 | 27 | } -------------------------------------------------------------------------------- /rollup.flow.config.js: -------------------------------------------------------------------------------- 1 | // import babel from 'rollup-plugin-babel'; 2 | import buble from 'rollup-plugin-buble'; 3 | import alias from 'rollup-plugin-alias'; 4 | 5 | const path = require('path'); 6 | 7 | export default { 8 | entry: path.join(__dirname, '/plugins/flowplayer/flowplayer.streamedian.js'), 9 | targets: [ 10 | {dest: path.join(__dirname, 'dist/test.flowplayer.bundle.js'), format: 'umd'} 11 | ], 12 | sourceMap: true, 13 | plugins: [ 14 | //buble({ 15 | //exclude: 'node_modules/**' 16 | //}), 17 | alias({ 18 | bp_logger: path.join(__dirname,'node_modules/bp_logger/logger'), 19 | bp_event: path.join(__dirname,'node_modules/bp_event/event'), 20 | bp_statemachine: path.join(__dirname,'node_modules/bp_statemachine/statemachine'), 21 | jsencrypt: path.join(__dirname,'node_modules/jsencrypt/src/jsencrypt'), 22 | rtsp: path.join(__dirname,'node_modules/html5_rtsp_player/src'), 23 | streamedian: path.join(__dirname,'src') 24 | }) 25 | ] 26 | 27 | } -------------------------------------------------------------------------------- /rollup.clappr.config.js: -------------------------------------------------------------------------------- 1 | // import babel from 'rollup-plugin-babel'; 2 | import buble from 'rollup-plugin-buble'; 3 | import alias from 'rollup-plugin-alias'; 4 | 5 | const path = require('path'); 6 | 7 | export default { 8 | entry: path.join(__dirname, 'test.clappr.js'), 9 | targets: [ 10 | {dest: path.join(__dirname, 'dist/test.clappr.bundle.js'), format: 'umd'} 11 | ], 12 | sourceMap: true, 13 | plugins: [ 14 | //buble({ 15 | //exclude: 'node_modules/**' 16 | //}), 17 | alias({ 18 | bp_logger: path.join(__dirname,'node_modules/bp_logger/logger'), 19 | bp_event: path.join(__dirname,'node_modules/bp_event/event'), 20 | bp_statemachine: path.join(__dirname,'node_modules/bp_statemachine/statemachine'), 21 | jsencrypt: path.join(__dirname,'node_modules/jsencrypt/src/jsencrypt'), 22 | rtsp: path.join(__dirname,'node_modules/html5_rtsp_player/src'), 23 | clappr_player: path.join(__dirname,'node_modules/clappr/src'), 24 | streamedian: path.join(__dirname,'src') 25 | }) 26 | ] 27 | 28 | } -------------------------------------------------------------------------------- /src/client/rtsp/rtp/payload/parser.js: -------------------------------------------------------------------------------- 1 | import {NALUAsm} from "../../../../core/elementary/NALUAsm.js"; 2 | import {AACAsm} from "../../../../core/elementary/AACAsm.js"; 3 | 4 | export class RTPPayloadParser { 5 | 6 | constructor() { 7 | this.h264parser = new RTPH264Parser(); 8 | this.aacparser = new RTPAACParser(); 9 | } 10 | 11 | parse(rtp) { 12 | if (rtp.media.type=='video') { 13 | return this.h264parser.parse(rtp); 14 | } else if (rtp.media.type == 'audio') { 15 | return this.aacparser.parse(rtp); 16 | } 17 | return null; 18 | } 19 | } 20 | 21 | class RTPH264Parser { 22 | constructor() { 23 | this.naluasm = new NALUAsm(); 24 | } 25 | 26 | parse(rtp) { 27 | return this.naluasm.onNALUFragment(rtp.getPayload(), rtp.getTimestampMS()); 28 | } 29 | } 30 | 31 | class RTPAACParser { 32 | 33 | constructor() { 34 | this.scale = 1; 35 | this.asm = new AACAsm(); 36 | } 37 | 38 | setConfig(conf) { 39 | this.asm.config = conf; 40 | } 41 | 42 | parse(rtp) { 43 | return this.asm.onAACFragment(rtp); 44 | } 45 | } -------------------------------------------------------------------------------- /plugins/flowplayer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamedian", 3 | "version": "1.8.3", 4 | "description": "HTML5 MSE RTSP player over websockets", 5 | "license": "Apache2", 6 | "author": "SpecForge (http://specforge.com/)", 7 | "homepage": "http://streamedian.com/", 8 | "keywords": [ 9 | "html5", 10 | "rtsp", 11 | "mse", 12 | "streaming" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/SpecForge/streamedian.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/SpecForge/streamedian/issues", 20 | "email": "specforge@gmail.com" 21 | }, 22 | "devDependencies": { 23 | "babel-preset-es2015-rollup": "^3.0.0", 24 | "rollup": "^0.33.2", 25 | "rollup-plugin-alias": "^1.4.0", 26 | "rollup-plugin-async": "^1.2.0", 27 | "rollup-plugin-babel": "^2.7.1", 28 | "rollup-plugin-buble": "^0.17.0", 29 | "rollup-plugin-node-resolve": "^1.7.3", 30 | "rollup-plugin-uglify": "^2.0.1" 31 | }, 32 | "dependencies": { 33 | "bp_event": "^1.1.2", 34 | "bp_logger": "^1.0.3", 35 | "bp_statemachine": "^1.0.13", 36 | "jsencrypt": "git+https://github.com/kreopt/jsencrypt.git" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugins/videojs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs_streamedian_plugin", 3 | "version": "1.8.3", 4 | "description": "rtsp/hls plugin for videojs", 5 | "license": "Apache2", 6 | "author": "SpecForge (http://specforge.com/)", 7 | "homepage": "http://streamedian.com/", 8 | "directories": { 9 | "example": "examples" 10 | }, 11 | "dependencies": { 12 | "video.js": "^7.2.0", 13 | "bp_event": "^1.1.2", 14 | "bp_logger": "^1.0.3", 15 | "bp_statemachine": "^1.0.13", 16 | "jsencrypt": "git+https://github.com/kreopt/jsencrypt.git" 17 | }, 18 | "devDependencies": { 19 | "babel-preset-es2015-rollup": "^3.0.0", 20 | "bp_logger": "^1.1.1", 21 | "rollup": "^0.33.0", 22 | "rollup-plugin-alias": "^1.2.0", 23 | "rollup-plugin-babel": "^2.6.1", 24 | "rollup-plugin-buble": "^0.12.1", 25 | "rollup-plugin-butternut": "^0.1.0", 26 | "rollup-plugin-node-resolve": "^1.7.1" 27 | }, 28 | "scripts": { 29 | "test": "echo \"Error: no test specified\" && exit 1" 30 | }, 31 | "keywords": [ 32 | "hlsjs", 33 | "rtspjs", 34 | "rtsp.js", 35 | "videojs", 36 | "video.js", 37 | "videojsrtsp", 38 | "videojshls" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /plugins/clappr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clappr.streamedian", 3 | "version": "1.8.3", 4 | "description": "HTML5 MSE RTSP player over websockets", 5 | "license": "Apache2", 6 | "author": "SpecForge (http://specforge.com/)", 7 | "homepage": "http://streamedian.com/", 8 | "keywords": [ 9 | "html5", 10 | "rtsp", 11 | "mse", 12 | "streaming" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/SpecForge/streamedian.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/SpecForge/streamedian/issues", 20 | "email": "specforge@gmail.com" 21 | }, 22 | "devDependencies": { 23 | "babel-preset-es2015-rollup": "^3.0.0", 24 | "rollup": "^0.33.2", 25 | "rollup-plugin-alias": "^1.4.0", 26 | "rollup-plugin-async": "^1.2.0", 27 | "rollup-plugin-babel": "^2.7.1", 28 | "rollup-plugin-buble": "^0.17.0", 29 | "rollup-plugin-node-resolve": "^1.7.3", 30 | "rollup-plugin-uglify": "^2.0.1" 31 | }, 32 | "dependencies": { 33 | "clappr": "latest", 34 | "bp_event": "^1.1.2", 35 | "bp_logger": "^1.0.3", 36 | "bp_statemachine": "^1.0.13", 37 | "jsencrypt": "git+https://github.com/kreopt/jsencrypt.git" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frameworks/react/src/Player.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class Player extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | source: this.props.src, 8 | info: '', 9 | }; 10 | 11 | this.player = null; 12 | this.changeSource = this.changeSource.bind(this); 13 | this.restartPlayer = this.restartPlayer.bind(this); 14 | this.changeBufferDuration = this.changeBufferDuration.bind(this); 15 | } 16 | 17 | componentDidMount() { 18 | this.player = window.Streamedian.player(this.props.id, this.props.options); 19 | } 20 | 21 | changeSource(src) { 22 | this.setState({source: src}, () => { 23 | this.restartPlayer(); 24 | }); 25 | } 26 | 27 | changeBufferDuration(duration) { 28 | this.player.bufferDuration = duration; 29 | this.setState({bufferDuration: duration}); 30 | } 31 | 32 | restart() { 33 | this.player.player.src = this.state.source; 34 | this.player.destroy(); 35 | this.player = null; 36 | this.player = window.Streamedian.player(this.props.id, this.props.options); 37 | this.changeBufferDuration(this.player.bufferDuration); 38 | } 39 | 40 | render() { 41 | return ( 42 |
43 | {this.props.children} 44 |
45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/client/rtsp/rtp/factory.js: -------------------------------------------------------------------------------- 1 | import RTP from './rtp.js'; 2 | 3 | export default class RTPFactory { 4 | constructor(sdp) { 5 | this.tsOffsets={}; 6 | for (let pay in sdp.media) { 7 | for (let pt of sdp.media[pay].fmt) { 8 | this.tsOffsets[pt] = {last: 0, overflow: 0}; 9 | } 10 | } 11 | } 12 | 13 | build(pkt/*uint8array*/, sdp) { 14 | let rtp = new RTP(pkt, sdp); 15 | 16 | let tsOffset = this.tsOffsets[rtp.pt]; 17 | if (tsOffset) { 18 | rtp.timestamp += tsOffset.overflow; 19 | if (tsOffset.last && Math.abs(rtp.timestamp - tsOffset.last) > 0x7fffffff) { 20 | console.log(`\nlast ts: ${tsOffset.last}\n 21 | new ts: ${rtp.timestamp}\n 22 | new ts adjusted: ${rtp.timestamp+0xffffffff}\n 23 | last overflow: ${tsOffset.overflow}\n 24 | new overflow: ${tsOffset.overflow+0xffffffff}\n 25 | `); 26 | tsOffset.overflow += 0xffffffff; 27 | rtp.timestamp += 0xffffffff; 28 | } 29 | /*if (rtp.timestamp>0xffffffff) { 30 | console.log(`ts: ${rtp.timestamp}, seq: ${rtp.sequence}`); 31 | }*/ 32 | tsOffset.last = rtp.timestamp; 33 | } 34 | 35 | return rtp; 36 | } 37 | } -------------------------------------------------------------------------------- /plugins/flowplayer/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Streamedian 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | src/iso-bmff/mp4-generator.js src/h264/exp-golomb.js implementation in this project 16 | are derived from the hls.js library (https://github.com/dailymotion/hls.js) 17 | 18 | That work is also covered by the Apache 2 License, following copyright: 19 | Copyright (c) 2013-2015 Brightcove 20 | 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | THE SOFTWARE. -------------------------------------------------------------------------------- /test.clappr.js: -------------------------------------------------------------------------------- 1 | import {LogLevel, getTagged, setDefaultLogLevel} from 'bp_logger'; 2 | import {isSafari} from "streamedian/core/util/browser"; 3 | import {ClapprLive} from "./plugins/clappr_plugin/src/clappr"; 4 | 5 | setDefaultLogLevel(LogLevel.Debug); 6 | getTagged("transport:ws").setLevel(LogLevel.Error); 7 | getTagged("client:rtsp").setLevel(LogLevel.Error); 8 | 9 | // let wsTransport = { 10 | // constructor: WebsocketTransport, 11 | // options: { 12 | // socket: "ws://127.0.0.1:8080/ws/" 13 | // } 14 | // }; 15 | // 16 | // let p = new wsp.WSPlayer('test_video', { 17 | // // url: `${STREAM_UNIX}${STREAM_URL}`, 18 | // // type: wsp.StreamType.HLS, 19 | // modules: [ 20 | // { 21 | // client: RTSPClient, 22 | // transport: wsTransport 23 | // }, 24 | // { 25 | // client: HLSClient, 26 | // transport: wsTransport 27 | // } 28 | // ] 29 | // }); 30 | window.onload = function() { 31 | var player = new Clappr.Player({ 32 | parentId: '#clappr_video', 33 | plugins: { 34 | playback: [ClapprLive] 35 | }, 36 | // poster:'https://cloudfront-prod.bitmovin.com/wp-content/uploads/2015/08/apple_hls_6401.jpg', 37 | rtsp_config:{websocket_url:"ws://127.0.0.1:8080/ws/"}, 38 | sources: [{source:'rtsp://192.168.10.161/H264_LOW', mimeType:'application/x-rtsp'}], 39 | mediacontrol: {seekbar: "#E113D3", buttons: "#66B2FF"}, 40 | autoPlay: true 41 | }); 42 | } -------------------------------------------------------------------------------- /src/client/rtsp/message.js: -------------------------------------------------------------------------------- 1 | export class RTSPMessage { 2 | static get RTSP_1_0() {return "RTSP/1.0";} 3 | 4 | constructor(_rtsp_version) { 5 | this.version = _rtsp_version; 6 | } 7 | 8 | build(_cmd, _host, _params={}, _payload=null) { 9 | let requestString = `${_cmd} ${_host} ${this.version}\r\n`; 10 | for (let param in _params) { 11 | requestString+=`${param}: ${_params[param]}\r\n` 12 | } 13 | // TODO: binary payload 14 | if (_payload) { 15 | requestString+=`Content-Length: ${_payload.length}\r\n` 16 | } 17 | requestString+='\r\n'; 18 | if (_payload) { 19 | requestString+=_payload; 20 | } 21 | return requestString; 22 | } 23 | 24 | parse(_data) { 25 | let lines = _data.split('\r\n'); 26 | let parsed = { 27 | headers:{}, 28 | body:null, 29 | code: 0, 30 | statusLine: '' 31 | }; 32 | 33 | let match; 34 | [match, parsed.code, parsed.statusLine] = lines[0].match(new RegExp(`${this.version}[ ]+([0-9]{3})[ ]+(.*)`)); 35 | parsed.code = Number(parsed.code); 36 | let lineIdx = 1; 37 | 38 | while (lines[lineIdx]) { 39 | let [k,v] = lines[lineIdx].split(/:(.+)/); 40 | parsed.headers[k.toLowerCase()] = v.trim(); 41 | lineIdx++; 42 | } 43 | 44 | parsed.body = lines.slice(lineIdx).join('\n\r'); 45 | 46 | return parsed; 47 | } 48 | 49 | } 50 | 51 | export const MessageBuilder = new RTSPMessage(RTSPMessage.RTSP_1_0); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamedian", 3 | "version": "1.8.3", 4 | "description": "HTML5 MSE RTSP player over websockets", 5 | "license": "Apache2", 6 | "author": "Streamedian (http://streamedian.com/)", 7 | "homepage": "https://streamedian.com/", 8 | "keywords": [ 9 | "html5", 10 | "rtsp", 11 | "mse", 12 | "streaming" 13 | ], 14 | "contributors": [ 15 | { 16 | "name": "Victor Grenke", 17 | "email": "viktor.grenke@gmail.com", 18 | "url": "https://streamedian.com/" 19 | }, 20 | { 21 | "name": "Igor Shakirov", 22 | "email": "igor.shakirov@specforge.com", 23 | "url": "https://specforge.com/" 24 | } 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/Streamedian/html5_rtsp_player.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/Streamedian/html5_rtsp_player/issues", 32 | "email": "specforge@gmail.com" 33 | }, 34 | "devDependencies": { 35 | "babel-preset-es2015-rollup": "^3.0.0", 36 | "rollup": "^0.33.2", 37 | "rollup-plugin-alias": "^1.4.0", 38 | "rollup-plugin-async": "^1.2.0", 39 | "rollup-plugin-babel": "^2.7.1", 40 | "rollup-plugin-buble": "^0.17.0", 41 | "rollup-plugin-node-resolve": "^1.7.3", 42 | "rollup-plugin-uglify": "^2.0.1" 43 | }, 44 | "dependencies": { 45 | "babel-polyfill": "^6.26.0", 46 | "bp_event": "^1.1.2", 47 | "bp_logger": "^1.0.3", 48 | "bp_statemachine": "^1.0.13", 49 | "jsencrypt": "git+https://github.com/kreopt/jsencrypt.git" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/media_error.js: -------------------------------------------------------------------------------- 1 | function SMediaError(data) { 2 | if(data instanceof SMediaError) { 3 | return data; 4 | } 5 | 6 | if (typeof data === 'number') { 7 | this.code = data; 8 | } else if (typeof value === 'string') { 9 | this.message = data; 10 | } 11 | 12 | if (!this.message) { 13 | this.message = SMediaError.defaultMessages[this.code] || ''; 14 | } 15 | } 16 | 17 | SMediaError.prototype.code = 0; 18 | SMediaError.prototype.message = ''; 19 | 20 | SMediaError.errorTypes = [ 21 | 'MEDIA_ERR_CUSTOM', 22 | 'MEDIA_ERR_ABORTED', 23 | 'MEDIA_ERR_NETWORK', 24 | 'MEDIA_ERR_DECODE', 25 | 'MEDIA_ERR_SRC_NOT_SUPPORTED', 26 | 'MEDIA_ERR_ENCRYPTED', 27 | 'MEDIA_ERR_TRANSPORT' 28 | ]; 29 | 30 | SMediaError.defaultMessages = { 31 | 1: 'The fetching of the associated resource was aborted by the user\'s request.', 32 | 2: 'Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.', 33 | 3: 'Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.', 34 | 4: 'The associated resource or media provider object (such as a MediaStream) has been found to be unsuitable.', 35 | 5: 'The media is encrypted and we do not have the keys to decrypt it.', 36 | 6: 'Transport error' 37 | }; 38 | 39 | for (let errIndex = 0; errIndex < SMediaError.errorTypes.length; errIndex++) { 40 | SMediaError[SMediaError.errorTypes[errIndex]] = errIndex; 41 | SMediaError.prototype[SMediaError.errorTypes[errIndex]] = errIndex; 42 | } 43 | export default SMediaError; -------------------------------------------------------------------------------- /streamedian.js: -------------------------------------------------------------------------------- 1 | import {LogLevel, getTagged, setDefaultLogLevel} from 'bp_logger'; 2 | import * as streamedian from 'streamedian/player.js'; 3 | import WebsocketTransport from 'streamedian/transport/websocket.js'; 4 | import RTSPClient from 'streamedian/client/rtsp/client.js'; 5 | import {isSafari} from "streamedian/core/util/browser.js"; 6 | 7 | 8 | setDefaultLogLevel(LogLevel.Error); 9 | 10 | 11 | window.Streamedian = { 12 | logger(tag) { 13 | return getTagged(tag) 14 | }, 15 | player(node, opts) { 16 | if (!opts.socket) { 17 | throw new Error("socket parameter is not set"); 18 | } 19 | let _options = { 20 | modules: [ 21 | { 22 | client: RTSPClient, 23 | transport: { 24 | constructor: WebsocketTransport, 25 | options: { 26 | socket: opts.socket 27 | } 28 | } 29 | } 30 | ], 31 | errorHandler(e) { 32 | alert(`Failed to start player: ${e.message}`); 33 | }, 34 | queryCredentials(client) { 35 | return new Promise((resolve, reject) => { 36 | let c = prompt('input credentials in format user:password'); 37 | if (c) { 38 | client.setCredentials.apply(client, c.split(':')); 39 | resolve(); 40 | } else { 41 | reject(); 42 | } 43 | }); 44 | } 45 | }; 46 | return new streamedian.WSPlayer(node, _options); 47 | } 48 | }; -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | import {LogLevel, getTagged, setDefaultLogLevel} from 'bp_logger'; 2 | import * as streamedian from 'streamedian/player.js'; 3 | import WebsocketTransport from 'streamedian/transport/websocket.js'; 4 | import RTSPClient from 'streamedian/client/rtsp/client.js'; 5 | import {isSafari} from "streamedian/core/util/browser.js"; 6 | 7 | setDefaultLogLevel(LogLevel.Debug); 8 | getTagged("transport:ws").setLevel(LogLevel.Error); 9 | getTagged("client:rtsp").setLevel(LogLevel.Debug); 10 | getTagged("mse").setLevel(LogLevel.Debug); 11 | 12 | let wsTransport = { 13 | constructor: WebsocketTransport, 14 | options: { 15 | socket: "wss://specforge.com/ws/" 16 | // socket: "ws://localhost:8080/ws/" 17 | } 18 | }; 19 | 20 | window.StreamedianPlayer = new streamedian.WSPlayer('test_video', { 21 | // url: `${STREAM_UNIX}${STREAM_URL}`, 22 | // type: wsp.StreamType.RTSP, 23 | modules: [ 24 | { 25 | client: RTSPClient, 26 | transport: wsTransport 27 | } 28 | ], 29 | errorHandler (e){ 30 | alert(`Failed to start player: ${e.message}`); 31 | }, 32 | queryCredentials(client) { 33 | return new Promise((resolve, reject)=>{ 34 | let c = prompt('input credentials in format user:password'); 35 | if (c) { 36 | this.setCredentials.apply(this, c.split(':')); 37 | resolve(); 38 | } else { 39 | reject(); 40 | } 41 | }); 42 | } 43 | }); 44 | 45 | var setUrl = document.getElementById('set_new_url'); 46 | setUrl.onclick = function() { 47 | StreamedianPlayer.setSource(document.getElementById('stream_url').value, streamedian.StreamType.RTSP); 48 | }; -------------------------------------------------------------------------------- /src/core/base_transport.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from "../deps/bp_event.js"; 2 | 3 | export class BaseRequest { 4 | constructor(data) { 5 | this.data = data; 6 | this.before = (data)=>{return Promise.resolve(data)}; 7 | } 8 | 9 | send() { 10 | return this.before(this.data); 11 | } 12 | 13 | before(fn) { 14 | return Promise.resolve; 15 | } 16 | } 17 | 18 | export class BaseTransport { 19 | constructor(endpoint, stream_type, config={}) { 20 | this.stream_type = stream_type; 21 | this.endpoint = endpoint; 22 | this.eventSource = new EventEmitter(); 23 | this.dataQueue = []; 24 | } 25 | 26 | static canTransfer(stream_type) { 27 | return BaseTransport.streamTypes().includes(stream_type); 28 | } 29 | 30 | static streamTypes() { 31 | return []; 32 | } 33 | 34 | destroy() { 35 | this.eventSource.destroy(); 36 | } 37 | 38 | connect() { 39 | // TO be impemented 40 | } 41 | 42 | disconnect() { 43 | // TO be impemented 44 | } 45 | 46 | reconnect() { 47 | return this.disconnect().then(()=>{ 48 | return this.connect(); 49 | }); 50 | } 51 | 52 | setEndpoint(endpoint) { 53 | this.endpoint = endpoint; 54 | return this.reconnect(); 55 | } 56 | 57 | send(data) { 58 | // TO be impemented 59 | // return this.prepare(data).send(); 60 | } 61 | 62 | prepare(data) { 63 | // TO be impemented 64 | // return new Request(data); 65 | } 66 | 67 | // onData(type, data) { 68 | // this.eventSource.dispatchEvent(type, data); 69 | // } 70 | } -------------------------------------------------------------------------------- /src/core/parsers/aac.js: -------------------------------------------------------------------------------- 1 | import {BitArray, bitSlice} from '../util/binary.js'; 2 | 3 | export class AACParser { 4 | static get SampleRates() {return [ 5 | 96000, 88200, 6 | 64000, 48000, 7 | 44100, 32000, 8 | 24000, 22050, 9 | 16000, 12000, 10 | 11025, 8000, 11 | 7350];} 12 | 13 | // static Profile = [ 14 | // 0: Null 15 | // 1: AAC Main 16 | // 2: AAC LC (Low Complexity) 17 | // 3: AAC SSR (Scalable Sample Rate) 18 | // 4: AAC LTP (Long Term Prediction) 19 | // 5: SBR (Spectral Band Replication) 20 | // 6: AAC Scalable 21 | // ] 22 | 23 | static parseAudioSpecificConfig(bytesOrBits) { 24 | let config; 25 | if (bytesOrBits.byteLength) { // is byteArray 26 | config = new BitArray(bytesOrBits); 27 | } else { 28 | config = bytesOrBits; 29 | } 30 | 31 | let bitpos = config.bitpos+(config.src.byteOffset+config.bytepos)*8; 32 | let prof = config.readBits(5); 33 | this.codec = `mp4a.40.${prof}`; 34 | let sfi = config.readBits(4); 35 | if (sfi == 0xf) config.skipBits(24); 36 | let channels = config.readBits(4); 37 | 38 | return { 39 | config: bitSlice(new Uint8Array(config.src.buffer), bitpos, bitpos+16), 40 | codec: `mp4a.40.${prof}`, 41 | samplerate: AACParser.SampleRates[sfi], 42 | channels: channels 43 | } 44 | } 45 | 46 | static parseStreamMuxConfig(bytes) { 47 | // ISO_IEC_14496-3 Part 3 Audio. StreamMuxConfig 48 | let config = new BitArray(bytes); 49 | 50 | if (!config.readBits(1)) { 51 | config.skipBits(14); 52 | return AACParser.parseAudioSpecificConfig(config); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 29 | node_modules 30 | ### Linux template 31 | *~ 32 | 33 | # KDE directory preferences 34 | .directory 35 | 36 | # Linux trash folder which might appear on any partition or disk 37 | .Trash-* 38 | ### JetBrains template 39 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 40 | 41 | *.iml 42 | 43 | ## Directory-based project format: 44 | .idea/ 45 | # if you remove the above rule, at least ignore the following: 46 | 47 | # User-specific stuff: 48 | # .idea/workspace.xml 49 | # .idea/tasks.xml 50 | # .idea/dictionaries 51 | 52 | # Sensitive or high-churn files: 53 | # .idea/dataSources.ids 54 | # .idea/dataSources.xml 55 | # .idea/sqlDataSources.xml 56 | # .idea/dynamic.xml 57 | # .idea/uiDesigner.xml 58 | 59 | # Gradle: 60 | # .idea/gradle.xml 61 | # .idea/libraries 62 | 63 | # Mongo Explorer plugin: 64 | # .idea/mongoSettings.xml 65 | 66 | ## File-based project format: 67 | *.ipr 68 | *.iws 69 | 70 | ## Plugin-specific files: 71 | 72 | # IntelliJ 73 | /out/ 74 | 75 | # mpeltonen/sbt-idea plugin 76 | .idea_modules/ 77 | 78 | # JIRA plugin 79 | atlassian-ide-plugin.xml 80 | 81 | # Crashlytics plugin (for Android Studio and IntelliJ) 82 | com_crashlytics_export_strings.xml 83 | crashlytics.properties 84 | crashlytics-build.properties 85 | 86 | dist/ -------------------------------------------------------------------------------- /plugins/clappr/src/streamedian.clappr.js: -------------------------------------------------------------------------------- 1 | import Clappr from 'clappr'; 2 | 3 | import * as streamedian from 'streamedian/player'; 4 | import WebsocketTransport from 'streamedian/transport/websocket'; 5 | import RTSPClient from 'streamedian/client/rtsp/client'; 6 | import HLSClient from 'streamedian/client/hls/client'; 7 | 8 | export class ClapprLive extends Clappr.HTML5Video { 9 | constructor(options) { 10 | super(options); 11 | this.transport_ = { 12 | constructor: WebsocketTransport, 13 | options: options.streamedian_player_config_, 14 | }; 15 | this.player_ = new streamedian.WSPlayer(this.el, { 16 | bufferDuration : options.streamedian_player_config_.bufferDuration, 17 | url: options.sources[0].source, 18 | modules: [ 19 | { 20 | client: RTSPClient, 21 | transport: this.transport_ 22 | }, 23 | { 24 | client: HLSClient, 25 | transport: this.transport_ 26 | } 27 | ] 28 | }); 29 | window.StreamedianPlayer = this.player_; 30 | } 31 | 32 | play(){ 33 | this.el.play(); 34 | } 35 | 36 | destroy() { 37 | super.destroy(); 38 | this.player_.destroy(); 39 | this.player_ = null; 40 | window.StreamedianPlayer = null; 41 | } 42 | } 43 | 44 | ClapprLive.canPlay = function (resourceUrl, mimeType) { 45 | let canPlay = streamedian.WSPlayer.canPlayWithModules(mimeType,[ 46 | { 47 | client: RTSPClient, 48 | transport: WebsocketTransport 49 | }, 50 | { 51 | client: HLSClient, 52 | transport: WebsocketTransport 53 | }]); 54 | return canPlay; 55 | }; 56 | 57 | // global export user will can use it 58 | window.ClapprLive = ClapprLive; -------------------------------------------------------------------------------- /frameworks/react/src/VideoRateControl.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class VideoRateControl extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | playbackRate: 1.0, 8 | playbackRateText: 'live', 9 | } 10 | 11 | this.video = null; 12 | this.updatePlaybackRate = this.updatePlaybackRate.bind(this); 13 | this.livePlaybackRate = this.livePlaybackRate.bind(this); 14 | } 15 | 16 | componentDidMount() { 17 | this.video = document.getElementById(this.props.video); 18 | this.setState({playbackRate: this.video.playbackRate}); 19 | this.setState({playbackRateText: this.video.playbackRate}); 20 | } 21 | 22 | componentWillUpdate(nextProps, nestState) { 23 | this.video.playbackRate = this.state.playbackRate 24 | } 25 | 26 | updatePlaybackRate(event) { 27 | this.setState({playbackRate: event.target.value}); 28 | this.setState({playbackRateText: event.target.value}); 29 | } 30 | 31 | livePlaybackRate(event) { 32 | this.setState({playbackRate: 1}); 33 | this.setState({playbackRateText: 'live'}); 34 | if (this.video.buffered.length) { 35 | this.video.currentTime = this.video.buffered.end(0); 36 | } 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 |
43 | Playback rate:  44 | 53 | {this.state.playbackRateText} 54 |
55 |
56 | 60 |
61 |
62 | ); 63 | } 64 | } -------------------------------------------------------------------------------- /src/core/util/url.js: -------------------------------------------------------------------------------- 1 | export class Url { 2 | static parse(url) { 3 | let ret = {}; 4 | 5 | let urlparts = decodeURI(url).split(' '); 6 | url = urlparts.shift(); 7 | ret.client = urlparts.join(' '); 8 | 9 | let regex = /^([^:]+):\/\/([^\/]+)(.*)$/; //protocol, login, urlpath 10 | let result = regex.exec(url); 11 | 12 | if (!result) { 13 | throw new Error("bad url"); 14 | } 15 | 16 | ret.full = url; 17 | ret.protocol = result[1]; 18 | ret.urlpath = result[3]; 19 | 20 | let parts = ret.urlpath.split('/'); 21 | ret.basename = parts.pop().split(/\?|#/)[0]; 22 | ret.basepath = parts.join('/'); 23 | 24 | let loginSplit = result[2].split('@'); 25 | let hostport = loginSplit[0].split(':'); 26 | let userpass = [ null, null ]; 27 | if (loginSplit.length === 2) { 28 | userpass = loginSplit[0].split(':'); 29 | hostport = loginSplit[1].split(':'); 30 | } 31 | 32 | ret.user = userpass[0]; 33 | ret.pass = userpass[1]; 34 | ret.host = hostport[0]; 35 | ret.auth = (ret.user && ret.pass) ? `${ret.user}:${ret.pass}` : ''; 36 | 37 | ret.port = (null == hostport[1]) ? Url.protocolDefaultPort(ret.protocol) : hostport[1]; 38 | ret.portDefined = (null != hostport[1]); 39 | ret.location = `${ret.host}:${ret.port}`; 40 | 41 | if (ret.protocol == 'unix') { 42 | ret.socket = ret.port; 43 | ret.port = undefined; 44 | } 45 | 46 | return ret; 47 | } 48 | 49 | static full(parsed) { 50 | return `${parsed.protocol}://${parsed.location}/${parsed.urlpath}`; 51 | } 52 | 53 | static isAbsolute(url) { 54 | return /^[^:]+:\/\//.test(url); 55 | } 56 | 57 | static protocolDefaultPort(protocol) { 58 | switch (protocol) { 59 | case 'rtsp': return 554; 60 | case 'http': return 80; 61 | case 'https': return 443; 62 | } 63 | 64 | return 0; 65 | } 66 | } -------------------------------------------------------------------------------- /frameworks/react/src/StreamedianPlayer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ViewInfo from "./ViewInfo"; 3 | import InputSource from "./InputSource"; 4 | import InputBufferDuration from "./InputBufferDuration" 5 | import VideoRateControl from "./VideoRateControl"; 6 | 7 | export default class StreamedianPlayer extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | bufferDuration: 30, 12 | socket: "ws://localhost:8080/ws/", 13 | redirectNativeMediaErrors: true, 14 | errorHandler: this.errHandler.bind(this), 15 | infoHandler: this.infHandler.bind(this), 16 | }; 17 | 18 | this.player = null; 19 | this.restart = this.restart.bind(this); 20 | this.changeSource = this.changeSource.bind(this); 21 | this.changeBufferDuration = this.changeBufferDuration.bind(this); 22 | } 23 | 24 | componentDidMount() { 25 | this.player = window.Streamedian.player(this.props.id, this.state); 26 | } 27 | 28 | componentWillUnmount() { 29 | this.player.destroy(); 30 | this.player = null; 31 | } 32 | 33 | restart() { 34 | this.player.player.src = this.state.source; 35 | this.player.destroy(); 36 | this.player = null; 37 | this.player = window.Streamedian.player(this.props.id, this.state); 38 | } 39 | 40 | changeSource(src) { 41 | this.setState({source: src}, () => { 42 | this.restart(); 43 | }); 44 | } 45 | 46 | changeBufferDuration(duration) { 47 | this.setState({bufferDuration: duration}); 48 | } 49 | 50 | errHandler(err) { 51 | console.error(err.message); 52 | } 53 | 54 | infHandler(inf) { 55 | this.setState({info: inf}); 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 | 62 | 63 | 64 | 67 | 68 |
69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/core/elementary/NALU.js: -------------------------------------------------------------------------------- 1 | import {appendByteArray} from '../util/binary.js'; 2 | 3 | export class NALU { 4 | 5 | static get NDR() {return 1;} 6 | static get SLICE_PART_A() {return 2;} 7 | static get SLICE_PART_B() {return 3;} 8 | static get SLICE_PART_C() {return 4;} 9 | static get IDR() {return 5;} 10 | static get SEI() {return 6;} 11 | static get SPS() {return 7;} 12 | static get PPS() {return 8;} 13 | static get DELIMITER() {return 9;} 14 | static get EOSEQ() {return 10;} 15 | static get EOSTR() {return 11;} 16 | static get FILTER() {return 12;} 17 | static get STAP_A() {return 24;} 18 | static get STAP_B() {return 25;} 19 | static get FU_A() {return 28;} 20 | static get FU_B() {return 29;} 21 | 22 | static get TYPES() {return { 23 | [NALU.IDR]: 'IDR', 24 | [NALU.SEI]: 'SEI', 25 | [NALU.SPS]: 'SPS', 26 | [NALU.PPS]: 'PPS', 27 | [NALU.NDR]: 'NDR' 28 | }}; 29 | 30 | static type(nalu) { 31 | if (nalu.ntype in NALU.TYPES) { 32 | return NALU.TYPES[nalu.ntype]; 33 | } else { 34 | return 'UNKNOWN'; 35 | } 36 | } 37 | 38 | constructor(ntype, nri, data, dts, pts) { 39 | 40 | this.data = data; 41 | this.ntype = ntype; 42 | this.nri = nri; 43 | this.dts = dts; 44 | this.pts = pts ? pts : this.dts; 45 | this.sliceType = null; 46 | } 47 | 48 | appendData(idata) { 49 | this.data = appendByteArray(this.data, idata); 50 | } 51 | 52 | toString() { 53 | return `${NALU.type(this)}(${this.data.byteLength}): NRI: ${this.getNri()}, PTS: ${this.pts}, DTS: ${this.dts}`; 54 | } 55 | 56 | getNri() { 57 | return this.nri >> 5; 58 | } 59 | 60 | type() { 61 | return this.ntype; 62 | } 63 | 64 | isKeyframe() { 65 | return this.ntype === NALU.IDR || this.sliceType === 7; 66 | } 67 | 68 | getSize() { 69 | return 4 + 1 + this.data.byteLength; 70 | } 71 | 72 | getData() { 73 | let header = new Uint8Array(5 + this.data.byteLength); 74 | let view = new DataView(header.buffer); 75 | view.setUint32(0, this.data.byteLength + 1); 76 | view.setUint8(4, (0x0 & 0x80) | (this.nri & 0x60) | (this.ntype & 0x1F)); 77 | header.set(this.data, 5); 78 | return header; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /player.js: -------------------------------------------------------------------------------- 1 | import {LogLevel, getTagged, setDefaultLogLevel} from 'bp_logger'; 2 | import * as streamedian from 'streamedian/player.js'; 3 | import WebsocketTransport from 'streamedian/transport/websocket.js'; 4 | import RTSPClient from 'streamedian/client/rtsp/client.js'; 5 | import {isSafari} from "streamedian/core/util/browser.js"; 6 | 7 | 8 | setDefaultLogLevel(LogLevel.Error); 9 | getTagged("transport:ws").setLevel(LogLevel.Error); 10 | getTagged("client:rtsp").setLevel(LogLevel.Debug); 11 | getTagged("mse").setLevel(LogLevel.Debug); 12 | 13 | window.Streamedian = { 14 | logger(tag) { 15 | return getTagged(tag) 16 | }, 17 | player(node, opts) { 18 | if (!opts.socket) { 19 | throw new Error("socket parameter is not set"); 20 | } 21 | 22 | let _options = { 23 | modules: [ 24 | { 25 | client: RTSPClient, 26 | transport: { 27 | constructor: WebsocketTransport, 28 | options: { 29 | socket: opts.socket 30 | } 31 | } 32 | } 33 | ], 34 | errorHandler(e) { 35 | if(opts.errorHandler) { 36 | opts.errorHandler(e); 37 | } else { 38 | alert(`Failed to start player: ${e.message}`); 39 | } 40 | }, 41 | infoHandler(inf) { 42 | if(opts.infoHandler) { 43 | opts.infoHandler(inf); 44 | } 45 | }, 46 | dataHandler(data, prefix) { 47 | if(opts.dataHandler) { 48 | opts.dataHandler(data, prefix); 49 | } 50 | }, 51 | redirectNativeMediaErrors: opts.redirectNativeMediaErrors, 52 | bufferDuration : opts.bufferDuration, 53 | continuousFileLength: opts.continuousFileLength, 54 | eventFileLength: opts.eventFileLength, 55 | 56 | queryCredentials(client) { 57 | return new Promise((resolve, reject) => { 58 | let c = prompt('input credentials in format user:password'); 59 | if (c) { 60 | client.setCredentials.apply(client, c.split(':')); 61 | resolve(); 62 | } else { 63 | reject(); 64 | } 65 | }); 66 | } 67 | }; 68 | return new streamedian.WSPlayer(node, _options); 69 | } 70 | }; -------------------------------------------------------------------------------- /src/client/rtsp/session.js: -------------------------------------------------------------------------------- 1 | import {getTagged} from '../../deps/bp_logger.js'; 2 | 3 | import {RTSPClientSM as RTSPClient} from './client.js'; 4 | import {Url} from '../../core/util/url.js'; 5 | import {RTSPError} from "./client"; 6 | 7 | const LOG_TAG = "rtsp:session"; 8 | const Log = getTagged(LOG_TAG); 9 | 10 | export class RTSPSession { 11 | 12 | constructor(client, sessionId) { 13 | this.state = null; 14 | this.client = client; 15 | this.sessionId = sessionId; 16 | this.url = this.getControlURL(); 17 | } 18 | 19 | reset() { 20 | this.client = null; 21 | } 22 | 23 | start() { 24 | return this.sendPlay(); 25 | } 26 | 27 | stop() { 28 | return this.sendTeardown(); 29 | } 30 | 31 | getControlURL() { 32 | let ctrl = this.client.sdp.getSessionBlock().control; 33 | if (Url.isAbsolute(ctrl)) { 34 | return ctrl; 35 | } else if (!ctrl || '*' === ctrl) { 36 | return this.client.contentBase; 37 | } else { 38 | return `${this.client.contentBase}${ctrl}`; 39 | } 40 | } 41 | 42 | sendRequest(_cmd, _params = {}) { 43 | let params = {}; 44 | if (this.sessionId) { 45 | params['Session'] = this.sessionId; 46 | } 47 | Object.assign(params, _params); 48 | return this.client.sendRequest(_cmd, this.getControlURL(), params); 49 | } 50 | 51 | async sendPlay(pos = 0) { 52 | this.state = RTSPClient.STATE_PLAY; 53 | let params = {}; 54 | let range = this.client.sdp.sessionBlock.range; 55 | if (range) { 56 | // TODO: seekable 57 | if (range[0] == -1) { 58 | range[0] = 0;// Do not handle now at the moment 59 | } 60 | // params['Range'] = `${range[2]}=${range[0]}-`; 61 | } 62 | let data = await this.sendRequest('PLAY', params); 63 | this.state = RTSPClient.STATE_PLAYING; 64 | return {data: data}; 65 | } 66 | 67 | async sendPause() { 68 | if (!this.client.supports("PAUSE")) { 69 | return; 70 | } 71 | this.state = RTSPClient.STATE_PAUSE; 72 | await this.sendRequest("PAUSE"); 73 | this.state = RTSPClient.STATE_PAUSED; 74 | } 75 | 76 | async sendTeardown() { 77 | if (this.state != RTSPClient.STATE_TEARDOWN) { 78 | this.state = RTSPClient.STATE_TEARDOWN; 79 | await this.sendRequest("TEARDOWN"); 80 | Log.log('RTSPClient: STATE_TEARDOWN'); 81 | ///this.client.connection.disconnect(); 82 | // TODO: Notify client 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import buble from 'rollup-plugin-buble'; 3 | import alias from 'rollup-plugin-alias'; 4 | import async from 'rollup-plugin-async'; 5 | import uglify from 'rollup-plugin-uglify'; 6 | 7 | const path = require('path'); 8 | 9 | // export default { 10 | // entry: path.join(__dirname, 'example.js'), 11 | // targets: [ 12 | // {dest: path.join(__dirname, 'example/streamedian.js'), format: 'es'} 13 | // ], 14 | // sourceMap: true, 15 | // plugins: [ 16 | // // buble({ 17 | // // exclude: 'node_modules/**', 18 | // // transforms: {forOf: false} 19 | // // }), 20 | // async(), 21 | // babel({ 22 | // // exclude: 'node_modules/**', 23 | // // presets: [ 24 | // // 'es2015-rollup', "stage-0" 25 | // // ] 26 | // }), 27 | // uglify({ 28 | // mangle: false 29 | // }), 30 | // alias({ 31 | // bp_logger: path.join(__dirname,'node_modules/bp_logger/logger.js'), 32 | // bp_event: path.join(__dirname,'node_modules/bp_event/event.js'), 33 | // bp_statemachine: path.join(__dirname,'node_modules/bp_statemachine/statemachine.js'), 34 | // jsencrypt: path.join(__dirname,'node_modules/jsencrypt/src/jsencrypt.js'), 35 | // rtsp: path.join(__dirname,'node_modules/html5_rtsp_player/src'), 36 | // streamedian: path.join(__dirname,'src'), 37 | // }) 38 | // ] 39 | // 40 | // } 41 | export default { 42 | entry: path.join(__dirname, 'player.js'), 43 | targets: [ 44 | {dest: path.join(__dirname, 'example/streamedian.min.js'), format: 'iife'} 45 | ], 46 | sourceMap: true, 47 | plugins: [ 48 | // buble({ 49 | // exclude: 'node_modules/**', 50 | // transforms: {forOf: false} 51 | // }), 52 | // async(), 53 | // babel({ 54 | // // exclude: 'node_modules/**', 55 | // // presets: [ 56 | // // 'es2015-rollup', "stage-0" 57 | // // ] 58 | // }), 59 | // uglify({ 60 | // mangle: false 61 | // }), 62 | alias({ 63 | bp_logger: path.join(__dirname,'node_modules/bp_logger/logger.js'), 64 | bp_event: path.join(__dirname,'node_modules/bp_event/event.js'), 65 | bp_statemachine: path.join(__dirname,'node_modules/bp_statemachine/statemachine.js'), 66 | jsencrypt: path.join(__dirname,'node_modules/jsencrypt/src/jsencrypt.js'), 67 | rtsp: path.join(__dirname,'node_modules/html5_rtsp_player/src'), 68 | streamedian: path.join(__dirname,'src'), 69 | }) 70 | ] 71 | 72 | } -------------------------------------------------------------------------------- /src/client/rtsp/rtp/rtp.js: -------------------------------------------------------------------------------- 1 | // TODO: asm.js 2 | import {Log} from '../../../deps/bp_logger.js'; 3 | export default class RTP { 4 | constructor(pkt/*uint8array*/, sdp) { 5 | let bytes = new DataView(pkt.buffer, pkt.byteOffset, pkt.byteLength); 6 | 7 | this.version = bytes.getUint8(0) >>> 6; 8 | this.padding = bytes.getUint8(0) & 0x20 >>> 5; 9 | this.has_extension = bytes.getUint8(0) & 0x10 >>> 4; 10 | this.csrc = bytes.getUint8(0) & 0x0F; 11 | this.marker = bytes.getUint8(1) >>> 7; 12 | this.pt = bytes.getUint8(1) & 0x7F; 13 | this.sequence = bytes.getUint16(2) ; 14 | this.timestamp = bytes.getUint32(4); 15 | this.ssrc = bytes.getUint32(8); 16 | this.csrcs = []; 17 | 18 | let pktIndex=12; 19 | if (this.csrc>0) { 20 | this.csrcs.push(bytes.getUint32(pktIndex)); 21 | pktIndex+=4; 22 | } 23 | if (this.has_extension==1) { 24 | this.extension = bytes.getUint16(pktIndex); 25 | this.ehl = bytes.getUint16(pktIndex+2); 26 | pktIndex+=4; 27 | this.header_data = pkt.slice(pktIndex, this.ehl); 28 | pktIndex += this.ehl; 29 | } 30 | 31 | this.headerLength = pktIndex; 32 | let padLength = 0; 33 | if (this.padding) { 34 | padLength = bytes.getUint8(pkt.byteLength-1); 35 | } 36 | 37 | // this.bodyLength = pkt.byteLength-this.headerLength-padLength; 38 | 39 | this.media = sdp.getMediaBlockByPayloadType(this.pt); 40 | if (null === this.media) { 41 | Log.log(`Media description for payload type: ${this.pt} not provided.`); 42 | } else { 43 | this.type = this.media.ptype;//PayloadType.string_map[this.media.rtpmap[this.media.fmt[0]].name]; 44 | } 45 | 46 | this.data = pkt.subarray(pktIndex); 47 | // this.timestamp = 1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); 48 | // console.log(this); 49 | } 50 | getPayload() { 51 | return this.data; 52 | } 53 | 54 | getTimestampMS() { 55 | return this.timestamp; //1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); 56 | } 57 | 58 | toString() { 59 | return "RTP(" + 60 | "version:" + this.version + ", " + 61 | "padding:" + this.padding + ", " + 62 | "has_extension:" + this.has_extension + ", " + 63 | "csrc:" + this.csrc + ", " + 64 | "marker:" + this.marker + ", " + 65 | "pt:" + this.pt + ", " + 66 | "sequence:" + this.sequence + ", " + 67 | "timestamp:" + this.timestamp + ", " + 68 | "ssrc:" + this.ssrc + ")"; 69 | } 70 | 71 | isVideo(){return this.media.type == 'video';} 72 | isAudio(){return this.media.type == 'audio';} 73 | 74 | 75 | } -------------------------------------------------------------------------------- /src/client/hls/pes_avc.js: -------------------------------------------------------------------------------- 1 | import {NALUAsm} from "../../core/elementary/NALUAsm.js"; 2 | import {NALU} from "../../core/elementary/NALU.js"; 3 | import {appendByteArray} from "../../core/util/binary.js"; 4 | import {StreamType, PayloadType} from '../../core/defs.js'; 5 | 6 | export class AVCPES { 7 | constructor() { 8 | this.naluasm = new NALUAsm(); 9 | this.lastUnit = null; 10 | } 11 | 12 | parse(pes) { 13 | let array = pes.data; 14 | let i = 0, len = array.byteLength, value, overflow, state = 0; 15 | let units = [], lastUnitStart; 16 | while (i < len) { 17 | value = array[i++]; 18 | // finding 3 or 4-byte start codes (00 00 01 OR 00 00 00 01) 19 | switch (state) { 20 | case 0: 21 | if (value === 0) { 22 | state = 1; 23 | } 24 | break; 25 | case 1: 26 | if( value === 0) { 27 | state = 2; 28 | } else { 29 | state = 0; 30 | } 31 | break; 32 | case 2: 33 | case 3: 34 | if( value === 0) { 35 | state = 3; 36 | } else if (value === 1 && i < len) { 37 | if (lastUnitStart) { 38 | let nalu = this.naluasm.onNALUFragment(array.subarray(lastUnitStart, i - state - 1), pes.dts); 39 | if (nalu && (nalu.type() in NALU.TYPES)) { 40 | units.push(nalu); 41 | } 42 | } else { 43 | // If NAL units are not starting right at the beginning of the PES packet, push preceding data into previous NAL unit. 44 | overflow = i - state - 1; 45 | if (overflow) { 46 | if (this.lastUnit) { 47 | this.lastUnit.data = appendByteArray(this.lastUnit.data.byteLength, array.subarray(0, overflow)); 48 | } 49 | } 50 | } 51 | lastUnitStart = i; 52 | state = 0; 53 | } else { 54 | state = 0; 55 | } 56 | break; 57 | default: 58 | break; 59 | } 60 | } 61 | if (lastUnitStart) { 62 | let nalu = this.naluasm.onNALUFragment(array.subarray(lastUnitStart, len), pes.dts, pes.pts); 63 | if (nalu) { 64 | units.push(nalu); 65 | } 66 | } 67 | this.lastUnit = units[units.length-1]; 68 | return {units: units, type: StreamType.VIDEO, pay: PayloadType.H264}; 69 | } 70 | } -------------------------------------------------------------------------------- /src/core/base_client.js: -------------------------------------------------------------------------------- 1 | import {Log} from '../deps/bp_logger.js'; 2 | import {EventEmitter} from '../deps/bp_event.js'; 3 | 4 | export class BaseClient { 5 | constructor(options={flush: 100}) { 6 | this.options = options; 7 | this.eventSource = new EventEmitter(); 8 | 9 | Object.defineProperties(this, { 10 | sourceUrl: {value: null, writable: true}, // TODO: getter with validator 11 | paused: {value: true, writable: true}, 12 | seekable: {value: false, writable: true}, 13 | connected: {value: false, writable: true} 14 | }); 15 | 16 | this._onData = ()=>{ 17 | if (this.connected) { 18 | while (this.transport.dataQueue.length) { 19 | this.onData(this.transport.dataQueue.pop()); 20 | } 21 | } 22 | }; 23 | this._onConnect = this.onConnected.bind(this); 24 | this._onDisconnect = this.onDisconnected.bind(this); 25 | } 26 | 27 | static streamType() { 28 | return null; 29 | } 30 | 31 | destroy() { 32 | this.detachTransport(); 33 | } 34 | 35 | attachTransport(transport) { 36 | if (this.transport) { 37 | this.detachTransport(); 38 | } 39 | this.transport = transport; 40 | this.transport.eventSource.addEventListener('data', this._onData); 41 | this.transport.eventSource.addEventListener('connected', this._onConnect); 42 | this.transport.eventSource.addEventListener('disconnected', this._onDisconnect); 43 | } 44 | 45 | detachTransport() { 46 | if (this.transport) { 47 | this.transport.eventSource.removeEventListener('data', this._onData); 48 | this.transport.eventSource.removeEventListener('connected', this._onConnect); 49 | this.transport.eventSource.removeEventListener('disconnected', this._onDisconnect); 50 | this.transport = null; 51 | } 52 | } 53 | reset() { 54 | 55 | } 56 | 57 | start() { 58 | Log.log('Client started'); 59 | this.paused = false; 60 | // this.startStreamFlush(); 61 | } 62 | 63 | stop() { 64 | Log.log('Client paused'); 65 | this.paused = true; 66 | // this.stopStreamFlush(); 67 | } 68 | 69 | seek(timeOffset) { 70 | 71 | } 72 | 73 | setSource(source) { 74 | this.stop(); 75 | this.endpoint = source; 76 | this.sourceUrl = source.urlpath; 77 | } 78 | 79 | startStreamFlush() { 80 | this.flushInterval = setInterval(()=>{ 81 | if (!this.paused) { 82 | this.eventSource.dispatchEvent('flush'); 83 | } 84 | }, this.options.flush); 85 | } 86 | 87 | stopStreamFlush() { 88 | clearInterval(this.flushInterval); 89 | } 90 | 91 | onData(data) { 92 | 93 | } 94 | 95 | onConnected() { 96 | if (!this.seekable) { 97 | this.transport.dataQueue = []; 98 | this.eventSource.dispatchEvent('clear'); 99 | } 100 | this.connected = true; 101 | } 102 | 103 | onDisconnected() { 104 | this.connected = false; 105 | } 106 | 107 | queryCredentials() { 108 | return Promise.resolve(); 109 | } 110 | 111 | setCredentials(user, password) { 112 | this.endpoint.user = user; 113 | this.endpoint.pass = password; 114 | this.endpoint.auth = `${user}:${password}`; 115 | } 116 | } -------------------------------------------------------------------------------- /plugins/videojs/src/videojs.streamedian.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import {getTagged} from 'streamedian/deps/bp_logger'; 3 | import {WSPlayer, StreamType} from 'streamedian/player'; 4 | import WebsocketTransport from 'streamedian/transport/websocket'; 5 | import RTSPClient from 'streamedian/client/rtsp/client'; 6 | import HLSClient from 'streamedian/client/hls/client'; 7 | import MediaError from 'streamedian/media_error' 8 | 9 | const Log = getTagged("streamedian.video.js.plugin"); 10 | 11 | class StreamedianVideojs{ 12 | initPlayer(source, tech, techOptions){ 13 | var player = videojs.getPlayer(techOptions.playerId); 14 | this.videojsPlayer_ = player; 15 | this.playerOptions_ = player.options_; 16 | this.tech_ = tech; 17 | this.streamedian_player_config_ = this.playerOptions_.streamedian_player_config; 18 | 19 | this.transport_ = { 20 | constructor: WebsocketTransport, 21 | options: this.streamedian_player_config_ 22 | }; 23 | 24 | var boundErrorHandler = this.onError.bind(this); 25 | this.player_ = new WSPlayer(tech.el(), { 26 | bufferDuration : this.streamedian_player_config_.bufferDuration, 27 | redirectNativeMediaErrors : false, 28 | errorHandler: boundErrorHandler, 29 | modules: [ 30 | { 31 | client: RTSPClient, 32 | transport: this.transport_ 33 | }, 34 | { 35 | client: HLSClient, 36 | transport: this.transport_ 37 | } 38 | ] 39 | }); 40 | 41 | window.StreamedianPlayer = this.player_; 42 | } 43 | 44 | handleSource(source, tech, techOptions){ 45 | if(this.player_) { 46 | this.player_.destroy(); 47 | this.player_ = null; 48 | } 49 | 50 | this.initPlayer(source, tech, techOptions); 51 | } 52 | 53 | onError(error){ 54 | Log.debug(error); 55 | this.tech_.reset(); 56 | this.videojsPlayer_.error(error); 57 | } 58 | } 59 | 60 | let handle_ = new StreamedianVideojs; 61 | class LiveSourceHandler { 62 | 63 | static canHandleSource(source) { 64 | if(source.type) 65 | return this.canPlayType(source.type); 66 | else if(source.src){ 67 | const ext = Url.getFileExtension(source.src); 68 | return this.canPlayType(`video/${ext}`); 69 | } 70 | return ''; 71 | } 72 | 73 | static handleSource(source, tech, options) { 74 | tech.setSrc(source.src); 75 | return handle_.handleSource(source, tech, options); 76 | } 77 | 78 | static canPlayType(type) { 79 | var canPlayType = ''; 80 | if (WSPlayer.canPlayWithModules(type,[ 81 | { 82 | client: RTSPClient, 83 | transport: WebsocketTransport 84 | }, 85 | { 86 | client: HLSClient, 87 | transport: WebsocketTransport 88 | } 89 | ])) { 90 | canPlayType = 'probably'; 91 | } 92 | return canPlayType; 93 | } 94 | 95 | static dispose(){ 96 | Log.debug("dispose"); 97 | handle_.player.dispose(); 98 | } 99 | }; 100 | 101 | const Html5Tech = videojs.getTech('Html5'); 102 | if(Html5Tech) 103 | Html5Tech.registerSourceHandler(LiveSourceHandler, 0); 104 | else 105 | Log.error("Can't get Html5 Tech"); 106 | -------------------------------------------------------------------------------- /src/recorder.js: -------------------------------------------------------------------------------- 1 | import {MP4} from './core/iso-bmff/mp4-generator.js'; 2 | 3 | export class MediaRecorder { 4 | constructor(parent, prefix) { 5 | this.parent = parent; 6 | this.prefix = prefix; 7 | this.remuxer; 8 | this.header; 9 | this.firstBuffer = new Uint8Array(0); 10 | this.secondBuffer = new Uint8Array(0); 11 | this.byteBuffer = this.firstBuffer; 12 | this.isSwaped = false; 13 | this.isPaused = false; 14 | this.isRecording = false; 15 | 16 | this.fileLength = 180000; /*download every 3 minutes*/ 17 | } 18 | 19 | setFileLength(milliseconds) { 20 | if (milliseconds) { 21 | this.fileLength = milliseconds; 22 | } 23 | } 24 | 25 | init(event) { 26 | let tracks = event.detail; 27 | let tracks_list = []; 28 | for (let key in tracks) { 29 | let type = tracks[key].mp4track.type; 30 | if (type === "video" || type === "audio") { 31 | tracks_list.push(tracks[key].mp4track); 32 | } 33 | } 34 | 35 | this.header = MP4.initSegment(tracks_list, tracks[1].duration*tracks[1].timescale, tracks[1].timescale) 36 | } 37 | 38 | pushData(event) { 39 | if (this.isRecording) { 40 | if (this.byteBuffer.length == 0) { 41 | this.setBuffer(this.header); 42 | } 43 | 44 | this.setBuffer(event.detail[0]); 45 | this.setBuffer(event.detail[1]); 46 | } 47 | } 48 | 49 | record(recordvalue) { 50 | if (recordvalue) { 51 | if (!this.isRecording) { 52 | this.flushInterval = setInterval(this.flush.bind(this), this.fileLength); 53 | } 54 | } else { 55 | clearInterval(this.flushInterval); 56 | this.flush(); 57 | } 58 | 59 | this.isRecording = recordvalue; 60 | } 61 | 62 | pause(value) { 63 | if (this.isRecording || this.isPaused) { 64 | this.record(!value); 65 | this.isPaused = value; 66 | } 67 | } 68 | 69 | setBuffer(data) { 70 | if (this.isRecording) { 71 | let tmp = new Uint8Array(this.byteBuffer.byteLength + data.byteLength); 72 | tmp.set(new Uint8Array(this.byteBuffer), 0); 73 | tmp.set(new Uint8Array(data), this.byteBuffer.byteLength); 74 | this.byteBuffer = tmp; 75 | } 76 | } 77 | 78 | swapBuffer() { 79 | this.isSwaped = !this.isSwaped; 80 | 81 | if (this.isSwaped) { 82 | this.byteBuffer = this.secondBuffer; 83 | this.firstBuffer = new Uint8Array(0); 84 | } else { 85 | this.byteBuffer = this.firstBuffer; 86 | this.secondBuffer = new Uint8Array(0); 87 | } 88 | } 89 | 90 | flush() { 91 | let byteBuffer = this.byteBuffer; 92 | this.swapBuffer(); 93 | if (this.header && byteBuffer.length > this.header.length) { 94 | this.parent.mediadata(byteBuffer, this.prefix); 95 | } 96 | } 97 | 98 | attachSource(remuxer) { 99 | this.remuxer = remuxer; 100 | this.remuxer.eventSource.addEventListener('mp4initsegement', this.init.bind(this)); 101 | this.remuxer.eventSource.addEventListener('mp4payload', this.pushData.bind(this)); 102 | } 103 | 104 | dettachSource() { 105 | if (this.remuxer) { 106 | this.remuxer.eventSource.removeEventListener('mp4initsegement'); 107 | this.remuxer.eventSource.removeEventListener('mp4payload'); 108 | this.remuxer = null; 109 | } 110 | } 111 | 112 | destroy() { 113 | this.record(false); 114 | this.dettachSource(); 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /src/core/elementary/AACAsm.js: -------------------------------------------------------------------------------- 1 | import {AACFrame} from './AACFrame.js'; 2 | import {BitArray} from '../util/binary'; 3 | // import {AACParser} from "../parsers/aac.js"; 4 | // TODO: asm.js 5 | export class AACAsm { 6 | constructor() { 7 | this.config = null; 8 | } 9 | 10 | onAACFragment(pkt) { 11 | let rawData = pkt.getPayload(); 12 | if (!pkt.media) { 13 | return null; 14 | } 15 | let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); 16 | 17 | let sizeLength = Number(pkt.media.fmtp['sizelength'] || 0); 18 | let indexLength = Number(pkt.media.fmtp['indexlength'] || 0); 19 | let indexDeltaLength = Number(pkt.media.fmtp['indexdeltalength'] || 0); 20 | let CTSDeltaLength = Number(pkt.media.fmtp['ctsdeltalength'] || 0); 21 | let DTSDeltaLength = Number(pkt.media.fmtp['dtsdeltalength'] || 0); 22 | let RandomAccessIndication = Number(pkt.media.fmtp['randomaccessindication'] || 0); 23 | let StreamStateIndication = Number(pkt.media.fmtp['streamstateindication'] || 0); 24 | let AuxiliaryDataSizeLength = Number(pkt.media.fmtp['auxiliarydatasizelength'] || 0); 25 | 26 | let configHeaderLength = 27 | sizeLength + Math.max(indexLength, indexDeltaLength) + CTSDeltaLength + DTSDeltaLength + 28 | RandomAccessIndication + StreamStateIndication + AuxiliaryDataSizeLength; 29 | 30 | 31 | let auHeadersLengthPadded = 0; 32 | let offset = 0; 33 | let ts = (Math.round(pkt.getTimestampMS()/1024) << 10) * 90000 / this.config.samplerate; 34 | if (0 !== configHeaderLength) { 35 | /* The AU header section is not empty, read it from payload */ 36 | let auHeadersLengthInBits = data.getUint16(0); // Always 2 octets, without padding 37 | auHeadersLengthPadded = 2 + (auHeadersLengthInBits>>>3) + ((auHeadersLengthInBits & 0x7)?1:0); // Add padding 38 | 39 | //this.config = AACParser.parseAudioSpecificConfig(new Uint8Array(rawData, 0 , auHeadersLengthPadded)); 40 | // TODO: parse config 41 | let frames = []; 42 | let frameOffset=0; 43 | let bits = new BitArray(rawData.subarray(2 + offset)); 44 | let cts = 0; 45 | let dts = 0; 46 | for (let offset=0; offset>> 7; 51 | let is_end = (fu_header & 0x40) >>> 6; 52 | let payload_type = fu_header & 0x1F; 53 | let ret = null; 54 | 55 | nal_start_idx++; 56 | let don = 0; 57 | if (NALU.FU_B === header.type) { 58 | don = data.getUint16(nal_start_idx); 59 | nal_start_idx += 2; 60 | } 61 | 62 | if (is_start) { 63 | this.fragmented_nalu = new NALU(payload_type, header.nri, rawData.subarray(nal_start_idx), dts, pts); 64 | } 65 | if (this.fragmented_nalu && this.fragmented_nalu.ntype === payload_type) { 66 | if (!is_start) { 67 | this.fragmented_nalu.appendData(rawData.subarray(nal_start_idx)); 68 | } 69 | if (is_end) { 70 | ret = this.fragmented_nalu; 71 | this.fragmented_nalu = null; 72 | return ret; 73 | } 74 | } 75 | return null; 76 | } 77 | 78 | onNALUFragment(rawData, dts, pts) { 79 | 80 | let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); 81 | 82 | let header = NALUAsm.parseNALHeader(data.getUint8(0)); 83 | 84 | let nal_start_idx = 1; 85 | 86 | let unit = null; 87 | if (header.type > 0 && header.type < 24) { 88 | unit = this.parseSingleNALUPacket(rawData.subarray(nal_start_idx), header, dts, pts); 89 | } else if (NALU.FU_A === header.type || NALU.FU_B === header.type) { 90 | unit = this.parseFragmentationUnit(rawData.subarray(nal_start_idx), header, dts, pts); 91 | } else if (NALU.STAP_A === header.type || NALU.STAP_B === header.type) { 92 | return this.parseAggregationPacket(rawData.subarray(nal_start_idx), header, dts, pts); 93 | } else { 94 | /* 30 - 31 is undefined, ignore those (RFC3984). */ 95 | Log.log('Undefined NAL unit, type: ' + header.type); 96 | return null; 97 | } 98 | if (unit) { 99 | return [unit]; 100 | } 101 | return null; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/core/parsers/ts.js: -------------------------------------------------------------------------------- 1 | import {BitArray} from '../util/binary.js'; 2 | import {PESAsm} from './pes.js'; 3 | import {PayloadType} from '../defs.js'; 4 | 5 | export class PESType { 6 | static get AAC() {return 0x0f;} // ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio) 7 | static get ID3() {return 0x15;} // Packetized metadata (ID3) 8 | static get H264() {return 0x1b;} // ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video) 9 | } 10 | 11 | export class TSParser { 12 | static get PACKET_LENGTH() {return 188;} 13 | 14 | constructor() { 15 | this.pmtParsed = false; 16 | this.pesParserTypes = new Map(); 17 | this.pesParsers = new Map(); 18 | this.pesAsms = {}; 19 | this.ontracks = null; 20 | this.toSkip = 0; 21 | } 22 | 23 | addPesParser(pesType, constructor) { 24 | this.pesParserTypes.set(pesType, constructor); 25 | } 26 | 27 | parse(packet) { 28 | let bits = new BitArray(packet); 29 | if (packet[0] === 0x47) { 30 | bits.skipBits(9); 31 | let payStart = bits.readBits(1); 32 | bits.skipBits(1); 33 | let pid = bits.readBits(13); 34 | bits.skipBits(2); 35 | let adaptFlag = bits.readBits(1); 36 | let payFlag = bits.readBits(1); 37 | bits.skipBits(4); 38 | if (adaptFlag) { 39 | let adaptSize = bits.readBits(8); 40 | this.toSkip = bits.skipBits(adaptSize*8); 41 | if (bits.finished()) { 42 | return; 43 | } 44 | } 45 | if (!payFlag) return; 46 | 47 | let payload = packet.subarray(bits.bytepos);//bitSlice(packet, bits.bitpos+bits.bytepos*8); 48 | 49 | if (this.pmtParsed && this.pesParsers.has(pid)) { 50 | let pes = this.pesAsms[pid].feed(payload, payStart); 51 | if (pes) { 52 | return this.pesParsers.get(pid).parse(pes); 53 | } 54 | } else { 55 | if (pid === 0) { 56 | this.pmtId = this.parsePAT(payload); 57 | } else if (pid === this.pmtId) { 58 | this.parsePMT(payload); 59 | this.pmtParsed = true; 60 | } 61 | } 62 | } 63 | return null; 64 | } 65 | 66 | parsePAT(data) { 67 | let bits = new BitArray(data); 68 | let ptr = bits.readBits(8); 69 | bits.skipBits(8*ptr+83); 70 | return bits.readBits(13); 71 | } 72 | 73 | parsePMT(data) { 74 | let bits = new BitArray(data); 75 | let ptr = bits.readBits(8); 76 | bits.skipBits(8*ptr + 8); 77 | bits.skipBits(6); 78 | let secLen = bits.readBits(10); 79 | bits.skipBits(62); 80 | let pil = bits.readBits(10); 81 | bits.skipBits(pil*8); 82 | 83 | let tracks = new Set(); 84 | let readLen = secLen-13-pil; 85 | while (readLen>0) { 86 | let pesType = bits.readBits(8); 87 | bits.skipBits(3); 88 | let pid = bits.readBits(13); 89 | bits.skipBits(6); 90 | let il = bits.readBits(10); 91 | bits.skipBits(il*8); 92 | if ([PESType.AAC, PESType.H264].includes(pesType)) { 93 | if (this.pesParserTypes.has(pesType) && !this.pesParsers.has(pid)) { 94 | this.pesParsers.set(pid, new (this.pesParserTypes.get(pesType))); 95 | this.pesAsms[pid] = new PESAsm(); 96 | switch (pesType) { 97 | case PESType.H264: tracks.add({type: PayloadType.H264, offset: 0});break; 98 | case PESType.AAC: tracks.add({type: PayloadType.AAC, offset: 0});break; 99 | } 100 | } 101 | } 102 | readLen -= 5+il; 103 | } 104 | // TODO: notify about tracks 105 | if (this.ontracks) { 106 | this.ontracks(tracks); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /src/core/util/exp-golomb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parser for exponential Golomb codes, a variable-bitwidth number encoding scheme used by h264. 3 | */ 4 | // TODO: asm.js 5 | import {Log as logger} from '../../deps/bp_logger.js'; 6 | 7 | export class ExpGolomb { 8 | 9 | constructor(data) { 10 | this.data = data; 11 | // the number of bytes left to examine in this.data 12 | this.bytesAvailable = this.data.byteLength; 13 | // the current word being examined 14 | this.word = 0; // :uint 15 | // the number of bits left to examine in the current word 16 | this.bitsAvailable = 0; // :uint 17 | } 18 | 19 | // ():void 20 | loadWord() { 21 | var 22 | position = this.data.byteLength - this.bytesAvailable, 23 | workingBytes = new Uint8Array(4), 24 | availableBytes = Math.min(4, this.bytesAvailable); 25 | if (availableBytes === 0) { 26 | throw new Error('no bytes available'); 27 | } 28 | workingBytes.set(this.data.subarray(position, position + availableBytes)); 29 | this.word = new DataView(workingBytes.buffer, workingBytes.byteOffset, workingBytes.byteLength).getUint32(0); 30 | // track the amount of this.data that has been processed 31 | this.bitsAvailable = availableBytes * 8; 32 | this.bytesAvailable -= availableBytes; 33 | } 34 | 35 | // (count:int):void 36 | skipBits(count) { 37 | var skipBytes; // :int 38 | if (this.bitsAvailable > count) { 39 | this.word <<= count; 40 | this.bitsAvailable -= count; 41 | } else { 42 | count -= this.bitsAvailable; 43 | skipBytes = count >> 3; 44 | count -= (skipBytes << 3); 45 | this.bytesAvailable -= skipBytes; 46 | this.loadWord(); 47 | this.word <<= count; 48 | this.bitsAvailable -= count; 49 | } 50 | } 51 | 52 | // (size:int):uint 53 | readBits(size) { 54 | var 55 | bits = Math.min(this.bitsAvailable, size), // :uint 56 | valu = this.word >>> (32 - bits); // :uint 57 | if (size > 32) { 58 | logger.error('Cannot read more than 32 bits at a time'); 59 | } 60 | this.bitsAvailable -= bits; 61 | if (this.bitsAvailable > 0) { 62 | this.word <<= bits; 63 | } else if (this.bytesAvailable > 0) { 64 | this.loadWord(); 65 | } 66 | bits = size - bits; 67 | if (bits > 0) { 68 | return valu << bits | this.readBits(bits); 69 | } else { 70 | return valu; 71 | } 72 | } 73 | 74 | // ():uint 75 | skipLZ() { 76 | var leadingZeroCount; // :uint 77 | for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) { 78 | if (0 !== (this.word & (0x80000000 >>> leadingZeroCount))) { 79 | // the first bit of working word is 1 80 | this.word <<= leadingZeroCount; 81 | this.bitsAvailable -= leadingZeroCount; 82 | return leadingZeroCount; 83 | } 84 | } 85 | // we exhausted word and still have not found a 1 86 | this.loadWord(); 87 | return leadingZeroCount + this.skipLZ(); 88 | } 89 | 90 | // ():void 91 | skipUEG() { 92 | this.skipBits(1 + this.skipLZ()); 93 | } 94 | 95 | // ():void 96 | skipEG() { 97 | this.skipBits(1 + this.skipLZ()); 98 | } 99 | 100 | // ():uint 101 | readUEG() { 102 | var clz = this.skipLZ(); // :uint 103 | return this.readBits(clz + 1) - 1; 104 | } 105 | 106 | // ():int 107 | readEG() { 108 | var valu = this.readUEG(); // :int 109 | if (0x01 & valu) { 110 | // the number is odd if the low order bit is set 111 | return (1 + valu) >>> 1; // add 1 to make it even, and divide by 2 112 | } else { 113 | return -1 * (valu >>> 1); // divide by two then make it negative 114 | } 115 | } 116 | 117 | // Some convenience functions 118 | // :Boolean 119 | readBoolean() { 120 | return 1 === this.readBits(1); 121 | } 122 | 123 | // ():int 124 | readUByte() { 125 | return this.readBits(8); 126 | } 127 | 128 | // ():int 129 | readUShort() { 130 | return this.readBits(16); 131 | } 132 | // ():int 133 | readUInt() { 134 | return this.readBits(32); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/core/util/binary.js: -------------------------------------------------------------------------------- 1 | // TODO: asm.js 2 | 3 | export function appendByteArray(buffer1, buffer2) { 4 | let tmp = new Uint8Array((buffer1.byteLength|0) + (buffer2.byteLength|0)); 5 | tmp.set(buffer1, 0); 6 | tmp.set(buffer2, buffer1.byteLength|0); 7 | return tmp; 8 | } 9 | 10 | export function appendByteArrayAsync(buffer1, buffer2) { 11 | return new Promise((resolve, reject)=>{ 12 | let blob = new Blob([buffer1, buffer2]); 13 | let reader = new FileReader(); 14 | reader.addEventListener("loadend", function() { 15 | resolve(); 16 | }); 17 | reader.readAsArrayBuffer(blob); 18 | }); 19 | } 20 | export function base64ToArrayBuffer(base64) { 21 | var binary_string = window.atob(base64); 22 | var len = binary_string.length; 23 | var bytes = new Uint8Array( len ); 24 | for (var i = 0; i < len; i++) { 25 | bytes[i] = binary_string.charCodeAt(i); 26 | } 27 | return bytes.buffer; 28 | } 29 | 30 | export function hexToByteArray(hex) { 31 | let len = hex.length >> 1; 32 | var bufView = new Uint8Array(len); 33 | for (var i = 0; i < len; i++) { 34 | bufView[i] = parseInt(hex.substr(i<<1,2),16); 35 | } 36 | return bufView; 37 | } 38 | 39 | export function concatenate(resultConstructor, ...arrays) { 40 | let totalLength = 0; 41 | for (let arr of arrays) { 42 | totalLength += arr.length; 43 | } 44 | let result = new resultConstructor(totalLength); 45 | let offset = 0; 46 | for (let arr of arrays) { 47 | result.set(arr, offset); 48 | offset += arr.length; 49 | } 50 | return result; 51 | } 52 | 53 | export function bitSlice(bytearray, start=0, end=bytearray.byteLength*8) { 54 | let byteLen = Math.ceil((end-start)/8); 55 | let res = new Uint8Array(byteLen); 56 | let startByte = start >>> 3; // /8 57 | let endByte = (end>>>3) - 1; // /8 58 | let bitOffset = start & 0x7; // %8 59 | let nBitOffset = 8 - bitOffset; 60 | let endOffset = 8 - end & 0x7; // %8 61 | for (let i=0; i> nBitOffset; 65 | if (i == endByte-1 && endOffset < 8) { 66 | tail >>= endOffset; 67 | tail <<= endOffset; 68 | } 69 | } 70 | res[i]=(bytearray[startByte+i]< 0; --i) { 92 | 93 | /* Shift result one left to make room for another bit, 94 | then add the next bit on the stream. */ 95 | result = ((result|0) << 1) | (((this.byte|0) >> (8 - (++this.bitpos))) & 0x01); 96 | if ((this.bitpos|0)>=8) { 97 | this.byte = this.src.getUint8(++this.bytepos); 98 | this.bitpos &= 0x7; 99 | } 100 | } 101 | 102 | return result; 103 | } 104 | skipBits(length) { 105 | this.bitpos += (length|0) & 0x7; // %8 106 | this.bytepos += (length|0) >>> 3; // *8 107 | if (this.bitpos > 7) { 108 | this.bitpos &= 0x7; 109 | ++this.bytepos; 110 | } 111 | 112 | if (!this.finished()) { 113 | this.byte = this.src.getUint8(this.bytepos); 114 | return 0; 115 | } else { 116 | return this.bytepos-this.src.byteLength-this.src.bitpos; 117 | } 118 | } 119 | 120 | finished() { 121 | return this.bytepos >= this.src.byteLength; 122 | } 123 | } -------------------------------------------------------------------------------- /src/client/hls/pes_aac.js: -------------------------------------------------------------------------------- 1 | import {Log} from '../../deps/bp_logger.js'; 2 | import {ADTS} from './adts.js'; 3 | import {StreamType, PayloadType} from '../../core/defs.js'; 4 | import {AACFrame} from '../../core/elementary/AACFrame.js'; 5 | 6 | export class AACPES { 7 | constructor() { 8 | this.aacOverFlow = null; 9 | this.lastAacPTS = null; 10 | this.track = {}; 11 | this.config = null; 12 | } 13 | 14 | parse(pes) { 15 | let data = pes.data; 16 | let pts = pes.pts; 17 | let startOffset = 0; 18 | let aacOverFlow = this.aacOverFlow; 19 | let lastAacPTS = this.lastAacPTS; 20 | var config, frameDuration, frameIndex, offset, stamp, len; 21 | 22 | if (aacOverFlow) { 23 | var tmp = new Uint8Array(aacOverFlow.byteLength + data.byteLength); 24 | tmp.set(aacOverFlow, 0); 25 | tmp.set(data, aacOverFlow.byteLength); 26 | Log.debug(`AAC: append overflowing ${aacOverFlow.byteLength} bytes to beginning of new PES`); 27 | data = tmp; 28 | } 29 | 30 | // look for ADTS header (0xFFFx) 31 | for (offset = startOffset, len = data.length; offset < len - 1; offset++) { 32 | if ((data[offset] === 0xff) && (data[offset+1] & 0xf0) === 0xf0) { 33 | break; 34 | } 35 | } 36 | // if ADTS header does not start straight from the beginning of the PES payload, raise an error 37 | if (offset) { 38 | var reason, fatal; 39 | if (offset < len - 1) { 40 | reason = `AAC PES did not start with ADTS header,offset:${offset}`; 41 | fatal = false; 42 | } else { 43 | reason = 'no ADTS header found in AAC PES'; 44 | fatal = true; 45 | } 46 | Log.error(reason); 47 | if (fatal) { 48 | return; 49 | } 50 | } 51 | 52 | let hdr = null; 53 | let res = {units:[], type: StreamType.AUDIO, pay: PayloadType.AAC}; 54 | if (!this.config) { 55 | hdr = ADTS.parseHeaderConfig(data.subarray(offset)); 56 | this.config = hdr.config; 57 | res.config = hdr.config; 58 | hdr.config = null; 59 | Log.debug(`parsed codec:${this.config.codec},rate:${this.config.samplerate},nb channel:${this.config.channels}`); 60 | } 61 | frameIndex = 0; 62 | frameDuration = 1024 * 90000 / this.config.samplerate; 63 | 64 | // if last AAC frame is overflowing, we should ensure timestamps are contiguous: 65 | // first sample PTS should be equal to last sample PTS + frameDuration 66 | if(aacOverFlow && lastAacPTS) { 67 | var newPTS = lastAacPTS+frameDuration; 68 | if(Math.abs(newPTS-pts) > 1) { 69 | Log.debug(`AAC: align PTS for overlapping frames by ${Math.round((newPTS-pts)/90)}`); 70 | pts=newPTS; 71 | } 72 | } 73 | 74 | while ((offset + 5) < len) { 75 | if (!hdr) { 76 | hdr = ADTS.parseHeader(data.subarray(offset)); 77 | } 78 | if ((hdr.size > 0) && ((offset + hdr.offset + hdr.size) <= len)) { 79 | stamp = pts + frameIndex * frameDuration; 80 | res.units.push(new AACFrame(data.subarray(offset + hdr.offset, offset + hdr.offset + hdr.size), stamp)); 81 | offset += hdr.offset + hdr.size; 82 | frameIndex++; 83 | // look for ADTS header (0xFFFx) 84 | for ( ; offset < (len - 1); offset++) { 85 | if ((data[offset] === 0xff) && ((data[offset + 1] & 0xf0) === 0xf0)) { 86 | break; 87 | } 88 | } 89 | } else { 90 | break; 91 | } 92 | hdr = null; 93 | } 94 | if ((offset < len) && (data[offset]==0xff)) { // TODO: check it 95 | aacOverFlow = data.subarray(offset, len); 96 | //logger.log(`AAC: overflow detected:${len-offset}`); 97 | } else { 98 | aacOverFlow = null; 99 | } 100 | this.aacOverFlow = aacOverFlow; 101 | this.lastAacPTS = stamp; 102 | 103 | return res; 104 | } 105 | } -------------------------------------------------------------------------------- /src/client/hls/id3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ID3 parser 3 | */ 4 | import {getTagged} from '../../deps/bp_logger.js'; 5 | 6 | const logger = getTagged('id3'); 7 | 8 | class ID3 { 9 | 10 | constructor(data) { 11 | this._hasTimeStamp = false; 12 | var offset = 0, byte1,byte2,byte3,byte4,tagSize,endPos,header,len; 13 | do { 14 | header = this.readUTF(data,offset,3); 15 | offset+=3; 16 | // first check for ID3 header 17 | if (header === 'ID3') { 18 | // skip 24 bits 19 | offset += 3; 20 | // retrieve tag(s) length 21 | byte1 = data[offset++] & 0x7f; 22 | byte2 = data[offset++] & 0x7f; 23 | byte3 = data[offset++] & 0x7f; 24 | byte4 = data[offset++] & 0x7f; 25 | tagSize = (byte1 << 21) + (byte2 << 14) + (byte3 << 7) + byte4; 26 | endPos = offset + tagSize; 27 | //logger.log(`ID3 tag found, size/end: ${tagSize}/${endPos}`); 28 | 29 | // read ID3 tags 30 | this._parseID3Frames(data, offset,endPos); 31 | offset = endPos; 32 | } else if (header === '3DI') { 33 | // http://id3.org/id3v2.4.0-structure chapter 3.4. ID3v2 footer 34 | offset += 7; 35 | logger.log(`3DI footer found, end: ${offset}`); 36 | } else { 37 | offset -= 3; 38 | len = offset; 39 | if (len) { 40 | //logger.log(`ID3 len: ${len}`); 41 | if (!this.hasTimeStamp) { 42 | logger.warn('ID3 tag found, but no timestamp'); 43 | } 44 | this._length = len; 45 | this._payload = data.subarray(0,len); 46 | } 47 | return; 48 | } 49 | } while (true); 50 | } 51 | 52 | readUTF(data,start,len) { 53 | 54 | var result = '',offset = start, end = start + len; 55 | do { 56 | result += String.fromCharCode(data[offset++]); 57 | } while(offset < end); 58 | return result; 59 | } 60 | 61 | _parseID3Frames(data,offset,endPos) { 62 | var tagId,tagLen,tagStart,tagFlags,timestamp; 63 | while(offset + 8 <= endPos) { 64 | tagId = this.readUTF(data,offset,4); 65 | offset +=4; 66 | 67 | tagLen = data[offset++] << 24 + 68 | data[offset++] << 16 + 69 | data[offset++] << 8 + 70 | data[offset++]; 71 | 72 | tagFlags = data[offset++] << 8 + 73 | data[offset++]; 74 | 75 | tagStart = offset; 76 | //logger.log("ID3 tag id:" + tagId); 77 | switch(tagId) { 78 | case 'PRIV': 79 | //logger.log('parse frame:' + Hex.hexDump(data.subarray(offset,endPos))); 80 | // owner should be "com.apple.streaming.transportStreamTimestamp" 81 | if (this.readUTF(data,offset,44) === 'com.apple.streaming.transportStreamTimestamp') { 82 | offset+=44; 83 | // smelling even better ! we found the right descriptor 84 | // skip null character (string end) + 3 first bytes 85 | offset+= 4; 86 | 87 | // timestamp is 33 bit expressed as a big-endian eight-octet number, with the upper 31 bits set to zero. 88 | var pts33Bit = data[offset++] & 0x1; 89 | this._hasTimeStamp = true; 90 | 91 | timestamp = ((data[offset++] << 23) + 92 | (data[offset++] << 15) + 93 | (data[offset++] << 7) + 94 | data[offset++]) /45; 95 | 96 | if (pts33Bit) { 97 | timestamp += 47721858.84; // 2^32 / 90 98 | } 99 | timestamp = Math.round(timestamp); 100 | logger.trace(`ID3 timestamp found: ${timestamp}`); 101 | this._timeStamp = timestamp; 102 | } 103 | break; 104 | default: 105 | break; 106 | } 107 | } 108 | } 109 | 110 | get hasTimeStamp() { 111 | return this._hasTimeStamp; 112 | } 113 | 114 | get timeStamp() { 115 | return this._timeStamp; 116 | } 117 | 118 | get length() { 119 | return this._length; 120 | } 121 | 122 | get payload() { 123 | return this._payload; 124 | } 125 | 126 | } 127 | 128 | export default ID3; 129 | 130 | -------------------------------------------------------------------------------- /src/client/hls/adts.js: -------------------------------------------------------------------------------- 1 | import {BitArray} from '../../core/util/binary.js'; 2 | import {AACParser} from '../../core/parsers/aac.js'; 3 | 4 | export class ADTS { 5 | 6 | static parseHeader(data) { 7 | let bits = new BitArray(data); 8 | bits.skipBits(15); 9 | let protectionAbs = bits.readBits(1); 10 | bits.skipBits(14); 11 | let len = bits.readBits(13); 12 | bits.skipBits(11); 13 | let cnt = bits.readBits(2); 14 | if (!protectionAbs) { 15 | bits.skipBits(16); 16 | } 17 | return {size: len-bits.bytepos, frameCount: cnt, offset: bits.bytepos} 18 | } 19 | 20 | static parseHeaderConfig(data) { 21 | let bits = new BitArray(data); 22 | bits.skipBits(15); 23 | let protectionAbs = bits.readBits(1); 24 | let profile = bits.readBits(2) + 1; 25 | let freq = bits.readBits(4); 26 | bits.skipBits(1); 27 | let channels = bits.readBits(3); 28 | bits.skipBits(4); 29 | let len = bits.readBits(13); 30 | bits.skipBits(11); 31 | let cnt = bits.readBits(2); 32 | if (!protectionAbs) { 33 | bits.skipBits(16); 34 | } 35 | 36 | let userAgent = navigator.userAgent.toLowerCase(); 37 | let configLen = 4; 38 | let extSamplingIdx; 39 | 40 | // firefox: freq less than 24kHz = AAC SBR (HE-AAC) 41 | if (userAgent.indexOf('firefox') !== -1) { 42 | if (freq >= 6) { 43 | profile = 5; 44 | configLen = 4; 45 | // HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies 46 | // there is a factor 2 between frame sample rate and output sample rate 47 | // multiply frequency by 2 (see table below, equivalent to substract 3) 48 | extSamplingIdx = freq - 3; 49 | } else { 50 | profile = 2; 51 | configLen = 2; 52 | extSamplingIdx = freq; 53 | } 54 | // Android : always use AAC 55 | } else if (userAgent.indexOf('android') !== -1) { 56 | profile = 2; 57 | configLen = 2; 58 | extSamplingIdx = freq; 59 | } else { 60 | /* for other browsers (chrome ...) 61 | always force audio type to be HE-AAC SBR, as some browsers do not support audio codec switch properly (like Chrome ...) 62 | */ 63 | profile = 5; 64 | configLen = 4; 65 | // if (manifest codec is HE-AAC or HE-AACv2) OR (manifest codec not specified AND frequency less than 24kHz) 66 | if (freq >= 6) { 67 | // HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies 68 | // there is a factor 2 between frame sample rate and output sample rate 69 | // multiply frequency by 2 (see table below, equivalent to substract 3) 70 | extSamplingIdx = freq - 3; 71 | } else { 72 | // if (manifest codec is AAC) AND (frequency less than 24kHz OR nb channel is 1) OR (manifest codec not specified and mono audio) 73 | // Chrome fails to play back with AAC LC mono when initialized with HE-AAC. This is not a problem with stereo. 74 | if (channels === 1) { 75 | profile = 2; 76 | configLen = 2; 77 | } 78 | extSamplingIdx = freq; 79 | } 80 | } 81 | 82 | 83 | let config = new Uint8Array(configLen); 84 | 85 | config[0] = profile << 3; 86 | // samplingFrequencyIndex 87 | config[0] |= (freq & 0x0E) >> 1; 88 | config[1] |= (freq & 0x01) << 7; 89 | // channelConfiguration 90 | config[1] |= channels << 3; 91 | if (profile === 5) { 92 | // adtsExtensionSampleingIndex 93 | config[1] |= (extSamplingIdx & 0x0E) >> 1; 94 | config[2] = (extSamplingIdx & 0x01) << 7; 95 | // adtsObjectType (force to 2, chrome is checking that object type is less than 5 ??? 96 | // https://chromium.googlesource.com/chromium/src.git/+/master/media/formats/mp4/aac.cc 97 | config[2] |= 2 << 2; 98 | config[3] = 0; 99 | } 100 | return { 101 | config: { 102 | config: config, 103 | codec: `mp4a.40.${profile}`, 104 | samplerate: AACParser.SampleRates[freq], 105 | channels: channels, 106 | }, 107 | size: len-bits.bytepos, 108 | frameCount: cnt, 109 | offset: bits.bytepos 110 | }; 111 | } 112 | } -------------------------------------------------------------------------------- /src/core/parsers/m3u8.js: -------------------------------------------------------------------------------- 1 | // adapted from https://github.com/kanongil/node-m3u8parse/blob/master/attrlist.js 2 | class AttrList { 3 | 4 | constructor(attrs) { 5 | if (typeof attrs === 'string') { 6 | attrs = AttrList.parseAttrList(attrs); 7 | } 8 | for(var attr in attrs){ 9 | if(attrs.hasOwnProperty(attr)) { 10 | this[attr] = attrs[attr]; 11 | } 12 | } 13 | this.attrs = attrs; 14 | } 15 | 16 | decimalInteger(attrName) { 17 | const intValue = parseInt(this[attrName], 10); 18 | if (intValue > Number.MAX_SAFE_INTEGER) { 19 | return Infinity; 20 | } 21 | return intValue; 22 | } 23 | 24 | hexadecimalInteger(attrName) { 25 | if(this[attrName]) { 26 | let stringValue = (this[attrName] || '0x').slice(2); 27 | stringValue = ((stringValue.length & 1) ? '0' : '') + stringValue; 28 | 29 | const value = new Uint8Array(stringValue.length / 2); 30 | for (let i = 0; i < stringValue.length / 2; i++) { 31 | value[i] = parseInt(stringValue.slice(i * 2, i * 2 + 2), 16); 32 | } 33 | return value; 34 | } else { 35 | return null; 36 | } 37 | } 38 | 39 | hexadecimalIntegerAsNumber(attrName) { 40 | const intValue = parseInt(this[attrName], 16); 41 | if (intValue > Number.MAX_SAFE_INTEGER) { 42 | return Infinity; 43 | } 44 | return intValue; 45 | } 46 | 47 | decimalFloatingPoint(attrName) { 48 | return parseFloat(this[attrName]); 49 | } 50 | 51 | enumeratedString(attrName) { 52 | return this[attrName]; 53 | } 54 | 55 | decimalResolution(attrName) { 56 | const res = /^(\d+)x(\d+)$/.exec(this[attrName]); 57 | if (res === null) { 58 | return undefined; 59 | } 60 | return { 61 | width: parseInt(res[1], 10), 62 | height: parseInt(res[2], 10) 63 | }; 64 | } 65 | 66 | static parseAttrList(input) { 67 | const re = /\s*(.+?)\s*=((?:\".*?\")|.*?)(?:,|$)/g; 68 | var match, attrs = {}; 69 | while ((match = re.exec(input)) !== null) { 70 | var value = match[2], quote = '"'; 71 | 72 | if (value.indexOf(quote) === 0 && 73 | value.lastIndexOf(quote) === (value.length-1)) { 74 | value = value.slice(1, -1); 75 | } 76 | attrs[match[1]] = value; 77 | } 78 | return attrs; 79 | } 80 | 81 | } 82 | 83 | export class M3U8Parser { 84 | static get TYPE_CHUNK() {return 0;} 85 | static get TYPE_PLAYLIST() {return 1;} 86 | // TODO: parse master playlists 87 | static parse(playlist, baseUrl='') { 88 | playlist = playlist.replace(/\r/, '').split('\n'); 89 | if (playlist.shift().indexOf('#EXTM3U') < 0) { 90 | throw new Error("Bad playlist"); 91 | } 92 | let chunkList = []; 93 | let playlistList = []; 94 | let expectedUrl = false; 95 | let cont = {}; 96 | let urlType=null; 97 | while (playlist.length) { 98 | let entry = playlist.shift().replace(/\r/, ''); 99 | if (expectedUrl) { 100 | if (entry) { 101 | cont.url = entry.startsWith('http')?entry:`${baseUrl}/${entry}`; 102 | switch (urlType) { 103 | case M3U8Parser.TYPE_CHUNK: 104 | chunkList.push(cont); 105 | break; 106 | case M3U8Parser.TYPE_PLAYLIST: 107 | playlistList.push(cont); 108 | break; 109 | } 110 | cont={}; 111 | expectedUrl = false; 112 | } 113 | } else { 114 | if (entry.startsWith('#EXTINF')) { 115 | // TODO: it's unsafe 116 | cont.duration = Number(entry.split(':')[1].split(',')[0]); 117 | urlType = M3U8Parser.TYPE_CHUNK; 118 | expectedUrl = true; 119 | } else if (entry.startsWith('#EXT-X-STREAM-INF')){ 120 | let props = entry.split(':')[1]; 121 | let al = new AttrList(props); 122 | for (let prop in al.attrs) { 123 | cont[prop.toLowerCase()]= al.attrs[prop]; 124 | } 125 | urlType = M3U8Parser.TYPE_PLAYLIST; 126 | expectedUrl = true; 127 | } 128 | } 129 | } 130 | return {chunks: chunkList, playlists: playlistList} 131 | } 132 | } -------------------------------------------------------------------------------- /src/core/remuxer/base.js: -------------------------------------------------------------------------------- 1 | import {getTagged} from '../../deps/bp_logger.js'; 2 | 3 | const Log = getTagged('remuxer:base'); 4 | let track_id = 1; 5 | export class BaseRemuxer { 6 | 7 | static get MP4_TIMESCALE() { return 90000;} 8 | 9 | // TODO: move to ts parser 10 | // static PTSNormalize(value, reference) { 11 | // 12 | // let offset; 13 | // if (reference === undefined) { 14 | // return value; 15 | // } 16 | // if (reference < value) { 17 | // // - 2^33 18 | // offset = -8589934592; 19 | // } else { 20 | // // + 2^33 21 | // offset = 8589934592; 22 | // } 23 | // /* PTS is 33bit (from 0 to 2^33 -1) 24 | // if diff between value and reference is bigger than half of the amplitude (2^32) then it means that 25 | // PTS looping occured. fill the gap */ 26 | // while (Math.abs(value - reference) > 4294967296) { 27 | // value += offset; 28 | // } 29 | // return value; 30 | // } 31 | 32 | static getTrackID() { 33 | return track_id++; 34 | } 35 | 36 | constructor(timescale, scaleFactor, params) { 37 | this.timeOffset = 0; 38 | this.timescale = timescale; 39 | this.scaleFactor = scaleFactor; 40 | this.readyToDecode = false; 41 | this.samples = []; 42 | this.seq = 1; 43 | this.tsAlign = 1; 44 | } 45 | 46 | scaled(timestamp) { 47 | return timestamp / this.scaleFactor; 48 | } 49 | 50 | unscaled(timestamp) { 51 | return timestamp * this.scaleFactor; 52 | } 53 | 54 | remux(unit) { 55 | if (unit) { 56 | this.samples.push({ 57 | unit: unit, 58 | pts: unit.pts, 59 | dts: unit.dts 60 | }); 61 | return true; 62 | } 63 | return false; 64 | } 65 | 66 | static toMS(timestamp) { 67 | return timestamp/90; 68 | } 69 | 70 | setConfig(config) { 71 | 72 | } 73 | 74 | insertDscontinuity() { 75 | this.samples.push(null); 76 | } 77 | 78 | init(initPTS, initDTS, shouldInitialize=true) { 79 | this.initPTS = Math.min(initPTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); 80 | this.initDTS = Math.min(initDTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); 81 | Log.debug(`Initial pts=${this.initPTS} dts=${this.initDTS} offset=${this.unscaled(this.timeOffset)}`); 82 | this.initialized = shouldInitialize; 83 | } 84 | 85 | flush() { 86 | this.seq++; 87 | this.mp4track.len = 0; 88 | this.mp4track.samples = []; 89 | } 90 | 91 | static dtsSortFunc(a,b) { 92 | return (a.dts-b.dts); 93 | } 94 | 95 | static groupByDts(gop) { 96 | const groupBy = (xs, key) => { 97 | return xs.reduce((rv, x) => { 98 | (rv[x[key]] = rv[x[key]] || []).push(x); 99 | return rv; 100 | }, {}); 101 | }; 102 | return groupBy(gop, 'dts'); 103 | } 104 | 105 | getPayloadBase(sampleFunction, setupSample) { 106 | if (!this.readyToDecode || !this.initialized || !this.samples.length) return null; 107 | this.samples.sort(BaseRemuxer.dtsSortFunc); 108 | return true; 109 | // 110 | // let payload = new Uint8Array(this.mp4track.len); 111 | // let offset = 0; 112 | // let samples=this.mp4track.samples; 113 | // let mp4Sample, lastDTS, pts, dts; 114 | // 115 | // while (this.samples.length) { 116 | // let sample = this.samples.shift(); 117 | // if (sample === null) { 118 | // // discontinuity 119 | // this.nextDts = undefined; 120 | // break; 121 | // } 122 | // 123 | // let unit = sample.unit; 124 | // 125 | // pts = Math.round((sample.pts - this.initDTS)/this.tsAlign)*this.tsAlign; 126 | // dts = Math.round((sample.dts - this.initDTS)/this.tsAlign)*this.tsAlign; 127 | // // ensure DTS is not bigger than PTS 128 | // dts = Math.min(pts, dts); 129 | // 130 | // // sampleFunction(pts, dts); // TODO: 131 | // 132 | // // mp4Sample = setupSample(unit, pts, dts); // TODO: 133 | // 134 | // payload.set(unit.getData(), offset); 135 | // offset += unit.getSize(); 136 | // 137 | // samples.push(mp4Sample); 138 | // lastDTS = dts; 139 | // } 140 | // if (!samples.length) return null; 141 | // 142 | // // samplesPostFunction(samples); // TODO: 143 | // 144 | // return new Uint8Array(payload.buffer, 0, this.mp4track.len); 145 | } 146 | } -------------------------------------------------------------------------------- /src/core/remuxer/aac.js: -------------------------------------------------------------------------------- 1 | import {getTagged} from '../../deps/bp_logger.js'; 2 | import {MSE} from '../presentation/mse.js'; 3 | import {BaseRemuxer} from './base.js'; 4 | 5 | const Log = getTagged("remuxer:aac"); 6 | // TODO: asm.js 7 | export class AACRemuxer extends BaseRemuxer { 8 | 9 | constructor(timescale, scaleFactor = 1, params={}) { 10 | super(timescale, scaleFactor); 11 | 12 | this.codecstring=MSE.CODEC_AAC; 13 | this.units = []; 14 | this.initDTS = undefined; 15 | this.nextAacPts = undefined; 16 | this.lastPts = 0; 17 | this.firstDTS = 0; 18 | this.firstPTS = 0; 19 | this.duration = params.duration || 1; 20 | this.initialized = false; 21 | 22 | this.mp4track={ 23 | id:BaseRemuxer.getTrackID(), 24 | type: 'audio', 25 | fragmented:true, 26 | channelCount:0, 27 | audiosamplerate: this.timescale, 28 | duration: 0, 29 | timescale: this.timescale, 30 | volume: 1, 31 | samples: [], 32 | config: '', 33 | len: 0 34 | }; 35 | if (params.config) { 36 | this.setConfig(params.config); 37 | } 38 | } 39 | 40 | setConfig(config) { 41 | this.mp4track.channelCount = config.channels; 42 | this.mp4track.audiosamplerate = config.samplerate; 43 | if (!this.mp4track.duration) { 44 | this.mp4track.duration = (this.duration?this.duration:1)*config.samplerate; 45 | } 46 | this.mp4track.timescale = config.samplerate; 47 | this.mp4track.config = config.config; 48 | this.mp4track.codec = config.codec; 49 | this.timescale = config.samplerate; 50 | this.scaleFactor = BaseRemuxer.MP4_TIMESCALE / config.samplerate; 51 | this.expectedSampleDuration = 1024 * this.scaleFactor; 52 | this.readyToDecode = true; 53 | } 54 | 55 | remux(aac) { 56 | if (super.remux.call(this, aac)) { 57 | this.mp4track.len += aac.getSize(); 58 | } 59 | } 60 | 61 | getPayload() { 62 | if (!this.readyToDecode || !this.samples.length) return null; 63 | this.samples.sort(function(a, b) { 64 | return (a.dts-b.dts); 65 | }); 66 | 67 | let payload = new Uint8Array(this.mp4track.len); 68 | let offset = 0; 69 | let samples=this.mp4track.samples; 70 | let mp4Sample, lastDTS, pts, dts; 71 | 72 | while (this.samples.length) { 73 | let sample = this.samples.shift(); 74 | if (sample === null) { 75 | // discontinuity 76 | this.nextDts = undefined; 77 | break; 78 | } 79 | let unit = sample.unit; 80 | pts = sample.pts - this.initDTS; 81 | dts = sample.dts - this.initDTS; 82 | 83 | if (lastDTS === undefined) { 84 | if (this.nextDts) { 85 | let delta = Math.round(this.scaled(pts - this.nextAacPts)); 86 | // if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments 87 | if (/*contiguous || */Math.abs(delta) < 600) { 88 | // log delta 89 | if (delta) { 90 | if (delta > 0) { 91 | Log.log(`${delta} ms hole between AAC samples detected,filling it`); 92 | // if we have frame overlap, overlapping for more than half a frame duraion 93 | } else if (delta < -12) { 94 | // drop overlapping audio frames... browser will deal with it 95 | Log.log(`${(-delta)} ms overlapping between AAC samples detected, drop frame`); 96 | this.mp4track.len -= unit.getSize(); 97 | continue; 98 | } 99 | // set DTS to next DTS 100 | pts = dts = this.nextAacPts; 101 | } 102 | } 103 | } 104 | // remember first PTS of our aacSamples, ensure value is positive 105 | this.firstDTS = Math.max(0, dts); 106 | } 107 | 108 | mp4Sample = { 109 | size: unit.getSize(), 110 | cts: 0, 111 | duration:1024, 112 | flags: { 113 | isLeading: 0, 114 | isDependedOn: 0, 115 | hasRedundancy: 0, 116 | degradPrio: 0, 117 | dependsOn: 1 118 | } 119 | }; 120 | 121 | payload.set(unit.getData(), offset); 122 | offset += unit.getSize(); 123 | samples.push(mp4Sample); 124 | lastDTS = dts; 125 | } 126 | if (!samples.length) return null; 127 | this.nextDts =pts+this.expectedSampleDuration; 128 | return new Uint8Array(payload.buffer, 0, this.mp4track.len); 129 | } 130 | } 131 | //test.bundle.js:42 [remuxer:h264] skip frame from the past at DTS=18397972271140676 with expected DTS=18397998040950484 -------------------------------------------------------------------------------- /frameworks/react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | Streamedian RTSP player example 14 | 15 | 16 | 17 | 18 |

Streamedian RTSP Player Example using the React framework

19 |
20 |


Have any suggestions to improve our player?
Feel free to leave comments or ideas email: streamedian.player@gmail.com

21 |

View HTML5 RTSP video player log

22 |
23 | 24 | 25 | 26 | 27 |

28 | 29 | How to use the player in the global network 30 |

31 | With an empty license file, you can only watch the stream on your computer locally (intranet).
32 | If you would like to stream into the global network please take a key to activate the license.
33 | You have personal 1 month validity key in the personal cabinet.
34 | To activate key, please, use the activation application that is placed: 35 |

36 |

37 | Windows: C:\Program Files\Streamedian\WS RTSP Proxy Server\activation_app
38 | Mac OS: /Library/Application Support/Streamedian/WS RTSP Proxy Server/activation_app
39 | Linux (Ubunty, Debian, Centos, Fedora ): /usr/bin/wsp/activation_app
40 |

41 |

For more information go to documentation

42 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /frameworks/react/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/core/parsers/pes.js: -------------------------------------------------------------------------------- 1 | import {appendByteArray} from '../util/binary.js'; 2 | 3 | export class PESAsm { 4 | 5 | constructor() { 6 | this.fragments = []; 7 | this.pesLength=0; 8 | this.pesPkt = null; 9 | } 10 | 11 | parse(frag) { 12 | 13 | if (this.extPresent) { 14 | let ext = this.parseExtension(frag); 15 | ext.data = frag.subarray(ext.offset); 16 | } else { 17 | return null; 18 | } 19 | } 20 | 21 | parseHeader() { 22 | let hdr = this.fragments[0]; 23 | let pesPrefix = (hdr[0] << 16) + (hdr[1] << 8) + hdr[2]; 24 | this.extPresent = ![0xbe, 0xbf].includes(hdr[3]); 25 | if (pesPrefix === 1) { 26 | let pesLength = (hdr[4] << 8) + hdr[5]; 27 | if (pesLength) { 28 | this.pesLength = pesLength; 29 | this.hasLength = true; 30 | } else { 31 | this.hasLength = false; 32 | this.pesPkt = null; 33 | } 34 | return true; 35 | } 36 | return false; 37 | } 38 | 39 | static PTSNormalize(value, reference) { 40 | 41 | let offset; 42 | if (reference === undefined) { 43 | return value; 44 | } 45 | if (reference < value) { 46 | // - 2^33 47 | offset = -8589934592; 48 | } else { 49 | // + 2^33 50 | offset = 8589934592; 51 | } 52 | /* PTS is 33bit (from 0 to 2^33 -1) 53 | if diff between value and reference is bigger than half of the amplitude (2^32) then it means that 54 | PTS looping occured. fill the gap */ 55 | while (Math.abs(value - reference) > 4294967296) { 56 | value += offset; 57 | } 58 | return value; 59 | } 60 | 61 | parseExtension(frag) { 62 | let pesFlags, pesPrefix, pesLen, pesHdrLen, pesPts, pesDts, payloadStartOffset; 63 | pesFlags = frag[1]; 64 | if (pesFlags & 0xC0) { 65 | /* PES header described here : http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 66 | as PTS / DTS is 33 bit we cannot use bitwise operator in JS, 67 | as Bitwise operators treat their operands as a sequence of 32 bits */ 68 | pesPts = (frag[3] & 0x0E) * 536870912 +// 1 << 29 69 | (frag[4] & 0xFF) * 4194304 +// 1 << 22 70 | (frag[5] & 0xFE) * 16384 +// 1 << 14 71 | (frag[6] & 0xFF) * 128 +// 1 << 7 72 | (frag[7] & 0xFE) / 2; 73 | // check if greater than 2^32 -1 74 | if (pesPts > 4294967295) { 75 | // decrement 2^33 76 | pesPts -= 8589934592; 77 | } 78 | if (pesFlags & 0x40) { 79 | pesDts = (frag[8] & 0x0E ) * 536870912 +// 1 << 29 80 | (frag[9] & 0xFF ) * 4194304 +// 1 << 22 81 | (frag[10] & 0xFE ) * 16384 +// 1 << 14 82 | (frag[11] & 0xFF ) * 128 +// 1 << 7 83 | (frag[12] & 0xFE ) / 2; 84 | // check if greater than 2^32 -1 85 | if (pesDts > 4294967295) { 86 | // decrement 2^33 87 | pesDts -= 8589934592; 88 | } 89 | } else { 90 | pesDts = pesPts; 91 | } 92 | 93 | pesHdrLen = frag[2]; 94 | payloadStartOffset = pesHdrLen + 9; 95 | 96 | // TODO: normalize pts/dts 97 | return {offset: payloadStartOffset, pts: pesPts, dts: pesDts}; 98 | } else { 99 | return null; 100 | } 101 | } 102 | 103 | feed(frag, shouldParse) { 104 | 105 | let res = null; 106 | if (shouldParse && this.fragments.length) { 107 | if (!this.parseHeader()) { 108 | throw new Error("Invalid PES packet"); 109 | } 110 | 111 | let offset = 6; 112 | let parsed = {}; 113 | if (this.extPresent) { 114 | // TODO: make sure fragment have necessary length 115 | parsed = this.parseExtension(this.fragments[0].subarray(6)); 116 | offset = parsed.offset; 117 | } 118 | if (!this.pesPkt) { 119 | this.pesPkt = new Uint8Array(this.pesLength); 120 | } 121 | 122 | let poffset = 0; 123 | while (this.pesLength && this.fragments.length) { 124 | let data = this.fragments.shift(); 125 | if (offset) { 126 | if (data.byteLength < offset) { 127 | offset -= data.byteLength; 128 | continue; 129 | } else { 130 | data = data.subarray(offset); 131 | this.pesLength -= offset - (this.hasLength?6:0); 132 | offset = 0; 133 | } 134 | } 135 | this.pesPkt.set(data, poffset); 136 | poffset += data.byteLength; 137 | this.pesLength -= data.byteLength; 138 | } 139 | res = {data:this.pesPkt, pts: parsed.pts, dts: parsed.dts}; 140 | } else { 141 | this.pesPkt = null; 142 | } 143 | this.pesLength += frag.byteLength; 144 | 145 | if (this.fragments.length && this.fragments[this.fragments.length-1].byteLength < 6) { 146 | this.fragments[this.fragments.length-1] = appendByteArray(this.fragments[0], frag); 147 | } else { 148 | this.fragments.push(frag); 149 | } 150 | 151 | return res; 152 | } 153 | } -------------------------------------------------------------------------------- /Server(NodeJS): -------------------------------------------------------------------------------- 1 | /** 2 | * Created by wendy on 2017/4/13. 3 | * 4/14 可以正常播放,但是花屏。。。效果不好啊 4 | */ 5 | var wsserver = new (require('ws').Server)({port:1104}); 6 | var net = require('net'); 7 | var channelIndex = 1; 8 | wsserver.on("close", function () { 9 | console.log("close",conn); 10 | }); 11 | var channelsocket = {}; 12 | wsserver.on('connection',function(conn) { 13 | console.log("protocol",conn.protocol); 14 | var protocol = conn.protocol; 15 | if(protocol == "control") { 16 | conn.onmessage = function (msg) { 17 | console.log(msg.data); 18 | var res = wspParse(msg.data); 19 | if(res.msg == "INIT"){ 20 | var ipIndex = _ip2int(res.data.host); 21 | 22 | var channel = channelIndex++; 23 | conn.channel = channel; 24 | InitChannel(channel,ipIndex,res.data.host,res.data.port,function(){ 25 | var msg = wspMsg("200","INIT OK",res.data.seq,{"channel":channel}); 26 | conn.send(msg); 27 | },function(msgFail){ 28 | var msg = wspMsg("501",msgFail,res.data.seq); 29 | conn.send(msg); 30 | }); 31 | 32 | 33 | } 34 | else if(res.msg == "WRAP"){ 35 | //console.log(res.payload); 36 | if(channelsocket[conn.channel]) 37 | { 38 | channelsocket[conn.channel].outControlData = true; 39 | channelsocket[conn.channel].once('data',function(data){ 40 | console.log(data.toString('utf8')); 41 | var msg = wspMsg("200","WRAP OK",res.data.seq,{"channel":conn.channel},data); 42 | conn.send(msg); 43 | }); 44 | channelsocket[conn.channel].write(res.payload); 45 | } 46 | } 47 | 48 | } 49 | } 50 | else if(protocol == "data"){ 51 | //建立pipe 52 | conn.onmessage = function (msg) { 53 | console.log(msg.data); 54 | var res = wspParse(msg.data); 55 | if(res.msg == "JOIN") { 56 | channelsocket[res.data.channel].on('rtpData', function (data) { 57 | console.log(data); 58 | conn.send(data); 59 | }); 60 | 61 | var msg = wspMsg("200", "JOIN OK", res.data.seq); 62 | conn.send(msg); 63 | 64 | 65 | } 66 | } 67 | } 68 | }); 69 | function _ip2int(ip) 70 | { 71 | var num = 0; 72 | ip = ip.split("."); 73 | num = Number(ip[0]) * 256 * 256 * 256 + Number(ip[1]) * 256 * 256 + Number(ip[2]) * 256 + Number(ip[3]); 74 | num = num >>> 0; 75 | return num; 76 | } 77 | function InitChannel(channel,ipIndex,ip,prt,okFunc,failFunc){ 78 | 79 | var sock = net.connect({host:ip,port:prt},function(){ 80 | channelsocket[channel] = sock; 81 | okFunc(); 82 | sock.connectInfo = true; 83 | 84 | sock.rtpBuffer = new Buffer(2048); 85 | sock.on('data',function(data){ 86 | if(sock.outControlData) 87 | { 88 | sock.outControlData = false; 89 | return; 90 | } 91 | 92 | var flag = 0; 93 | if(sock.SubBuffer && sock.SubBufferLen>0){ 94 | flag = sock.SubBuffer.length - sock.SubBufferLen; 95 | data.copy(sock.SubBuffer,sock.SubBufferLen, 0, flag - 1); 96 | sock.emit("rtpData",sock.SubBuffer); 97 | 98 | sock.SubBufferLen = 0; 99 | } 100 | 101 | while(flag < data.length) { 102 | var len = data.readUIntBE(flag + 2, 2); 103 | sock.SubBuffer = new Buffer(4 + len); 104 | 105 | if ((flag+4+len) <= data.length) 106 | { 107 | data.copy(sock.SubBuffer, 0, flag, flag + len - 1); 108 | sock.emit("rtpData",sock.SubBuffer); 109 | sock.SubBufferLen = 0; 110 | } 111 | else { 112 | data.copy(sock.SubBuffer, 0, flag,data.length - 1); 113 | sock.SubBufferLen = data.length - flag; 114 | } 115 | flag += 4; 116 | flag += len; 117 | } 118 | }); 119 | 120 | }).on('error',function(e){ 121 | //clean all client; 122 | console.log(e); 123 | }); 124 | sock.setTimeout(1000 * 3,function() { 125 | if(!sock.connectInfo) { 126 | console.log("time out"); 127 | failFunc("relink host[" + ip + "] time out"); 128 | sock.destroy(); 129 | } 130 | }); 131 | 132 | sock.on('close',function(code){ 133 | //关闭所有子项目 134 | 135 | }); 136 | } 137 | 138 | 139 | function wspParse(data){ 140 | var payIdx = data.indexOf('\r\n\r\n'); 141 | var lines = data.substr(0, payIdx).split('\r\n'); 142 | var hdr = lines.shift().match(new RegExp('WSP/1.1\\s+(.+)')); 143 | if (hdr) { 144 | var res = { 145 | msg: hdr[1], 146 | data: {}, 147 | payload: '' 148 | }; 149 | while (lines.length) { 150 | var line = lines.shift(); 151 | if (line) { 152 | var subD = line.split(':'); 153 | res.data[subD[0]] = subD[1].trim(); 154 | } else { 155 | break; 156 | } 157 | } 158 | res.payload = data.substr(payIdx+4); 159 | return res; 160 | } 161 | return null; 162 | } 163 | function wspMsg(code,msg,seq,data,play){ 164 | 165 | var msg = "WSP/1.1 " + code + " " + msg + "\r\n"; 166 | msg += "seq:" + seq ; 167 | if(data) { 168 | for (var i in data) { 169 | msg += "\r\n"; 170 | msg += i.toString() + ":" + data[i].toString(); 171 | } 172 | } 173 | msg += "\r\n\r\n"; 174 | if(play) 175 | msg += play; 176 | 177 | return msg; 178 | } 179 | -------------------------------------------------------------------------------- /src/client/rtsp/stream.js: -------------------------------------------------------------------------------- 1 | import {getTagged} from '../../deps/bp_logger.js'; 2 | 3 | import {RTSPClientSM as RTSPClient} from './client.js'; 4 | import {Url} from '../../core/util/url.js'; 5 | import {RTSPError} from "./client"; 6 | 7 | const LOG_TAG = "rtsp:stream"; 8 | const Log = getTagged(LOG_TAG); 9 | 10 | export class RTSPStream { 11 | 12 | constructor(client, track) { 13 | this.state = null; 14 | this.client = client; 15 | this.track = track; 16 | this.rtpChannel = 1; 17 | 18 | this.stopKeepAlive(); 19 | this.keepaliveInterval = null; 20 | this.keepaliveTime = 30000; 21 | } 22 | 23 | reset() { 24 | this.stopKeepAlive(); 25 | this.client.forgetRTPChannel(this.rtpChannel); 26 | this.client = null; 27 | this.track = null; 28 | } 29 | 30 | start(lastSetupPromise = null) { 31 | if (lastSetupPromise != null) { 32 | // if a setup was already made, use the same session 33 | return lastSetupPromise.then((obj) => this.sendSetup(obj.session)) 34 | } else { 35 | return this.sendSetup(); 36 | } 37 | } 38 | 39 | stop() { 40 | return this.sendTeardown(); 41 | } 42 | 43 | getSetupURL(track) { 44 | let sessionBlock = this.client.sdp.getSessionBlock(); 45 | if (Url.isAbsolute(track.control)) { 46 | return track.control; 47 | } else if (Url.isAbsolute(`${sessionBlock.control}${track.control}`)) { 48 | return `${sessionBlock.control}${track.control}`; 49 | } else if (Url.isAbsolute(`${this.client.contentBase}${track.control}`)) { 50 | /* Check the end of the address for a separator */ 51 | if (this.client.contentBase[this.client.contentBase.length - 1] !== '/') { 52 | return `${this.client.contentBase}/${track.control}`; 53 | } 54 | 55 | /* Should probably check session level control before this */ 56 | return `${this.client.contentBase}${track.control}`; 57 | } 58 | else {//need return default 59 | return track.control; 60 | } 61 | Log.error('Can\'t determine track URL from ' + 62 | 'block.control:' + track.control + ', ' + 63 | 'session.control:' + sessionBlock.control + ', and ' + 64 | 'content-base:' + this.client.contentBase); 65 | } 66 | 67 | getControlURL() { 68 | let ctrl = this.client.sdp.getSessionBlock().control; 69 | if (Url.isAbsolute(ctrl)) { 70 | return ctrl; 71 | } else if (!ctrl || '*' === ctrl) { 72 | return this.client.contentBase; 73 | } else { 74 | return `${this.client.contentBase}${ctrl}`; 75 | } 76 | } 77 | 78 | sendKeepalive() { 79 | if (this.client.methods.includes('GET_PARAMETER')) { 80 | return this.client.sendRequest('GET_PARAMETER', this.getSetupURL(this.track), { 81 | 'Session': this.session 82 | }); 83 | } else { 84 | return this.client.sendRequest('OPTIONS', '*'); 85 | } 86 | } 87 | 88 | stopKeepAlive() { 89 | clearInterval(this.keepaliveInterval); 90 | } 91 | 92 | startKeepAlive() { 93 | this.keepaliveInterval = setInterval(() => { 94 | this.sendKeepalive().catch((e) => { 95 | Log.error(e); 96 | if (e instanceof RTSPError) { 97 | if (Number(e.data.parsed.code) == 501) { 98 | return; 99 | } 100 | } 101 | this.client.reconnect(); 102 | }); 103 | }, this.keepaliveTime); 104 | } 105 | 106 | sendRequest(_cmd, _params = {}) { 107 | let params = {}; 108 | if (this.session) { 109 | params['Session'] = this.session; 110 | } 111 | Object.assign(params, _params); 112 | return this.client.sendRequest(_cmd, this.getControlURL(), params); 113 | } 114 | 115 | sendSetup(session = null) { 116 | this.state = RTSPClient.STATE_SETUP; 117 | this.rtpChannel = this.client.interleaveChannelIndex; 118 | let interleavedChannels = this.client.interleaveChannelIndex++ + "-" + this.client.interleaveChannelIndex++; 119 | let params = { 120 | 'Transport': `RTP/AVP/TCP;unicast;interleaved=${interleavedChannels}`, 121 | 'Date': new Date().toUTCString() 122 | }; 123 | if(session){ 124 | params.Session = session; 125 | } 126 | return this.client.sendRequest('SETUP', this.getSetupURL(this.track), params).then((_data) => { 127 | this.session = _data.headers['session'].split(';'); 128 | let transport = _data.headers['transport']; 129 | if (transport) { 130 | let interleaved = transport.match(/interleaved=([0-9]+)-([0-9]+)/)[1]; 131 | if (interleaved) { 132 | this.rtpChannel = Number(interleaved); 133 | } 134 | } 135 | 136 | let sessionParamsChunks = this.session.slice(1); 137 | let sessionParams = {}; 138 | for (let chunk of sessionParamsChunks) { 139 | let kv = chunk.split('='); 140 | sessionParams[kv[0]] = kv[1]; 141 | } 142 | if (sessionParams['timeout']) { 143 | this.keepaliveInterval = Number(sessionParams['timeout']) * 500; // * 1000 / 2 144 | } 145 | /*if (!/RTP\/AVP\/TCP;unicast;interleaved=/.test(_data.headers["transport"])) { 146 | // TODO: disconnect stream and notify client 147 | throw new Error("Connection broken"); 148 | }*/ 149 | this.client.useRTPChannel(this.rtpChannel); 150 | this.startKeepAlive(); 151 | return {track: this.track, data: _data, session: this.session[0]}; 152 | }); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /plugins/videojs/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Streamedian RTSP player example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 |
28 | 29 | 30 | 32 | 33 |


Have any suggestions to improve our player?
Feel free to leave comments or ideas email: streamedian.player@gmail.com

34 |

View HTML5 RTSP video player log

35 |
36 | 37 | 38 | 39 | 40 |

41 | 42 | 136 | 137 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /plugins/flowplayer/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Streamedian RTSP player example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 | 25 |
26 | 27 |


Have any suggestions to improve our player?
Feel free to leave comments or ideas email: streamedian.player@gmail.com

28 |

View HTML5 RTSP video player log

29 |
30 | 31 | 32 | 33 | 34 |

35 | 36 | 130 | 131 | 187 | 188 | -------------------------------------------------------------------------------- /plugins/clappr/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Streamedian RTSP player example 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 | 21 | 22 |
23 | 24 | 25 | 26 |


Have any suggestions to improve our player?
Feel free to leave comments or ideas email: streamedian.player@gmail.com

27 |

View HTML5 RTSP video player log

28 |
29 | 30 | 31 | 32 | 33 |

34 | 35 | 36 | 37 | 38 | 132 | 133 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /plugins/flowplayer/Readme.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Streamedian is a Javascript library which implements RTSP client for watching live streams in your browser 4 | that works directly on top of a standard HTML