diff --git a/.gitignore b/.gitignore index 66fd13c..5f50b92 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,31 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a *.so -*.dylib -# Test binary, built with `go test -c` +# Folders +_obj +_test +logs +bin + +# file +tomatox +routetable.json +users.json + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe *.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ +*.prof diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100755 index 0000000..589bf85 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "tomatox", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceRoot}", + "env": {}, + "args": [] + }, + { + "name": "tomatox -log-tofile", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceRoot}", + "env": {}, + "args": ["-log-tofile"] + }, + { + "name": "tomatox -h", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceRoot}", + "env": {}, + "args": ["-h"] + } + ] +} \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100755 index 0000000..a640775 --- /dev/null +++ b/config/config.go @@ -0,0 +1,37 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package config + +import ( + "flag" +) + +// config 服务配置 +type config struct { + ListenAddr string `json:"listen"` // 服务侦听地址和端口 + Auth bool `json:"auth"` // 启用安全验证 + CacheGop bool `json:"cache_gop"` // 缓存图像组,以便提高播放端打开速度,但内存需求大 + HlsPath string `json:"hlspath,omitempty"` // Hls临时缓存目录 + Profile bool `json:"profile"` // 是否启动Profile + TLS *TLSConfig `json:"tls,omitempty"` // https安全端口交互 + Routetable *ProviderConfig `json:"routetable,omitempty"` // 路由表 + Users *ProviderConfig `json:"users,omitempty"` // 用户 + Log LogConfig `json:"log"` // 日志配置 +} + +func (c *config) initFlags() { + // 服务的端口 + flag.StringVar(&c.ListenAddr, "listen", ":554", "Set server listen address") + flag.BoolVar(&c.Auth, "auth", false, + "Determines if requires permission verification to access stream media") + flag.BoolVar(&c.CacheGop, "cachegop", false, + "Determines if Gop should be cached to memory") + flag.StringVar(&c.HlsPath, "hlspath", "", "Set HLS live dir") + flag.BoolVar(&c.Profile, "pprof", false, + "Determines if profile enabled") + + // 初始化日志配置 + c.Log.initFlags() +} diff --git a/config/global.go b/config/global.go new file mode 100755 index 0000000..e91c264 --- /dev/null +++ b/config/global.go @@ -0,0 +1,229 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package config + +import ( + "net" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + cfg "github.com/cnotch/loader" + "github.com/cnotch/tomatox/provider/auth" + "github.com/cnotch/tomatox/utils" + "github.com/cnotch/xlog" +) + +// 服务名 +const ( + Vendor = "CAOHONGJU" + Name = "tomatox" + Version = "V1.0.0" +) + +var ( + globalC *config + consoleAppDir string + demosAppDir string +) + +// InitConfig 初始化 Config +func InitConfig() { + exe, err := os.Executable() + if err != nil { + xlog.Panic(err.Error()) + } + + configPath := filepath.Join(filepath.Dir(exe), Name+".conf") + consoleAppDir = filepath.Join(filepath.Dir(exe), "console") + demosAppDir = filepath.Join(filepath.Dir(exe), "demos") + + globalC = new(config) + globalC.initFlags() + + // 创建或加载配置文件 + if err := cfg.Load(globalC, + &cfg.JSONLoader{Path: configPath, CreatedIfNonExsit: true}, + &cfg.EnvLoader{Prefix: strings.ToUpper(Name)}, + &cfg.FlagLoader{}); err != nil { + // 异常,直接退出 + xlog.Panic(err.Error()) + } + + if globalC.HlsPath != "" { + if !filepath.IsAbs(globalC.HlsPath) { + globalC.HlsPath = filepath.Join(filepath.Dir(exe), globalC.HlsPath) + } + + _, err = os.Stat(globalC.HlsPath) + if err != nil { + if os.IsNotExist(err) { + if err = os.MkdirAll(globalC.HlsPath, os.ModePerm); err != nil { + panic(err) + } + } else { + panic(err) + } + } + } + + // 初始化日志 + globalC.Log.initLogger() +} + +// Addr Listen addr +func Addr() string { + if globalC == nil { + return ":554" + } + return globalC.ListenAddr +} + +// Auth 是否启用验证 +func Auth() bool { + if globalC == nil { + return false + } + return globalC.Auth +} + +// CacheGop 是否Cache Gop +func CacheGop() bool { + if globalC == nil { + return false + } + return globalC.CacheGop +} + +// Profile 是否启动 Http Profile +func Profile() bool { + if globalC == nil { + return false + } + return globalC.Profile +} + +// GetTLSConfig 获取TLSConfig +func GetTLSConfig() *TLSConfig { + if globalC == nil { + return nil + } + return globalC.TLS +} + +// ConsoleAppDir 管理员控制台应用的目录 +func ConsoleAppDir() (string, bool) { + if consoleAppDir == "" { + return "", false + } + finfo, err := os.Stat(consoleAppDir) + if err != nil || !finfo.IsDir() { + return "", false + } + return consoleAppDir, true +} + +// DemosAppDir 例子应用目录 +func DemosAppDir() (string, bool) { + if demosAppDir == "" { + return "", false + } + finfo, err := os.Stat(demosAppDir) + if err != nil || !finfo.IsDir() { + return "", false + } + return demosAppDir, true +} + +// NetTimeout 返回网络超时设置 +func NetTimeout() time.Duration { + return time.Second * 45 +} + +// NetHeartbeatInterval 返回网络心跳间隔 +func NetHeartbeatInterval() time.Duration { + return time.Second * 30 +} + +// NetBufferSize 网络通讯时的BufferSize +func NetBufferSize() int { + return 128 * 1024 +} + +// NetFlushRate 网络刷新频率 +func NetFlushRate() int { + return 30 +} + +// RtspAuthMode rtsp 认证模式 +func RtspAuthMode() auth.Mode { + if globalC == nil || !globalC.Auth { + return auth.NoneAuth + } + return auth.DigestAuth +} + +// MulticastTTL 组播TTL值 +func MulticastTTL() int { + return 127 +} + +// ChunkSize Rtmp ChunkSize +func ChunkSize(ip net.IP) uint32 { + if utils.IsLocalhostIP(ip) { + return 48 * 1024 + } + return 16 * 1024 +} + +// HlsEnable 是否启动 Hls +func HlsEnable() bool { + return true +} + +// HlsFragment TS片段时长(s) +func HlsFragment() int { + return 3 +} + +// HlsPath hls 存储目录 +func HlsPath() string { + if globalC == nil { + return "" + } + return globalC.HlsPath +} + +// LoadRoutetableProvider 加载路由表提供者 +func LoadRoutetableProvider(providers ...Provider) Provider { + if globalC == nil { + return LoadProvider(nil, providers...) + } + return LoadProvider(globalC.Routetable, providers...) +} + +// LoadUsersProvider 加载用户提供者 +func LoadUsersProvider(providers ...Provider) Provider { + if globalC == nil { + return LoadProvider(nil, providers...) + } + return LoadProvider(globalC.Users, providers...) +} + +// DetectFfmpeg 判断ffmpeg命令行是否存在 +func DetectFfmpeg(l *xlog.Logger) bool { + out, err := exec.Command("ffmpeg", "-version").Output() + if err != nil { + return false + } + + i := strings.Index(string(out), "Copyright") + if i > 0 { + l.Infof("detect %s", out[:i]) + } + return true +} diff --git a/config/log.go b/config/log.go new file mode 100755 index 0000000..c888ab5 --- /dev/null +++ b/config/log.go @@ -0,0 +1,80 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package config + +import ( + "flag" + "os" + + "github.com/cnotch/xlog" + lumberjack "gopkg.in/natefinch/lumberjack.v2" +) + +// LogConfig 日志配置 +type LogConfig struct { + // Level 是否启动记录调试日志 + Level xlog.Level `json:"level"` + + // ToFile 是否将日志记录到文件 + ToFile bool `json:"tofile"` + + // Filename 日志文件名称 + Filename string `json:"filename"` + + // MaxSize 日志文件的最大尺寸,以兆为单位 + MaxSize int `json:"maxsize"` + + // MaxDays 旧日志最多保存多少天 + MaxDays int `json:"maxdays"` + + // MaxBackups 旧日志最多保持数量。 + // 注意:旧日志保存的条件包括 <=MaxAge && <=MaxBackups + MaxBackups int `json:"maxbackups"` + + // Compress 是否用 gzip 压缩 + Compress bool `json:"compress"` +} + +func (c *LogConfig) initFlags() { + // 日志配置的 Flag + flag.Var(&c.Level, "log-level", + "Set the log level to output") + flag.BoolVar(&c.ToFile, "log-tofile", false, + "Determines if logs should be saved to file") + flag.StringVar(&c.Filename, "log-filename", + "./logs/"+Name+".log", "Set the file to write logs to") + flag.IntVar(&c.MaxSize, "log-maxsize", 20, + "Set the maximum size in megabytes of the log file before it gets rotated") + flag.IntVar(&c.MaxDays, "log-maxdays", 7, + "Set the maximum days of old log files to retain") + flag.IntVar(&c.MaxBackups, "log-maxbackups", 14, + "Set the maximum number of old log files to retain") + flag.BoolVar(&c.Compress, "log-compress", false, + "Determines if the log files should be compressed") +} + +// 初始化跟日志 +func (c *LogConfig) initLogger() { + if c.ToFile { + // 文件输出 + fileWriter := &lumberjack.Logger{ + Filename: c.Filename, // 日志文件路径 + MaxSize: c.MaxSize, // 每个日志文件保存的最大尺寸 单位:M + MaxBackups: c.MaxBackups, // 日志文件最多保存多少个备份 + MaxAge: c.MaxDays, // 文件最多保存多少天 + LocalTime: true, // 使用本地时间 + Compress: c.Compress, // 日志压缩 + } + + xlog.ReplaceGlobal( + xlog.New(xlog.NewTee(xlog.NewCore(xlog.NewConsoleEncoder(xlog.LstdFlags|xlog.Lmicroseconds|xlog.Llongfile), xlog.Lock(os.Stderr), c.Level), + xlog.NewCore(xlog.NewJSONEncoder(xlog.Llongfile), fileWriter, c.Level)), + xlog.AddCaller())) + } else { + xlog.ReplaceGlobal( + xlog.New(xlog.NewCore(xlog.NewConsoleEncoder(xlog.LstdFlags|xlog.Lmicroseconds|xlog.Llongfile), xlog.Lock(os.Stderr), c.Level), + xlog.AddCaller())) + } +} diff --git a/config/provider.go b/config/provider.go new file mode 100755 index 0000000..0a22a41 --- /dev/null +++ b/config/provider.go @@ -0,0 +1,60 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package config + +import ( + "errors" + "strings" +) + +// Provider 提供者接口 +type Provider interface { + Name() string + Configure(config map[string]interface{}) error +} + +// ProviderConfig 可扩展提供者配置 +type ProviderConfig struct { + Provider string `json:"provider"` // 提供者类型 + Config map[string]interface{} `json:"config,omitempty"` // 提供者配置 +} + +// Load 加载Provider +func (c *ProviderConfig) Load(builtins ...Provider) (Provider, error) { + for _, builtin := range builtins { + if strings.ToLower(builtin.Name()) == strings.ToLower(c.Provider) { + if err := builtin.Configure(c.Config); err != nil { + return nil, errors.New("The provider '" + c.Provider + "' could not be loaded. " + err.Error()) + } + + return builtin, nil + } + } + + // TODO: load a plugin provider + return nil, errors.New("The provider '" + c.Provider + "' could not be loaded. ") +} + +// LoadOrPanic 加载 Provider 如果失败直接 panics. +func (c *ProviderConfig) LoadOrPanic(builtins ...Provider) Provider { + provider, err := c.Load(builtins...) + if err != nil { + panic(err) + } + + return provider +} + +// LoadProvider 加载Provider或Panic,默认值为第一个provider +func LoadProvider(config *ProviderConfig, providers ...Provider) Provider { + if config == nil || config.Provider == "" { + config = &ProviderConfig{ + Provider: providers[0].Name(), + } + } + + // Load the provider according to the configuration + return config.LoadOrPanic(providers...) +} diff --git a/config/rtmp.go b/config/rtmp.go new file mode 100755 index 0000000..3423630 --- /dev/null +++ b/config/rtmp.go @@ -0,0 +1,19 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package config + +import ( + "flag" +) + +// RtmpConfig rtsp 配置 +type RtmpConfig struct { + ChunkSize int `json:"chunksize"` +} + +func (c *RtmpConfig) initFlags() { + // RTSP 组播 + flag.IntVar(&c.ChunkSize, "rtmp-chunksize", 16*1024, "Set RTMP ChunkSize") +} diff --git a/config/rtsp.go b/config/rtsp.go new file mode 100755 index 0000000..fe02c05 --- /dev/null +++ b/config/rtsp.go @@ -0,0 +1,21 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package config + +import ( + "flag" + + "github.com/cnotch/tomatox/provider/auth" +) + +// RtspConfig rtsp 配置 +type RtspConfig struct { + AuthMode auth.Mode `json:"authmode"` +} + +func (c *RtspConfig) initFlags() { + // RTSP 组播 + flag.Var(&c.AuthMode, "rtsp-auth", "Set RTSP auth mode") +} diff --git a/config/tls.go b/config/tls.go new file mode 100755 index 0000000..fee13c8 --- /dev/null +++ b/config/tls.go @@ -0,0 +1,58 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package config + +import ( + "crypto/tls" + "errors" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// TLSConfig TLS listen 配置. +type TLSConfig struct { + ListenAddr string `json:"listen"` + Certificate string `json:"cert"` + PrivateKey string `json:"key"` +} + +// Load loads the certificates from the cache or the configuration. +func (c *TLSConfig) Load() (*tls.Config, error) { + if c.PrivateKey == "" || c.Certificate == "" { + return &tls.Config{}, errors.New("No certificate or private key configured") + } + + // If the certificate provided is in plain text, write to file so we can read it. + if strings.HasPrefix(c.Certificate, "---") { + if err := ioutil.WriteFile("broker.crt", []byte(c.Certificate), os.ModePerm); err == nil { + c.Certificate = Name+".crt" + } + } + + // If the private key provided is in plain text, write to file so we can read it. + if strings.HasPrefix(c.PrivateKey, "---") { + if err := ioutil.WriteFile("broker.key", []byte(c.PrivateKey), os.ModePerm); err == nil { + c.PrivateKey = Name+".key" + } + } + + // Make sure the paths are absolute, otherwise we won't be able to read the files. + c.Certificate = resolvePath(c.Certificate) + c.PrivateKey = resolvePath(c.PrivateKey) + + // Load the certificate from the cert/key files. + cer, err := tls.LoadX509KeyPair(c.Certificate, c.PrivateKey) + return &tls.Config{ + Certificates: []tls.Certificate{cer}, + }, err +} + +func resolvePath(path string) string { + // Make sure the path is absolute + path, _ = filepath.Abs(path) + return path +} diff --git a/console/readme.md b/console/readme.md new file mode 100644 index 0000000..fc8fb2c --- /dev/null +++ b/console/readme.md @@ -0,0 +1 @@ +在此目录加入管理员控制台web项目 \ No newline at end of file diff --git a/demos/flv/demo.css b/demos/flv/demo.css new file mode 100755 index 0000000..6e2ee3d --- /dev/null +++ b/demos/flv/demo.css @@ -0,0 +1,108 @@ +.mainContainer { + display: block; + width: 100%; + margin-left: auto; + margin-right: auto; +} +@media screen and (min-width: 1152px) { + .mainContainer { + display: block; + width: 1152px; + margin-left: auto; + margin-right: auto; + } +} + +.video-container { + position: relative; + margin-top: 8px; +} + +.video-container:before { + display: block; + content: ""; + width: 100%; + padding-bottom: 56.25%; +} + +.video-container > div { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.video-container video { + width: 100%; + height: 100%; +} + +.urlInput { + display: block; + width: 100%; + margin-left: auto; + margin-right: auto; + margin-top: 8px; + margin-bottom: 8px; +} + +.centeredVideo { + display: block; + width: 100%; + height: 100%; + margin-left: auto; + margin-right: auto; + margin-bottom: auto; +} + +.controls { + display: block; + width: 100%; + text-align: left; + margin-left: auto; + margin-right: auto; + margin-top: 8px; + margin-bottom: 10px; +} + +.logcatBox { + border-color: #CCCCCC; + font-size: 11px; + font-family: Menlo, Consolas, monospace; + display: block; + width: 100%; + text-align: left; + margin-left: auto; + margin-right: auto; +} + +.url-input , .options { + font-size: 13px; +} + +.url-input { + display: flex; +} + +.url-input label { + flex: initial; +} + +.url-input input { + flex: auto; + margin-left: 8px; +} + +.url-input button { + flex: initial; + margin-left: 8px; +} + +.options { + margin-top: 5px; +} + +.hidden { + display: none; +} diff --git a/demos/flv/flv.js b/demos/flv/flv.js new file mode 100755 index 0000000..b7deb99 --- /dev/null +++ b/demos/flv/flv.js @@ -0,0 +1,12047 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.flvjs = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i postsJSON + values[1] // => commentsJSON + + return values; + }); + ``` + + @class Promise + @param {Function} resolver + Useful for tooling. + @constructor +*/ + +var Promise$1 = function () { + function Promise(resolver) { + this[PROMISE_ID] = nextId(); + this._result = this._state = undefined; + this._subscribers = []; + + if (noop !== resolver) { + typeof resolver !== 'function' && needsResolver(); + this instanceof Promise ? initializePromise(this, resolver) : needsNew(); + } + } + + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` + Chaining + -------- + The return value of `then` is itself a promise. This second, 'downstream' + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return 'default name'; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `'default name'` + }); + findUser().then(function (user) { + throw new Error('Found user, but still unhappy'); + }, function (reason) { + throw new Error('`findUser` rejected and we're unhappy'); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'. + // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'. + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + ```js + findUser().then(function (user) { + throw new PedagogicalException('Upstream error'); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` + Assimilation + ------------ + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` + If the assimliated promise rejects, then the downstream promise will also reject. + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` + Simple Example + -------------- + Synchronous Example + ```javascript + let result; + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` + Errback Example + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` + Promise Example; + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` + Advanced Example + -------------- + Synchronous Example + ```javascript + let author, books; + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` + Errback Example + ```js + function foundBooks(books) { + } + function failure(reason) { + } + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` + Promise Example; + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` + @method then + @param {Function} onFulfilled + @param {Function} onRejected + Useful for tooling. + @return {Promise} + */ + + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. + ```js + function findAuthor(){ + throw new Error('couldn't find that author'); + } + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` + @method catch + @param {Function} onRejection + Useful for tooling. + @return {Promise} + */ + + + Promise.prototype.catch = function _catch(onRejection) { + return this.then(null, onRejection); + }; + + /** + `finally` will be invoked regardless of the promise's fate just as native + try/catch/finally behaves + + Synchronous example: + + ```js + findAuthor() { + if (Math.random() > 0.5) { + throw new Error(); + } + return new Author(); + } + + try { + return findAuthor(); // succeed or fail + } catch(error) { + return findOtherAuther(); + } finally { + // always runs + // doesn't affect the return value + } + ``` + + Asynchronous example: + + ```js + findAuthor().catch(function(reason){ + return findOtherAuther(); + }).finally(function(){ + // author was either found, or not + }); + ``` + + @method finally + @param {Function} callback + @return {Promise} + */ + + + Promise.prototype.finally = function _finally(callback) { + var promise = this; + var constructor = promise.constructor; + + if (isFunction(callback)) { + return promise.then(function (value) { + return constructor.resolve(callback()).then(function () { + return value; + }); + }, function (reason) { + return constructor.resolve(callback()).then(function () { + throw reason; + }); + }); + } + + return promise.then(callback, callback); + }; + + return Promise; +}(); + +Promise$1.prototype.then = then; +Promise$1.all = all; +Promise$1.race = race; +Promise$1.resolve = resolve$1; +Promise$1.reject = reject$1; +Promise$1._setScheduler = setScheduler; +Promise$1._setAsap = setAsap; +Promise$1._asap = asap; + +/*global self*/ +function polyfill() { + var local = void 0; + + if (typeof global !== 'undefined') { + local = global; + } else if (typeof self !== 'undefined') { + local = self; + } else { + try { + local = Function('return this')(); + } catch (e) { + throw new Error('polyfill failed because global object is unavailable in this environment'); + } + } + + var P = local.Promise; + + if (P) { + var promiseToString = null; + try { + promiseToString = Object.prototype.toString.call(P.resolve()); + } catch (e) { + // silently ignored + } + + if (promiseToString === '[object Promise]' && !P.cast) { + return; + } + } + + local.Promise = Promise$1; +} + +// Strange compat.. +Promise$1.polyfill = polyfill; +Promise$1.Promise = Promise$1; + +return Promise$1; + +}))); + + + + + +}).call(this,_dereq_('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"_process":3}],2:[function(_dereq_,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +function EventEmitter() { + this._events = this._events || {}; + this._maxListeners = this._maxListeners || undefined; +} +module.exports = EventEmitter; + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +EventEmitter.defaultMaxListeners = 10; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function(n) { + if (!isNumber(n) || n < 0 || isNaN(n)) + throw TypeError('n must be a positive number'); + this._maxListeners = n; + return this; +}; + +EventEmitter.prototype.emit = function(type) { + var er, handler, len, args, i, listeners; + + if (!this._events) + this._events = {}; + + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events.error || + (isObject(this._events.error) && !this._events.error.length)) { + er = arguments[1]; + if (er instanceof Error) { + throw er; // Unhandled 'error' event + } else { + // At least give some kind of context to the user + var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); + err.context = er; + throw err; + } + } + } + + handler = this._events[type]; + + if (isUndefined(handler)) + return false; + + if (isFunction(handler)) { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + args = Array.prototype.slice.call(arguments, 1); + handler.apply(this, args); + } + } else if (isObject(handler)) { + args = Array.prototype.slice.call(arguments, 1); + listeners = handler.slice(); + len = listeners.length; + for (i = 0; i < len; i++) + listeners[i].apply(this, args); + } + + return true; +}; + +EventEmitter.prototype.addListener = function(type, listener) { + var m; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events) + this._events = {}; + + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (this._events.newListener) + this.emit('newListener', type, + isFunction(listener.listener) ? + listener.listener : listener); + + if (!this._events[type]) + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + else if (isObject(this._events[type])) + // If we've already got an array, just append. + this._events[type].push(listener); + else + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + + // Check for listener leak + if (isObject(this._events[type]) && !this._events[type].warned) { + if (!isUndefined(this._maxListeners)) { + m = this._maxListeners; + } else { + m = EventEmitter.defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + if (typeof console.trace === 'function') { + // not supported in IE 10 + console.trace(); + } + } + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + var fired = false; + + function g() { + this.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + g.listener = listener; + this.on(type, g); + + return this; +}; + +// emits a 'removeListener' event iff the listener was removed +EventEmitter.prototype.removeListener = function(type, listener) { + var list, position, length, i; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events || !this._events[type]) + return this; + + list = this._events[type]; + length = list.length; + position = -1; + + if (list === listener || + (isFunction(list.listener) && list.listener === listener)) { + delete this._events[type]; + if (this._events.removeListener) + this.emit('removeListener', type, listener); + + } else if (isObject(list)) { + for (i = length; i-- > 0;) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (list.length === 1) { + list.length = 0; + delete this._events[type]; + } else { + list.splice(position, 1); + } + + if (this._events.removeListener) + this.emit('removeListener', type, listener); + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + var key, listeners; + + if (!this._events) + return this; + + // not listening for removeListener, no need to emit + if (!this._events.removeListener) { + if (arguments.length === 0) + this._events = {}; + else if (this._events[type]) + delete this._events[type]; + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (key in this._events) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + return this; + } + + listeners = this._events[type]; + + if (isFunction(listeners)) { + this.removeListener(type, listeners); + } else if (listeners) { + // LIFO order + while (listeners.length) + this.removeListener(type, listeners[listeners.length - 1]); + } + delete this._events[type]; + + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + var ret; + if (!this._events || !this._events[type]) + ret = []; + else if (isFunction(this._events[type])) + ret = [this._events[type]]; + else + ret = this._events[type].slice(); + return ret; +}; + +EventEmitter.prototype.listenerCount = function(type) { + if (this._events) { + var evlistener = this._events[type]; + + if (isFunction(evlistener)) + return 1; + else if (evlistener) + return evlistener.length; + } + return 0; +}; + +EventEmitter.listenerCount = function(emitter, type) { + return emitter.listenerCount(type); +}; + +function isFunction(arg) { + return typeof arg === 'function'; +} + +function isNumber(arg) { + return typeof arg === 'number'; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isUndefined(arg) { + return arg === void 0; +} + +},{}],3:[function(_dereq_,module,exports){ +// shim for using process in browser +var process = module.exports = {}; + +// cached from whatever global is present so that test runners that stub it +// don't break things. But we need to wrap it in a try catch in case it is +// wrapped in strict mode code which doesn't define any globals. It's inside a +// function because try/catches deoptimize in certain engines. + +var cachedSetTimeout; +var cachedClearTimeout; + +function defaultSetTimout() { + throw new Error('setTimeout has not been defined'); +} +function defaultClearTimeout () { + throw new Error('clearTimeout has not been defined'); +} +(function () { + try { + if (typeof setTimeout === 'function') { + cachedSetTimeout = setTimeout; + } else { + cachedSetTimeout = defaultSetTimout; + } + } catch (e) { + cachedSetTimeout = defaultSetTimout; + } + try { + if (typeof clearTimeout === 'function') { + cachedClearTimeout = clearTimeout; + } else { + cachedClearTimeout = defaultClearTimeout; + } + } catch (e) { + cachedClearTimeout = defaultClearTimeout; + } +} ()) +function runTimeout(fun) { + if (cachedSetTimeout === setTimeout) { + //normal enviroments in sane situations + return setTimeout(fun, 0); + } + // if setTimeout wasn't available but was latter defined + if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { + cachedSetTimeout = setTimeout; + return setTimeout(fun, 0); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedSetTimeout(fun, 0); + } catch(e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedSetTimeout.call(null, fun, 0); + } catch(e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error + return cachedSetTimeout.call(this, fun, 0); + } + } + + +} +function runClearTimeout(marker) { + if (cachedClearTimeout === clearTimeout) { + //normal enviroments in sane situations + return clearTimeout(marker); + } + // if clearTimeout wasn't available but was latter defined + if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { + cachedClearTimeout = clearTimeout; + return clearTimeout(marker); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedClearTimeout(marker); + } catch (e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedClearTimeout.call(null, marker); + } catch (e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. + // Some versions of I.E. have different rules for clearTimeout vs setTimeout + return cachedClearTimeout.call(this, marker); + } + } + + + +} +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + if (!draining || !currentQueue) { + return; + } + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = runTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + runClearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; +process.prependListener = noop; +process.prependOnceListener = noop; + +process.listeners = function (name) { return [] } + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}],4:[function(_dereq_,module,exports){ +var bundleFn = arguments[3]; +var sources = arguments[4]; +var cache = arguments[5]; + +var stringify = JSON.stringify; + +module.exports = function (fn, options) { + var wkey; + var cacheKeys = Object.keys(cache); + + for (var i = 0, l = cacheKeys.length; i < l; i++) { + var key = cacheKeys[i]; + var exp = cache[key].exports; + // Using babel as a transpiler to use esmodule, the export will always + // be an object with the default export as a property of it. To ensure + // the existing api and babel esmodule exports are both supported we + // check for both + if (exp === fn || exp && exp.default === fn) { + wkey = key; + break; + } + } + + if (!wkey) { + wkey = Math.floor(Math.pow(16, 8) * Math.random()).toString(16); + var wcache = {}; + for (var i = 0, l = cacheKeys.length; i < l; i++) { + var key = cacheKeys[i]; + wcache[key] = key; + } + sources[wkey] = [ + 'function(require,module,exports){' + fn + '(self); }', + wcache + ]; + } + var skey = Math.floor(Math.pow(16, 8) * Math.random()).toString(16); + + var scache = {}; scache[wkey] = wkey; + sources[skey] = [ + 'function(require,module,exports){' + + // try to call default if defined to also support babel esmodule exports + 'var f = require(' + stringify(wkey) + ');' + + '(f.default ? f.default : f)(self);' + + '}', + scache + ]; + + var workerSources = {}; + resolveSources(skey); + + function resolveSources(key) { + workerSources[key] = true; + + for (var depPath in sources[key][1]) { + var depKey = sources[key][1][depPath]; + if (!workerSources[depKey]) { + resolveSources(depKey); + } + } + } + + var src = '(' + bundleFn + ')({' + + Object.keys(workerSources).map(function (key) { + return stringify(key) + ':[' + + sources[key][0] + + ',' + stringify(sources[key][1]) + ']' + ; + }).join(',') + + '},{},[' + stringify(skey) + '])' + ; + + var URL = window.URL || window.webkitURL || window.mozURL || window.msURL; + + var blob = new Blob([src], { type: 'text/javascript' }); + if (options && options.bare) { return blob; } + var workerUrl = URL.createObjectURL(blob); + var worker = new Worker(workerUrl); + worker.objectURL = workerUrl; + return worker; +}; + +},{}],5:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.createDefaultConfig = createDefaultConfig; +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var defaultConfig = exports.defaultConfig = { + enableWorker: false, + enableStashBuffer: true, + stashInitialSize: undefined, + + isLive: false, + + lazyLoad: true, + lazyLoadMaxDuration: 3 * 60, + lazyLoadRecoverDuration: 30, + deferLoadAfterSourceOpen: true, + + // autoCleanupSourceBuffer: default as false, leave unspecified + autoCleanupMaxBackwardDuration: 3 * 60, + autoCleanupMinBackwardDuration: 2 * 60, + + statisticsInfoReportInterval: 600, + + fixAudioTimestampGap: true, + + accurateSeek: false, + seekType: 'range', // [range, param, custom] + seekParamStart: 'bstart', + seekParamEnd: 'bend', + rangeLoadZeroStart: false, + customSeekHandler: undefined, + reuseRedirectedURL: false, + // referrerPolicy: leave as unspecified + + headers: undefined, + customLoader: undefined +}; + +function createDefaultConfig() { + return Object.assign({}, defaultConfig); +} + +},{}],6:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _ioController = _dereq_('../io/io-controller.js'); + +var _ioController2 = _interopRequireDefault(_ioController); + +var _config = _dereq_('../config.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Features = function () { + function Features() { + _classCallCheck(this, Features); + } + + _createClass(Features, null, [{ + key: 'supportMSEH264Playback', + value: function supportMSEH264Playback() { + return window.MediaSource && window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"'); + } + }, { + key: 'supportNetworkStreamIO', + value: function supportNetworkStreamIO() { + var ioctl = new _ioController2.default({}, (0, _config.createDefaultConfig)()); + var loaderType = ioctl.loaderType; + ioctl.destroy(); + return loaderType == 'fetch-stream-loader' || loaderType == 'xhr-moz-chunked-loader'; + } + }, { + key: 'getNetworkLoaderTypeName', + value: function getNetworkLoaderTypeName() { + var ioctl = new _ioController2.default({}, (0, _config.createDefaultConfig)()); + var loaderType = ioctl.loaderType; + ioctl.destroy(); + return loaderType; + } + }, { + key: 'supportNativeMediaPlayback', + value: function supportNativeMediaPlayback(mimeType) { + if (Features.videoElement == undefined) { + Features.videoElement = window.document.createElement('video'); + } + var canPlay = Features.videoElement.canPlayType(mimeType); + return canPlay === 'probably' || canPlay == 'maybe'; + } + }, { + key: 'getFeatureList', + value: function getFeatureList() { + var features = { + mseFlvPlayback: false, + mseLiveFlvPlayback: false, + networkStreamIO: false, + networkLoaderName: '', + nativeMP4H264Playback: false, + nativeWebmVP8Playback: false, + nativeWebmVP9Playback: false + }; + + features.mseFlvPlayback = Features.supportMSEH264Playback(); + features.networkStreamIO = Features.supportNetworkStreamIO(); + features.networkLoaderName = Features.getNetworkLoaderTypeName(); + features.mseLiveFlvPlayback = features.mseFlvPlayback && features.networkStreamIO; + features.nativeMP4H264Playback = Features.supportNativeMediaPlayback('video/mp4; codecs="avc1.42001E, mp4a.40.2"'); + features.nativeWebmVP8Playback = Features.supportNativeMediaPlayback('video/webm; codecs="vp8.0, vorbis"'); + features.nativeWebmVP9Playback = Features.supportNativeMediaPlayback('video/webm; codecs="vp9"'); + + return features; + } + }]); + + return Features; +}(); + +exports.default = Features; + +},{"../config.js":5,"../io/io-controller.js":23}],7:[function(_dereq_,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var MediaInfo = function () { + function MediaInfo() { + _classCallCheck(this, MediaInfo); + + this.mimeType = null; + this.duration = null; + + this.hasAudio = null; + this.hasVideo = null; + this.audioCodec = null; + this.videoCodec = null; + this.audioDataRate = null; + this.videoDataRate = null; + + this.audioSampleRate = null; + this.audioChannelCount = null; + + this.width = null; + this.height = null; + this.fps = null; + this.profile = null; + this.level = null; + this.refFrames = null; + this.chromaFormat = null; + this.sarNum = null; + this.sarDen = null; + + this.metadata = null; + this.segments = null; // MediaInfo[] + this.segmentCount = null; + this.hasKeyframesIndex = null; + this.keyframesIndex = null; + } + + _createClass(MediaInfo, [{ + key: "isComplete", + value: function isComplete() { + var audioInfoComplete = this.hasAudio === false || this.hasAudio === true && this.audioCodec != null && this.audioSampleRate != null && this.audioChannelCount != null; + + var videoInfoComplete = this.hasVideo === false || this.hasVideo === true && this.videoCodec != null && this.width != null && this.height != null && this.fps != null && this.profile != null && this.level != null && this.refFrames != null && this.chromaFormat != null && this.sarNum != null && this.sarDen != null; + + // keyframesIndex may not be present + return this.mimeType != null && this.duration != null && this.metadata != null && this.hasKeyframesIndex != null && audioInfoComplete && videoInfoComplete; + } + }, { + key: "isSeekable", + value: function isSeekable() { + return this.hasKeyframesIndex === true; + } + }, { + key: "getNearestKeyframe", + value: function getNearestKeyframe(milliseconds) { + if (this.keyframesIndex == null) { + return null; + } + + var table = this.keyframesIndex; + var keyframeIdx = this._search(table.times, milliseconds); + + return { + index: keyframeIdx, + milliseconds: table.times[keyframeIdx], + fileposition: table.filepositions[keyframeIdx] + }; + } + }, { + key: "_search", + value: function _search(list, value) { + var idx = 0; + + var last = list.length - 1; + var mid = 0; + var lbound = 0; + var ubound = last; + + if (value < list[0]) { + idx = 0; + lbound = ubound + 1; // skip search + } + + while (lbound <= ubound) { + mid = lbound + Math.floor((ubound - lbound) / 2); + if (mid === last || value >= list[mid] && value < list[mid + 1]) { + idx = mid; + break; + } else if (list[mid] < value) { + lbound = mid + 1; + } else { + ubound = mid - 1; + } + } + + return idx; + } + }]); + + return MediaInfo; +}(); + +exports.default = MediaInfo; + +},{}],8:[function(_dereq_,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Represents an media sample (audio / video) +var SampleInfo = exports.SampleInfo = function SampleInfo(dts, pts, duration, originalDts, isSync) { + _classCallCheck(this, SampleInfo); + + this.dts = dts; + this.pts = pts; + this.duration = duration; + this.originalDts = originalDts; + this.isSyncPoint = isSync; + this.fileposition = null; +}; + +// Media Segment concept is defined in Media Source Extensions spec. +// Particularly in ISO BMFF format, an Media Segment contains a moof box followed by a mdat box. + + +var MediaSegmentInfo = exports.MediaSegmentInfo = function () { + function MediaSegmentInfo() { + _classCallCheck(this, MediaSegmentInfo); + + this.beginDts = 0; + this.endDts = 0; + this.beginPts = 0; + this.endPts = 0; + this.originalBeginDts = 0; + this.originalEndDts = 0; + this.syncPoints = []; // SampleInfo[n], for video IDR frames only + this.firstSample = null; // SampleInfo + this.lastSample = null; // SampleInfo + } + + _createClass(MediaSegmentInfo, [{ + key: "appendSyncPoint", + value: function appendSyncPoint(sampleInfo) { + // also called Random Access Point + sampleInfo.isSyncPoint = true; + this.syncPoints.push(sampleInfo); + } + }]); + + return MediaSegmentInfo; +}(); + +// Ordered list for recording video IDR frames, sorted by originalDts + + +var IDRSampleList = exports.IDRSampleList = function () { + function IDRSampleList() { + _classCallCheck(this, IDRSampleList); + + this._list = []; + } + + _createClass(IDRSampleList, [{ + key: "clear", + value: function clear() { + this._list = []; + } + }, { + key: "appendArray", + value: function appendArray(syncPoints) { + var list = this._list; + + if (syncPoints.length === 0) { + return; + } + + if (list.length > 0 && syncPoints[0].originalDts < list[list.length - 1].originalDts) { + this.clear(); + } + + Array.prototype.push.apply(list, syncPoints); + } + }, { + key: "getLastSyncPointBeforeDts", + value: function getLastSyncPointBeforeDts(dts) { + if (this._list.length == 0) { + return null; + } + + var list = this._list; + var idx = 0; + var last = list.length - 1; + var mid = 0; + var lbound = 0; + var ubound = last; + + if (dts < list[0].dts) { + idx = 0; + lbound = ubound + 1; + } + + while (lbound <= ubound) { + mid = lbound + Math.floor((ubound - lbound) / 2); + if (mid === last || dts >= list[mid].dts && dts < list[mid + 1].dts) { + idx = mid; + break; + } else if (list[mid].dts < dts) { + lbound = mid + 1; + } else { + ubound = mid - 1; + } + } + return this._list[idx]; + } + }]); + + return IDRSampleList; +}(); + +// Data structure for recording information of media segments in single track. + + +var MediaSegmentInfoList = exports.MediaSegmentInfoList = function () { + function MediaSegmentInfoList(type) { + _classCallCheck(this, MediaSegmentInfoList); + + this._type = type; + this._list = []; + this._lastAppendLocation = -1; // cached last insert location + } + + _createClass(MediaSegmentInfoList, [{ + key: "isEmpty", + value: function isEmpty() { + return this._list.length === 0; + } + }, { + key: "clear", + value: function clear() { + this._list = []; + this._lastAppendLocation = -1; + } + }, { + key: "_searchNearestSegmentBefore", + value: function _searchNearestSegmentBefore(originalBeginDts) { + var list = this._list; + if (list.length === 0) { + return -2; + } + var last = list.length - 1; + var mid = 0; + var lbound = 0; + var ubound = last; + + var idx = 0; + + if (originalBeginDts < list[0].originalBeginDts) { + idx = -1; + return idx; + } + + while (lbound <= ubound) { + mid = lbound + Math.floor((ubound - lbound) / 2); + if (mid === last || originalBeginDts > list[mid].lastSample.originalDts && originalBeginDts < list[mid + 1].originalBeginDts) { + idx = mid; + break; + } else if (list[mid].originalBeginDts < originalBeginDts) { + lbound = mid + 1; + } else { + ubound = mid - 1; + } + } + return idx; + } + }, { + key: "_searchNearestSegmentAfter", + value: function _searchNearestSegmentAfter(originalBeginDts) { + return this._searchNearestSegmentBefore(originalBeginDts) + 1; + } + }, { + key: "append", + value: function append(mediaSegmentInfo) { + var list = this._list; + var msi = mediaSegmentInfo; + var lastAppendIdx = this._lastAppendLocation; + var insertIdx = 0; + + if (lastAppendIdx !== -1 && lastAppendIdx < list.length && msi.originalBeginDts >= list[lastAppendIdx].lastSample.originalDts && (lastAppendIdx === list.length - 1 || lastAppendIdx < list.length - 1 && msi.originalBeginDts < list[lastAppendIdx + 1].originalBeginDts)) { + insertIdx = lastAppendIdx + 1; // use cached location idx + } else { + if (list.length > 0) { + insertIdx = this._searchNearestSegmentBefore(msi.originalBeginDts) + 1; + } + } + + this._lastAppendLocation = insertIdx; + this._list.splice(insertIdx, 0, msi); + } + }, { + key: "getLastSegmentBefore", + value: function getLastSegmentBefore(originalBeginDts) { + var idx = this._searchNearestSegmentBefore(originalBeginDts); + if (idx >= 0) { + return this._list[idx]; + } else { + // -1 + return null; + } + } + }, { + key: "getLastSampleBefore", + value: function getLastSampleBefore(originalBeginDts) { + var segment = this.getLastSegmentBefore(originalBeginDts); + if (segment != null) { + return segment.lastSample; + } else { + return null; + } + } + }, { + key: "getLastSyncPointBefore", + value: function getLastSyncPointBefore(originalBeginDts) { + var segmentIdx = this._searchNearestSegmentBefore(originalBeginDts); + var syncPoints = this._list[segmentIdx].syncPoints; + while (syncPoints.length === 0 && segmentIdx > 0) { + segmentIdx--; + syncPoints = this._list[segmentIdx].syncPoints; + } + if (syncPoints.length > 0) { + return syncPoints[syncPoints.length - 1]; + } else { + return null; + } + } + }, { + key: "type", + get: function get() { + return this._type; + } + }, { + key: "length", + get: function get() { + return this._list.length; + } + }]); + + return MediaSegmentInfoList; +}(); + +},{}],9:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _events = _dereq_('events'); + +var _events2 = _interopRequireDefault(_events); + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _browser = _dereq_('../utils/browser.js'); + +var _browser2 = _interopRequireDefault(_browser); + +var _mseEvents = _dereq_('./mse-events.js'); + +var _mseEvents2 = _interopRequireDefault(_mseEvents); + +var _mediaSegmentInfo = _dereq_('./media-segment-info.js'); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +// Media Source Extensions controller +var MSEController = function () { + function MSEController(config) { + _classCallCheck(this, MSEController); + + this.TAG = 'MSEController'; + + this._config = config; + this._emitter = new _events2.default(); + + if (this._config.isLive && this._config.autoCleanupSourceBuffer == undefined) { + // For live stream, do auto cleanup by default + this._config.autoCleanupSourceBuffer = true; + } + + this.e = { + onSourceOpen: this._onSourceOpen.bind(this), + onSourceEnded: this._onSourceEnded.bind(this), + onSourceClose: this._onSourceClose.bind(this), + onSourceBufferError: this._onSourceBufferError.bind(this), + onSourceBufferUpdateEnd: this._onSourceBufferUpdateEnd.bind(this) + }; + + this._mediaSource = null; + this._mediaSourceObjectURL = null; + this._mediaElement = null; + + this._isBufferFull = false; + this._hasPendingEos = false; + + this._requireSetMediaDuration = false; + this._pendingMediaDuration = 0; + + this._pendingSourceBufferInit = []; + this._mimeTypes = { + video: null, + audio: null + }; + this._sourceBuffers = { + video: null, + audio: null + }; + this._lastInitSegments = { + video: null, + audio: null + }; + this._pendingSegments = { + video: [], + audio: [] + }; + this._pendingRemoveRanges = { + video: [], + audio: [] + }; + this._idrList = new _mediaSegmentInfo.IDRSampleList(); + } + + _createClass(MSEController, [{ + key: 'destroy', + value: function destroy() { + if (this._mediaElement || this._mediaSource) { + this.detachMediaElement(); + } + this.e = null; + this._emitter.removeAllListeners(); + this._emitter = null; + } + }, { + key: 'on', + value: function on(event, listener) { + this._emitter.addListener(event, listener); + } + }, { + key: 'off', + value: function off(event, listener) { + this._emitter.removeListener(event, listener); + } + }, { + key: 'attachMediaElement', + value: function attachMediaElement(mediaElement) { + if (this._mediaSource) { + throw new _exception.IllegalStateException('MediaSource has been attached to an HTMLMediaElement!'); + } + var ms = this._mediaSource = new window.MediaSource(); + ms.addEventListener('sourceopen', this.e.onSourceOpen); + ms.addEventListener('sourceended', this.e.onSourceEnded); + ms.addEventListener('sourceclose', this.e.onSourceClose); + + this._mediaElement = mediaElement; + this._mediaSourceObjectURL = window.URL.createObjectURL(this._mediaSource); + mediaElement.src = this._mediaSourceObjectURL; + } + }, { + key: 'detachMediaElement', + value: function detachMediaElement() { + if (this._mediaSource) { + var ms = this._mediaSource; + for (var type in this._sourceBuffers) { + // pending segments should be discard + var ps = this._pendingSegments[type]; + ps.splice(0, ps.length); + this._pendingSegments[type] = null; + this._pendingRemoveRanges[type] = null; + this._lastInitSegments[type] = null; + + // remove all sourcebuffers + var sb = this._sourceBuffers[type]; + if (sb) { + if (ms.readyState !== 'closed') { + // ms edge can throw an error: Unexpected call to method or property access + try { + ms.removeSourceBuffer(sb); + } catch (error) { + _logger2.default.e(this.TAG, error.message); + } + sb.removeEventListener('error', this.e.onSourceBufferError); + sb.removeEventListener('updateend', this.e.onSourceBufferUpdateEnd); + } + this._mimeTypes[type] = null; + this._sourceBuffers[type] = null; + } + } + if (ms.readyState === 'open') { + try { + ms.endOfStream(); + } catch (error) { + _logger2.default.e(this.TAG, error.message); + } + } + ms.removeEventListener('sourceopen', this.e.onSourceOpen); + ms.removeEventListener('sourceended', this.e.onSourceEnded); + ms.removeEventListener('sourceclose', this.e.onSourceClose); + this._pendingSourceBufferInit = []; + this._isBufferFull = false; + this._idrList.clear(); + this._mediaSource = null; + } + + if (this._mediaElement) { + this._mediaElement.src = ''; + this._mediaElement.removeAttribute('src'); + this._mediaElement = null; + } + if (this._mediaSourceObjectURL) { + window.URL.revokeObjectURL(this._mediaSourceObjectURL); + this._mediaSourceObjectURL = null; + } + } + }, { + key: 'appendInitSegment', + value: function appendInitSegment(initSegment, deferred) { + if (!this._mediaSource || this._mediaSource.readyState !== 'open') { + // sourcebuffer creation requires mediaSource.readyState === 'open' + // so we defer the sourcebuffer creation, until sourceopen event triggered + this._pendingSourceBufferInit.push(initSegment); + // make sure that this InitSegment is in the front of pending segments queue + this._pendingSegments[initSegment.type].push(initSegment); + return; + } + + var is = initSegment; + var mimeType = '' + is.container; + if (is.codec && is.codec.length > 0) { + mimeType += ';codecs=' + is.codec; + } + + var firstInitSegment = false; + + _logger2.default.v(this.TAG, 'Received Initialization Segment, mimeType: ' + mimeType); + this._lastInitSegments[is.type] = is; + + if (mimeType !== this._mimeTypes[is.type]) { + if (!this._mimeTypes[is.type]) { + // empty, first chance create sourcebuffer + firstInitSegment = true; + try { + var sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType); + sb.addEventListener('error', this.e.onSourceBufferError); + sb.addEventListener('updateend', this.e.onSourceBufferUpdateEnd); + } catch (error) { + _logger2.default.e(this.TAG, error.message); + this._emitter.emit(_mseEvents2.default.ERROR, { code: error.code, msg: error.message }); + return; + } + } else { + _logger2.default.v(this.TAG, 'Notice: ' + is.type + ' mimeType changed, origin: ' + this._mimeTypes[is.type] + ', target: ' + mimeType); + } + this._mimeTypes[is.type] = mimeType; + } + + if (!deferred) { + // deferred means this InitSegment has been pushed to pendingSegments queue + this._pendingSegments[is.type].push(is); + } + if (!firstInitSegment) { + // append immediately only if init segment in subsequence + if (this._sourceBuffers[is.type] && !this._sourceBuffers[is.type].updating) { + this._doAppendSegments(); + } + } + if (_browser2.default.safari && is.container === 'audio/mpeg' && is.mediaDuration > 0) { + // 'audio/mpeg' track under Safari may cause MediaElement's duration to be NaN + // Manually correct MediaSource.duration to make progress bar seekable, and report right duration + this._requireSetMediaDuration = true; + this._pendingMediaDuration = is.mediaDuration / 1000; // in seconds + this._updateMediaSourceDuration(); + } + } + }, { + key: 'appendMediaSegment', + value: function appendMediaSegment(mediaSegment) { + var ms = mediaSegment; + this._pendingSegments[ms.type].push(ms); + + if (this._config.autoCleanupSourceBuffer && this._needCleanupSourceBuffer()) { + this._doCleanupSourceBuffer(); + } + + var sb = this._sourceBuffers[ms.type]; + if (sb && !sb.updating && !this._hasPendingRemoveRanges()) { + this._doAppendSegments(); + } + } + }, { + key: 'seek', + value: function seek(seconds) { + // remove all appended buffers + for (var type in this._sourceBuffers) { + if (!this._sourceBuffers[type]) { + continue; + } + + // abort current buffer append algorithm + var sb = this._sourceBuffers[type]; + if (this._mediaSource.readyState === 'open') { + try { + // If range removal algorithm is running, InvalidStateError will be throwed + // Ignore it. + sb.abort(); + } catch (error) { + _logger2.default.e(this.TAG, error.message); + } + } + + // IDRList should be clear + this._idrList.clear(); + + // pending segments should be discard + var ps = this._pendingSegments[type]; + ps.splice(0, ps.length); + + if (this._mediaSource.readyState === 'closed') { + // Parent MediaSource object has been detached from HTMLMediaElement + continue; + } + + // record ranges to be remove from SourceBuffer + for (var i = 0; i < sb.buffered.length; i++) { + var start = sb.buffered.start(i); + var end = sb.buffered.end(i); + this._pendingRemoveRanges[type].push({ start: start, end: end }); + } + + // if sb is not updating, let's remove ranges now! + if (!sb.updating) { + this._doRemoveRanges(); + } + + // Safari 10 may get InvalidStateError in the later appendBuffer() after SourceBuffer.remove() call + // Internal parser's state may be invalid at this time. Re-append last InitSegment to workaround. + // Related issue: https://bugs.webkit.org/show_bug.cgi?id=159230 + if (_browser2.default.safari) { + var lastInitSegment = this._lastInitSegments[type]; + if (lastInitSegment) { + this._pendingSegments[type].push(lastInitSegment); + if (!sb.updating) { + this._doAppendSegments(); + } + } + } + } + } + }, { + key: 'endOfStream', + value: function endOfStream() { + var ms = this._mediaSource; + var sb = this._sourceBuffers; + if (!ms || ms.readyState !== 'open') { + if (ms && ms.readyState === 'closed' && this._hasPendingSegments()) { + // If MediaSource hasn't turned into open state, and there're pending segments + // Mark pending endOfStream, defer call until all pending segments appended complete + this._hasPendingEos = true; + } + return; + } + if (sb.video && sb.video.updating || sb.audio && sb.audio.updating) { + // If any sourcebuffer is updating, defer endOfStream operation + // See _onSourceBufferUpdateEnd() + this._hasPendingEos = true; + } else { + this._hasPendingEos = false; + // Notify media data loading complete + // This is helpful for correcting total duration to match last media segment + // Otherwise MediaElement's ended event may not be triggered + ms.endOfStream(); + } + } + }, { + key: 'getNearestKeyframe', + value: function getNearestKeyframe(dts) { + return this._idrList.getLastSyncPointBeforeDts(dts); + } + }, { + key: '_needCleanupSourceBuffer', + value: function _needCleanupSourceBuffer() { + if (!this._config.autoCleanupSourceBuffer) { + return false; + } + + var currentTime = this._mediaElement.currentTime; + + for (var type in this._sourceBuffers) { + var sb = this._sourceBuffers[type]; + if (sb) { + var buffered = sb.buffered; + if (buffered.length >= 1) { + if (currentTime - buffered.start(0) >= this._config.autoCleanupMaxBackwardDuration) { + return true; + } + } + } + } + + return false; + } + }, { + key: '_doCleanupSourceBuffer', + value: function _doCleanupSourceBuffer() { + var currentTime = this._mediaElement.currentTime; + + for (var type in this._sourceBuffers) { + var sb = this._sourceBuffers[type]; + if (sb) { + var buffered = sb.buffered; + var doRemove = false; + + for (var i = 0; i < buffered.length; i++) { + var start = buffered.start(i); + var end = buffered.end(i); + + if (start <= currentTime && currentTime < end + 3) { + // padding 3 seconds + if (currentTime - start >= this._config.autoCleanupMaxBackwardDuration) { + doRemove = true; + var removeEnd = currentTime - this._config.autoCleanupMinBackwardDuration; + this._pendingRemoveRanges[type].push({ start: start, end: removeEnd }); + } + } else if (end < currentTime) { + doRemove = true; + this._pendingRemoveRanges[type].push({ start: start, end: end }); + } + } + + if (doRemove && !sb.updating) { + this._doRemoveRanges(); + } + } + } + } + }, { + key: '_updateMediaSourceDuration', + value: function _updateMediaSourceDuration() { + var sb = this._sourceBuffers; + if (this._mediaElement.readyState === 0 || this._mediaSource.readyState !== 'open') { + return; + } + if (sb.video && sb.video.updating || sb.audio && sb.audio.updating) { + return; + } + + var current = this._mediaSource.duration; + var target = this._pendingMediaDuration; + + if (target > 0 && (isNaN(current) || target > current)) { + _logger2.default.v(this.TAG, 'Update MediaSource duration from ' + current + ' to ' + target); + this._mediaSource.duration = target; + } + + this._requireSetMediaDuration = false; + this._pendingMediaDuration = 0; + } + }, { + key: '_doRemoveRanges', + value: function _doRemoveRanges() { + for (var type in this._pendingRemoveRanges) { + if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) { + continue; + } + var sb = this._sourceBuffers[type]; + var ranges = this._pendingRemoveRanges[type]; + while (ranges.length && !sb.updating) { + var range = ranges.shift(); + sb.remove(range.start, range.end); + } + } + } + }, { + key: '_doAppendSegments', + value: function _doAppendSegments() { + var pendingSegments = this._pendingSegments; + + for (var type in pendingSegments) { + if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) { + continue; + } + + if (pendingSegments[type].length > 0) { + var segment = pendingSegments[type].shift(); + + if (segment.timestampOffset) { + // For MPEG audio stream in MSE, if unbuffered-seeking occurred + // We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer. + var currentOffset = this._sourceBuffers[type].timestampOffset; + var targetOffset = segment.timestampOffset / 1000; // in seconds + + var delta = Math.abs(currentOffset - targetOffset); + if (delta > 0.1) { + // If time delta > 100ms + _logger2.default.v(this.TAG, 'Update MPEG audio timestampOffset from ' + currentOffset + ' to ' + targetOffset); + this._sourceBuffers[type].timestampOffset = targetOffset; + } + delete segment.timestampOffset; + } + + if (!segment.data || segment.data.byteLength === 0) { + // Ignore empty buffer + continue; + } + + try { + this._sourceBuffers[type].appendBuffer(segment.data); + this._isBufferFull = false; + if (type === 'video' && segment.hasOwnProperty('info')) { + this._idrList.appendArray(segment.info.syncPoints); + } + } catch (error) { + this._pendingSegments[type].unshift(segment); + if (error.code === 22) { + // QuotaExceededError + /* Notice that FireFox may not throw QuotaExceededError if SourceBuffer is full + * Currently we can only do lazy-load to avoid SourceBuffer become scattered. + * SourceBuffer eviction policy may be changed in future version of FireFox. + * + * Related issues: + * https://bugzilla.mozilla.org/show_bug.cgi?id=1279885 + * https://bugzilla.mozilla.org/show_bug.cgi?id=1280023 + */ + + // report buffer full, abort network IO + if (!this._isBufferFull) { + this._emitter.emit(_mseEvents2.default.BUFFER_FULL); + } + this._isBufferFull = true; + } else { + _logger2.default.e(this.TAG, error.message); + this._emitter.emit(_mseEvents2.default.ERROR, { code: error.code, msg: error.message }); + } + } + } + } + } + }, { + key: '_onSourceOpen', + value: function _onSourceOpen() { + _logger2.default.v(this.TAG, 'MediaSource onSourceOpen'); + this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen); + // deferred sourcebuffer creation / initialization + if (this._pendingSourceBufferInit.length > 0) { + var pendings = this._pendingSourceBufferInit; + while (pendings.length) { + var segment = pendings.shift(); + this.appendInitSegment(segment, true); + } + } + // there may be some pending media segments, append them + if (this._hasPendingSegments()) { + this._doAppendSegments(); + } + this._emitter.emit(_mseEvents2.default.SOURCE_OPEN); + } + }, { + key: '_onSourceEnded', + value: function _onSourceEnded() { + // fired on endOfStream + _logger2.default.v(this.TAG, 'MediaSource onSourceEnded'); + } + }, { + key: '_onSourceClose', + value: function _onSourceClose() { + // fired on detaching from media element + _logger2.default.v(this.TAG, 'MediaSource onSourceClose'); + if (this._mediaSource && this.e != null) { + this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen); + this._mediaSource.removeEventListener('sourceended', this.e.onSourceEnded); + this._mediaSource.removeEventListener('sourceclose', this.e.onSourceClose); + } + } + }, { + key: '_hasPendingSegments', + value: function _hasPendingSegments() { + var ps = this._pendingSegments; + return ps.video.length > 0 || ps.audio.length > 0; + } + }, { + key: '_hasPendingRemoveRanges', + value: function _hasPendingRemoveRanges() { + var prr = this._pendingRemoveRanges; + return prr.video.length > 0 || prr.audio.length > 0; + } + }, { + key: '_onSourceBufferUpdateEnd', + value: function _onSourceBufferUpdateEnd() { + if (this._requireSetMediaDuration) { + this._updateMediaSourceDuration(); + } else if (this._hasPendingRemoveRanges()) { + this._doRemoveRanges(); + } else if (this._hasPendingSegments()) { + this._doAppendSegments(); + } else if (this._hasPendingEos) { + this.endOfStream(); + } + this._emitter.emit(_mseEvents2.default.UPDATE_END); + } + }, { + key: '_onSourceBufferError', + value: function _onSourceBufferError(e) { + _logger2.default.e(this.TAG, 'SourceBuffer Error: ' + e); + // this error might not always be fatal, just ignore it + } + }]); + + return MSEController; +}(); + +exports.default = MSEController; + +},{"../utils/browser.js":39,"../utils/exception.js":40,"../utils/logger.js":41,"./media-segment-info.js":8,"./mse-events.js":10,"events":2}],10:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var MSEEvents = { + ERROR: 'error', + SOURCE_OPEN: 'source_open', + UPDATE_END: 'update_end', + BUFFER_FULL: 'buffer_full' +}; + +exports.default = MSEEvents; + +},{}],11:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _events = _dereq_('events'); + +var _events2 = _interopRequireDefault(_events); + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _loggingControl = _dereq_('../utils/logging-control.js'); + +var _loggingControl2 = _interopRequireDefault(_loggingControl); + +var _transmuxingController = _dereq_('./transmuxing-controller.js'); + +var _transmuxingController2 = _interopRequireDefault(_transmuxingController); + +var _transmuxingEvents = _dereq_('./transmuxing-events.js'); + +var _transmuxingEvents2 = _interopRequireDefault(_transmuxingEvents); + +var _transmuxingWorker = _dereq_('./transmuxing-worker.js'); + +var _transmuxingWorker2 = _interopRequireDefault(_transmuxingWorker); + +var _mediaInfo = _dereq_('./media-info.js'); + +var _mediaInfo2 = _interopRequireDefault(_mediaInfo); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Transmuxer = function () { + function Transmuxer(mediaDataSource, config) { + _classCallCheck(this, Transmuxer); + + this.TAG = 'Transmuxer'; + this._emitter = new _events2.default(); + + if (config.enableWorker && typeof Worker !== 'undefined') { + try { + var work = _dereq_('webworkify'); + this._worker = work(_transmuxingWorker2.default); + this._workerDestroying = false; + this._worker.addEventListener('message', this._onWorkerMessage.bind(this)); + this._worker.postMessage({ cmd: 'init', param: [mediaDataSource, config] }); + this.e = { + onLoggingConfigChanged: this._onLoggingConfigChanged.bind(this) + }; + _loggingControl2.default.registerListener(this.e.onLoggingConfigChanged); + this._worker.postMessage({ cmd: 'logging_config', param: _loggingControl2.default.getConfig() }); + } catch (error) { + _logger2.default.e(this.TAG, 'Error while initialize transmuxing worker, fallback to inline transmuxing'); + this._worker = null; + this._controller = new _transmuxingController2.default(mediaDataSource, config); + } + } else { + this._controller = new _transmuxingController2.default(mediaDataSource, config); + } + + if (this._controller) { + var ctl = this._controller; + ctl.on(_transmuxingEvents2.default.IO_ERROR, this._onIOError.bind(this)); + ctl.on(_transmuxingEvents2.default.DEMUX_ERROR, this._onDemuxError.bind(this)); + ctl.on(_transmuxingEvents2.default.INIT_SEGMENT, this._onInitSegment.bind(this)); + ctl.on(_transmuxingEvents2.default.MEDIA_SEGMENT, this._onMediaSegment.bind(this)); + ctl.on(_transmuxingEvents2.default.LOADING_COMPLETE, this._onLoadingComplete.bind(this)); + ctl.on(_transmuxingEvents2.default.RECOVERED_EARLY_EOF, this._onRecoveredEarlyEof.bind(this)); + ctl.on(_transmuxingEvents2.default.MEDIA_INFO, this._onMediaInfo.bind(this)); + ctl.on(_transmuxingEvents2.default.METADATA_ARRIVED, this._onMetaDataArrived.bind(this)); + ctl.on(_transmuxingEvents2.default.SCRIPTDATA_ARRIVED, this._onScriptDataArrived.bind(this)); + ctl.on(_transmuxingEvents2.default.STATISTICS_INFO, this._onStatisticsInfo.bind(this)); + ctl.on(_transmuxingEvents2.default.RECOMMEND_SEEKPOINT, this._onRecommendSeekpoint.bind(this)); + } + } + + _createClass(Transmuxer, [{ + key: 'destroy', + value: function destroy() { + if (this._worker) { + if (!this._workerDestroying) { + this._workerDestroying = true; + this._worker.postMessage({ cmd: 'destroy' }); + _loggingControl2.default.removeListener(this.e.onLoggingConfigChanged); + this.e = null; + } + } else { + this._controller.destroy(); + this._controller = null; + } + this._emitter.removeAllListeners(); + this._emitter = null; + } + }, { + key: 'on', + value: function on(event, listener) { + this._emitter.addListener(event, listener); + } + }, { + key: 'off', + value: function off(event, listener) { + this._emitter.removeListener(event, listener); + } + }, { + key: 'hasWorker', + value: function hasWorker() { + return this._worker != null; + } + }, { + key: 'open', + value: function open() { + if (this._worker) { + this._worker.postMessage({ cmd: 'start' }); + } else { + this._controller.start(); + } + } + }, { + key: 'close', + value: function close() { + if (this._worker) { + this._worker.postMessage({ cmd: 'stop' }); + } else { + this._controller.stop(); + } + } + }, { + key: 'seek', + value: function seek(milliseconds) { + if (this._worker) { + this._worker.postMessage({ cmd: 'seek', param: milliseconds }); + } else { + this._controller.seek(milliseconds); + } + } + }, { + key: 'pause', + value: function pause() { + if (this._worker) { + this._worker.postMessage({ cmd: 'pause' }); + } else { + this._controller.pause(); + } + } + }, { + key: 'resume', + value: function resume() { + if (this._worker) { + this._worker.postMessage({ cmd: 'resume' }); + } else { + this._controller.resume(); + } + } + }, { + key: '_onInitSegment', + value: function _onInitSegment(type, initSegment) { + var _this = this; + + // do async invoke + Promise.resolve().then(function () { + _this._emitter.emit(_transmuxingEvents2.default.INIT_SEGMENT, type, initSegment); + }); + } + }, { + key: '_onMediaSegment', + value: function _onMediaSegment(type, mediaSegment) { + var _this2 = this; + + Promise.resolve().then(function () { + _this2._emitter.emit(_transmuxingEvents2.default.MEDIA_SEGMENT, type, mediaSegment); + }); + } + }, { + key: '_onLoadingComplete', + value: function _onLoadingComplete() { + var _this3 = this; + + Promise.resolve().then(function () { + _this3._emitter.emit(_transmuxingEvents2.default.LOADING_COMPLETE); + }); + } + }, { + key: '_onRecoveredEarlyEof', + value: function _onRecoveredEarlyEof() { + var _this4 = this; + + Promise.resolve().then(function () { + _this4._emitter.emit(_transmuxingEvents2.default.RECOVERED_EARLY_EOF); + }); + } + }, { + key: '_onMediaInfo', + value: function _onMediaInfo(mediaInfo) { + var _this5 = this; + + Promise.resolve().then(function () { + _this5._emitter.emit(_transmuxingEvents2.default.MEDIA_INFO, mediaInfo); + }); + } + }, { + key: '_onMetaDataArrived', + value: function _onMetaDataArrived(metadata) { + var _this6 = this; + + Promise.resolve().then(function () { + _this6._emitter.emit(_transmuxingEvents2.default.METADATA_ARRIVED, metadata); + }); + } + }, { + key: '_onScriptDataArrived', + value: function _onScriptDataArrived(data) { + var _this7 = this; + + Promise.resolve().then(function () { + _this7._emitter.emit(_transmuxingEvents2.default.SCRIPTDATA_ARRIVED, data); + }); + } + }, { + key: '_onStatisticsInfo', + value: function _onStatisticsInfo(statisticsInfo) { + var _this8 = this; + + Promise.resolve().then(function () { + _this8._emitter.emit(_transmuxingEvents2.default.STATISTICS_INFO, statisticsInfo); + }); + } + }, { + key: '_onIOError', + value: function _onIOError(type, info) { + var _this9 = this; + + Promise.resolve().then(function () { + _this9._emitter.emit(_transmuxingEvents2.default.IO_ERROR, type, info); + }); + } + }, { + key: '_onDemuxError', + value: function _onDemuxError(type, info) { + var _this10 = this; + + Promise.resolve().then(function () { + _this10._emitter.emit(_transmuxingEvents2.default.DEMUX_ERROR, type, info); + }); + } + }, { + key: '_onRecommendSeekpoint', + value: function _onRecommendSeekpoint(milliseconds) { + var _this11 = this; + + Promise.resolve().then(function () { + _this11._emitter.emit(_transmuxingEvents2.default.RECOMMEND_SEEKPOINT, milliseconds); + }); + } + }, { + key: '_onLoggingConfigChanged', + value: function _onLoggingConfigChanged(config) { + if (this._worker) { + this._worker.postMessage({ cmd: 'logging_config', param: config }); + } + } + }, { + key: '_onWorkerMessage', + value: function _onWorkerMessage(e) { + var message = e.data; + var data = message.data; + + if (message.msg === 'destroyed' || this._workerDestroying) { + this._workerDestroying = false; + this._worker.terminate(); + this._worker = null; + return; + } + + switch (message.msg) { + case _transmuxingEvents2.default.INIT_SEGMENT: + case _transmuxingEvents2.default.MEDIA_SEGMENT: + this._emitter.emit(message.msg, data.type, data.data); + break; + case _transmuxingEvents2.default.LOADING_COMPLETE: + case _transmuxingEvents2.default.RECOVERED_EARLY_EOF: + this._emitter.emit(message.msg); + break; + case _transmuxingEvents2.default.MEDIA_INFO: + Object.setPrototypeOf(data, _mediaInfo2.default.prototype); + this._emitter.emit(message.msg, data); + break; + case _transmuxingEvents2.default.METADATA_ARRIVED: + case _transmuxingEvents2.default.SCRIPTDATA_ARRIVED: + case _transmuxingEvents2.default.STATISTICS_INFO: + this._emitter.emit(message.msg, data); + break; + case _transmuxingEvents2.default.IO_ERROR: + case _transmuxingEvents2.default.DEMUX_ERROR: + this._emitter.emit(message.msg, data.type, data.info); + break; + case _transmuxingEvents2.default.RECOMMEND_SEEKPOINT: + this._emitter.emit(message.msg, data); + break; + case 'logcat_callback': + _logger2.default.emitter.emit('log', data.type, data.logcat); + break; + default: + break; + } + } + }]); + + return Transmuxer; +}(); + +exports.default = Transmuxer; + +},{"../utils/logger.js":41,"../utils/logging-control.js":42,"./media-info.js":7,"./transmuxing-controller.js":12,"./transmuxing-events.js":13,"./transmuxing-worker.js":14,"events":2,"webworkify":4}],12:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _events = _dereq_('events'); + +var _events2 = _interopRequireDefault(_events); + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _browser = _dereq_('../utils/browser.js'); + +var _browser2 = _interopRequireDefault(_browser); + +var _mediaInfo = _dereq_('./media-info.js'); + +var _mediaInfo2 = _interopRequireDefault(_mediaInfo); + +var _flvDemuxer = _dereq_('../demux/flv-demuxer.js'); + +var _flvDemuxer2 = _interopRequireDefault(_flvDemuxer); + +var _mp4Remuxer = _dereq_('../remux/mp4-remuxer.js'); + +var _mp4Remuxer2 = _interopRequireDefault(_mp4Remuxer); + +var _demuxErrors = _dereq_('../demux/demux-errors.js'); + +var _demuxErrors2 = _interopRequireDefault(_demuxErrors); + +var _ioController = _dereq_('../io/io-controller.js'); + +var _ioController2 = _interopRequireDefault(_ioController); + +var _transmuxingEvents = _dereq_('./transmuxing-events.js'); + +var _transmuxingEvents2 = _interopRequireDefault(_transmuxingEvents); + +var _loader = _dereq_('../io/loader.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +// Transmuxing (IO, Demuxing, Remuxing) controller, with multipart support +var TransmuxingController = function () { + function TransmuxingController(mediaDataSource, config) { + _classCallCheck(this, TransmuxingController); + + this.TAG = 'TransmuxingController'; + this._emitter = new _events2.default(); + + this._config = config; + + // treat single part media as multipart media, which has only one segment + if (!mediaDataSource.segments) { + mediaDataSource.segments = [{ + duration: mediaDataSource.duration, + filesize: mediaDataSource.filesize, + url: mediaDataSource.url + }]; + } + + // fill in default IO params if not exists + if (typeof mediaDataSource.cors !== 'boolean') { + mediaDataSource.cors = true; + } + if (typeof mediaDataSource.withCredentials !== 'boolean') { + mediaDataSource.withCredentials = false; + } + + this._mediaDataSource = mediaDataSource; + this._currentSegmentIndex = 0; + var totalDuration = 0; + + this._mediaDataSource.segments.forEach(function (segment) { + // timestampBase for each segment, and calculate total duration + segment.timestampBase = totalDuration; + totalDuration += segment.duration; + // params needed by IOController + segment.cors = mediaDataSource.cors; + segment.withCredentials = mediaDataSource.withCredentials; + // referrer policy control, if exist + if (config.referrerPolicy) { + segment.referrerPolicy = config.referrerPolicy; + } + }); + + if (!isNaN(totalDuration) && this._mediaDataSource.duration !== totalDuration) { + this._mediaDataSource.duration = totalDuration; + } + + this._mediaInfo = null; + this._demuxer = null; + this._remuxer = null; + this._ioctl = null; + + this._pendingSeekTime = null; + this._pendingResolveSeekPoint = null; + + this._statisticsReporter = null; + } + + _createClass(TransmuxingController, [{ + key: 'destroy', + value: function destroy() { + this._mediaInfo = null; + this._mediaDataSource = null; + + if (this._statisticsReporter) { + this._disableStatisticsReporter(); + } + if (this._ioctl) { + this._ioctl.destroy(); + this._ioctl = null; + } + if (this._demuxer) { + this._demuxer.destroy(); + this._demuxer = null; + } + if (this._remuxer) { + this._remuxer.destroy(); + this._remuxer = null; + } + + this._emitter.removeAllListeners(); + this._emitter = null; + } + }, { + key: 'on', + value: function on(event, listener) { + this._emitter.addListener(event, listener); + } + }, { + key: 'off', + value: function off(event, listener) { + this._emitter.removeListener(event, listener); + } + }, { + key: 'start', + value: function start() { + this._loadSegment(0); + this._enableStatisticsReporter(); + } + }, { + key: '_loadSegment', + value: function _loadSegment(segmentIndex, optionalFrom) { + this._currentSegmentIndex = segmentIndex; + var dataSource = this._mediaDataSource.segments[segmentIndex]; + + var ioctl = this._ioctl = new _ioController2.default(dataSource, this._config, segmentIndex); + ioctl.onError = this._onIOException.bind(this); + ioctl.onSeeked = this._onIOSeeked.bind(this); + ioctl.onComplete = this._onIOComplete.bind(this); + ioctl.onRedirect = this._onIORedirect.bind(this); + ioctl.onRecoveredEarlyEof = this._onIORecoveredEarlyEof.bind(this); + + if (optionalFrom) { + this._demuxer.bindDataSource(this._ioctl); + } else { + ioctl.onDataArrival = this._onInitChunkArrival.bind(this); + } + + ioctl.open(optionalFrom); + } + }, { + key: 'stop', + value: function stop() { + this._internalAbort(); + this._disableStatisticsReporter(); + } + }, { + key: '_internalAbort', + value: function _internalAbort() { + if (this._ioctl) { + this._ioctl.destroy(); + this._ioctl = null; + } + } + }, { + key: 'pause', + value: function pause() { + // take a rest + if (this._ioctl && this._ioctl.isWorking()) { + this._ioctl.pause(); + this._disableStatisticsReporter(); + } + } + }, { + key: 'resume', + value: function resume() { + if (this._ioctl && this._ioctl.isPaused()) { + this._ioctl.resume(); + this._enableStatisticsReporter(); + } + } + }, { + key: 'seek', + value: function seek(milliseconds) { + if (this._mediaInfo == null || !this._mediaInfo.isSeekable()) { + return; + } + + var targetSegmentIndex = this._searchSegmentIndexContains(milliseconds); + + if (targetSegmentIndex === this._currentSegmentIndex) { + // intra-segment seeking + var segmentInfo = this._mediaInfo.segments[targetSegmentIndex]; + + if (segmentInfo == undefined) { + // current segment loading started, but mediainfo hasn't received yet + // wait for the metadata loaded, then seek to expected position + this._pendingSeekTime = milliseconds; + } else { + var keyframe = segmentInfo.getNearestKeyframe(milliseconds); + this._remuxer.seek(keyframe.milliseconds); + this._ioctl.seek(keyframe.fileposition); + // Will be resolved in _onRemuxerMediaSegmentArrival() + this._pendingResolveSeekPoint = keyframe.milliseconds; + } + } else { + // cross-segment seeking + var targetSegmentInfo = this._mediaInfo.segments[targetSegmentIndex]; + + if (targetSegmentInfo == undefined) { + // target segment hasn't been loaded. We need metadata then seek to expected time + this._pendingSeekTime = milliseconds; + this._internalAbort(); + this._remuxer.seek(); + this._remuxer.insertDiscontinuity(); + this._loadSegment(targetSegmentIndex); + // Here we wait for the metadata loaded, then seek to expected position + } else { + // We have target segment's metadata, direct seek to target position + var _keyframe = targetSegmentInfo.getNearestKeyframe(milliseconds); + this._internalAbort(); + this._remuxer.seek(milliseconds); + this._remuxer.insertDiscontinuity(); + this._demuxer.resetMediaInfo(); + this._demuxer.timestampBase = this._mediaDataSource.segments[targetSegmentIndex].timestampBase; + this._loadSegment(targetSegmentIndex, _keyframe.fileposition); + this._pendingResolveSeekPoint = _keyframe.milliseconds; + this._reportSegmentMediaInfo(targetSegmentIndex); + } + } + + this._enableStatisticsReporter(); + } + }, { + key: '_searchSegmentIndexContains', + value: function _searchSegmentIndexContains(milliseconds) { + var segments = this._mediaDataSource.segments; + var idx = segments.length - 1; + + for (var i = 0; i < segments.length; i++) { + if (milliseconds < segments[i].timestampBase) { + idx = i - 1; + break; + } + } + return idx; + } + }, { + key: '_onInitChunkArrival', + value: function _onInitChunkArrival(data, byteStart) { + var _this = this; + + var probeData = null; + var consumed = 0; + + if (byteStart > 0) { + // IOController seeked immediately after opened, byteStart > 0 callback may received + this._demuxer.bindDataSource(this._ioctl); + this._demuxer.timestampBase = this._mediaDataSource.segments[this._currentSegmentIndex].timestampBase; + + consumed = this._demuxer.parseChunks(data, byteStart); + } else if ((probeData = _flvDemuxer2.default.probe(data)).match) { + // Always create new FLVDemuxer + this._demuxer = new _flvDemuxer2.default(probeData, this._config); + + if (!this._remuxer) { + this._remuxer = new _mp4Remuxer2.default(this._config); + } + + var mds = this._mediaDataSource; + if (mds.duration != undefined && !isNaN(mds.duration)) { + this._demuxer.overridedDuration = mds.duration; + } + if (typeof mds.hasAudio === 'boolean') { + this._demuxer.overridedHasAudio = mds.hasAudio; + } + if (typeof mds.hasVideo === 'boolean') { + this._demuxer.overridedHasVideo = mds.hasVideo; + } + + this._demuxer.timestampBase = mds.segments[this._currentSegmentIndex].timestampBase; + + this._demuxer.onError = this._onDemuxException.bind(this); + this._demuxer.onMediaInfo = this._onMediaInfo.bind(this); + this._demuxer.onMetaDataArrived = this._onMetaDataArrived.bind(this); + this._demuxer.onScriptDataArrived = this._onScriptDataArrived.bind(this); + + this._remuxer.bindDataSource(this._demuxer.bindDataSource(this._ioctl)); + + this._remuxer.onInitSegment = this._onRemuxerInitSegmentArrival.bind(this); + this._remuxer.onMediaSegment = this._onRemuxerMediaSegmentArrival.bind(this); + + consumed = this._demuxer.parseChunks(data, byteStart); + } else { + probeData = null; + _logger2.default.e(this.TAG, 'Non-FLV, Unsupported media type!'); + Promise.resolve().then(function () { + _this._internalAbort(); + }); + this._emitter.emit(_transmuxingEvents2.default.DEMUX_ERROR, _demuxErrors2.default.FORMAT_UNSUPPORTED, 'Non-FLV, Unsupported media type'); + + consumed = 0; + } + + return consumed; + } + }, { + key: '_onMediaInfo', + value: function _onMediaInfo(mediaInfo) { + var _this2 = this; + + if (this._mediaInfo == null) { + // Store first segment's mediainfo as global mediaInfo + this._mediaInfo = Object.assign({}, mediaInfo); + this._mediaInfo.keyframesIndex = null; + this._mediaInfo.segments = []; + this._mediaInfo.segmentCount = this._mediaDataSource.segments.length; + Object.setPrototypeOf(this._mediaInfo, _mediaInfo2.default.prototype); + } + + var segmentInfo = Object.assign({}, mediaInfo); + Object.setPrototypeOf(segmentInfo, _mediaInfo2.default.prototype); + this._mediaInfo.segments[this._currentSegmentIndex] = segmentInfo; + + // notify mediaInfo update + this._reportSegmentMediaInfo(this._currentSegmentIndex); + + if (this._pendingSeekTime != null) { + Promise.resolve().then(function () { + var target = _this2._pendingSeekTime; + _this2._pendingSeekTime = null; + _this2.seek(target); + }); + } + } + }, { + key: '_onMetaDataArrived', + value: function _onMetaDataArrived(metadata) { + this._emitter.emit(_transmuxingEvents2.default.METADATA_ARRIVED, metadata); + } + }, { + key: '_onScriptDataArrived', + value: function _onScriptDataArrived(data) { + this._emitter.emit(_transmuxingEvents2.default.SCRIPTDATA_ARRIVED, data); + } + }, { + key: '_onIOSeeked', + value: function _onIOSeeked() { + this._remuxer.insertDiscontinuity(); + } + }, { + key: '_onIOComplete', + value: function _onIOComplete(extraData) { + var segmentIndex = extraData; + var nextSegmentIndex = segmentIndex + 1; + + if (nextSegmentIndex < this._mediaDataSource.segments.length) { + this._internalAbort(); + this._remuxer.flushStashedSamples(); + this._loadSegment(nextSegmentIndex); + } else { + this._remuxer.flushStashedSamples(); + this._emitter.emit(_transmuxingEvents2.default.LOADING_COMPLETE); + this._disableStatisticsReporter(); + } + } + }, { + key: '_onIORedirect', + value: function _onIORedirect(redirectedURL) { + var segmentIndex = this._ioctl.extraData; + this._mediaDataSource.segments[segmentIndex].redirectedURL = redirectedURL; + } + }, { + key: '_onIORecoveredEarlyEof', + value: function _onIORecoveredEarlyEof() { + this._emitter.emit(_transmuxingEvents2.default.RECOVERED_EARLY_EOF); + } + }, { + key: '_onIOException', + value: function _onIOException(type, info) { + _logger2.default.e(this.TAG, 'IOException: type = ' + type + ', code = ' + info.code + ', msg = ' + info.msg); + this._emitter.emit(_transmuxingEvents2.default.IO_ERROR, type, info); + this._disableStatisticsReporter(); + } + }, { + key: '_onDemuxException', + value: function _onDemuxException(type, info) { + _logger2.default.e(this.TAG, 'DemuxException: type = ' + type + ', info = ' + info); + this._emitter.emit(_transmuxingEvents2.default.DEMUX_ERROR, type, info); + } + }, { + key: '_onRemuxerInitSegmentArrival', + value: function _onRemuxerInitSegmentArrival(type, initSegment) { + this._emitter.emit(_transmuxingEvents2.default.INIT_SEGMENT, type, initSegment); + } + }, { + key: '_onRemuxerMediaSegmentArrival', + value: function _onRemuxerMediaSegmentArrival(type, mediaSegment) { + if (this._pendingSeekTime != null) { + // Media segments after new-segment cross-seeking should be dropped. + return; + } + this._emitter.emit(_transmuxingEvents2.default.MEDIA_SEGMENT, type, mediaSegment); + + // Resolve pending seekPoint + if (this._pendingResolveSeekPoint != null && type === 'video') { + var syncPoints = mediaSegment.info.syncPoints; + var seekpoint = this._pendingResolveSeekPoint; + this._pendingResolveSeekPoint = null; + + // Safari: Pass PTS for recommend_seekpoint + if (_browser2.default.safari && syncPoints.length > 0 && syncPoints[0].originalDts === seekpoint) { + seekpoint = syncPoints[0].pts; + } + // else: use original DTS (keyframe.milliseconds) + + this._emitter.emit(_transmuxingEvents2.default.RECOMMEND_SEEKPOINT, seekpoint); + } + } + }, { + key: '_enableStatisticsReporter', + value: function _enableStatisticsReporter() { + if (this._statisticsReporter == null) { + this._statisticsReporter = self.setInterval(this._reportStatisticsInfo.bind(this), this._config.statisticsInfoReportInterval); + } + } + }, { + key: '_disableStatisticsReporter', + value: function _disableStatisticsReporter() { + if (this._statisticsReporter) { + self.clearInterval(this._statisticsReporter); + this._statisticsReporter = null; + } + } + }, { + key: '_reportSegmentMediaInfo', + value: function _reportSegmentMediaInfo(segmentIndex) { + var segmentInfo = this._mediaInfo.segments[segmentIndex]; + var exportInfo = Object.assign({}, segmentInfo); + + exportInfo.duration = this._mediaInfo.duration; + exportInfo.segmentCount = this._mediaInfo.segmentCount; + delete exportInfo.segments; + delete exportInfo.keyframesIndex; + + this._emitter.emit(_transmuxingEvents2.default.MEDIA_INFO, exportInfo); + } + }, { + key: '_reportStatisticsInfo', + value: function _reportStatisticsInfo() { + var info = {}; + + info.url = this._ioctl.currentURL; + info.hasRedirect = this._ioctl.hasRedirect; + if (info.hasRedirect) { + info.redirectedURL = this._ioctl.currentRedirectedURL; + } + + info.speed = this._ioctl.currentSpeed; + info.loaderType = this._ioctl.loaderType; + info.currentSegmentIndex = this._currentSegmentIndex; + info.totalSegmentCount = this._mediaDataSource.segments.length; + + this._emitter.emit(_transmuxingEvents2.default.STATISTICS_INFO, info); + } + }]); + + return TransmuxingController; +}(); + +exports.default = TransmuxingController; + +},{"../demux/demux-errors.js":16,"../demux/flv-demuxer.js":18,"../io/io-controller.js":23,"../io/loader.js":24,"../remux/mp4-remuxer.js":38,"../utils/browser.js":39,"../utils/logger.js":41,"./media-info.js":7,"./transmuxing-events.js":13,"events":2}],13:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var TransmuxingEvents = { + IO_ERROR: 'io_error', + DEMUX_ERROR: 'demux_error', + INIT_SEGMENT: 'init_segment', + MEDIA_SEGMENT: 'media_segment', + LOADING_COMPLETE: 'loading_complete', + RECOVERED_EARLY_EOF: 'recovered_early_eof', + MEDIA_INFO: 'media_info', + METADATA_ARRIVED: 'metadata_arrived', + SCRIPTDATA_ARRIVED: 'scriptdata_arrived', + STATISTICS_INFO: 'statistics_info', + RECOMMEND_SEEKPOINT: 'recommend_seekpoint' +}; + +exports.default = TransmuxingEvents; + +},{}],14:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _loggingControl = _dereq_('../utils/logging-control.js'); + +var _loggingControl2 = _interopRequireDefault(_loggingControl); + +var _polyfill = _dereq_('../utils/polyfill.js'); + +var _polyfill2 = _interopRequireDefault(_polyfill); + +var _transmuxingController = _dereq_('./transmuxing-controller.js'); + +var _transmuxingController2 = _interopRequireDefault(_transmuxingController); + +var _transmuxingEvents = _dereq_('./transmuxing-events.js'); + +var _transmuxingEvents2 = _interopRequireDefault(_transmuxingEvents); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/* post message to worker: + data: { + cmd: string + param: any + } + + receive message from worker: + data: { + msg: string, + data: any + } + */ + +var TransmuxingWorker = function TransmuxingWorker(self) { + + var TAG = 'TransmuxingWorker'; + var controller = null; + var logcatListener = onLogcatCallback.bind(this); + + _polyfill2.default.install(); + + self.addEventListener('message', function (e) { + switch (e.data.cmd) { + case 'init': + controller = new _transmuxingController2.default(e.data.param[0], e.data.param[1]); + controller.on(_transmuxingEvents2.default.IO_ERROR, onIOError.bind(this)); + controller.on(_transmuxingEvents2.default.DEMUX_ERROR, onDemuxError.bind(this)); + controller.on(_transmuxingEvents2.default.INIT_SEGMENT, onInitSegment.bind(this)); + controller.on(_transmuxingEvents2.default.MEDIA_SEGMENT, onMediaSegment.bind(this)); + controller.on(_transmuxingEvents2.default.LOADING_COMPLETE, onLoadingComplete.bind(this)); + controller.on(_transmuxingEvents2.default.RECOVERED_EARLY_EOF, onRecoveredEarlyEof.bind(this)); + controller.on(_transmuxingEvents2.default.MEDIA_INFO, onMediaInfo.bind(this)); + controller.on(_transmuxingEvents2.default.METADATA_ARRIVED, onMetaDataArrived.bind(this)); + controller.on(_transmuxingEvents2.default.SCRIPTDATA_ARRIVED, onScriptDataArrived.bind(this)); + controller.on(_transmuxingEvents2.default.STATISTICS_INFO, onStatisticsInfo.bind(this)); + controller.on(_transmuxingEvents2.default.RECOMMEND_SEEKPOINT, onRecommendSeekpoint.bind(this)); + break; + case 'destroy': + if (controller) { + controller.destroy(); + controller = null; + } + self.postMessage({ msg: 'destroyed' }); + break; + case 'start': + controller.start(); + break; + case 'stop': + controller.stop(); + break; + case 'seek': + controller.seek(e.data.param); + break; + case 'pause': + controller.pause(); + break; + case 'resume': + controller.resume(); + break; + case 'logging_config': + { + var config = e.data.param; + _loggingControl2.default.applyConfig(config); + + if (config.enableCallback === true) { + _loggingControl2.default.addLogListener(logcatListener); + } else { + _loggingControl2.default.removeLogListener(logcatListener); + } + break; + } + } + }); + + function onInitSegment(type, initSegment) { + var obj = { + msg: _transmuxingEvents2.default.INIT_SEGMENT, + data: { + type: type, + data: initSegment + } + }; + self.postMessage(obj, [initSegment.data]); // data: ArrayBuffer + } + + function onMediaSegment(type, mediaSegment) { + var obj = { + msg: _transmuxingEvents2.default.MEDIA_SEGMENT, + data: { + type: type, + data: mediaSegment + } + }; + self.postMessage(obj, [mediaSegment.data]); // data: ArrayBuffer + } + + function onLoadingComplete() { + var obj = { + msg: _transmuxingEvents2.default.LOADING_COMPLETE + }; + self.postMessage(obj); + } + + function onRecoveredEarlyEof() { + var obj = { + msg: _transmuxingEvents2.default.RECOVERED_EARLY_EOF + }; + self.postMessage(obj); + } + + function onMediaInfo(mediaInfo) { + var obj = { + msg: _transmuxingEvents2.default.MEDIA_INFO, + data: mediaInfo + }; + self.postMessage(obj); + } + + function onMetaDataArrived(metadata) { + var obj = { + msg: _transmuxingEvents2.default.METADATA_ARRIVED, + data: metadata + }; + self.postMessage(obj); + } + + function onScriptDataArrived(data) { + var obj = { + msg: _transmuxingEvents2.default.SCRIPTDATA_ARRIVED, + data: data + }; + self.postMessage(obj); + } + + function onStatisticsInfo(statInfo) { + var obj = { + msg: _transmuxingEvents2.default.STATISTICS_INFO, + data: statInfo + }; + self.postMessage(obj); + } + + function onIOError(type, info) { + self.postMessage({ + msg: _transmuxingEvents2.default.IO_ERROR, + data: { + type: type, + info: info + } + }); + } + + function onDemuxError(type, info) { + self.postMessage({ + msg: _transmuxingEvents2.default.DEMUX_ERROR, + data: { + type: type, + info: info + } + }); + } + + function onRecommendSeekpoint(milliseconds) { + self.postMessage({ + msg: _transmuxingEvents2.default.RECOMMEND_SEEKPOINT, + data: milliseconds + }); + } + + function onLogcatCallback(type, str) { + self.postMessage({ + msg: 'logcat_callback', + data: { + type: type, + logcat: str + } + }); + } +}; /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.default = TransmuxingWorker; + +},{"../utils/logger.js":41,"../utils/logging-control.js":42,"../utils/polyfill.js":43,"./transmuxing-controller.js":12,"./transmuxing-events.js":13}],15:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _utf8Conv = _dereq_('../utils/utf8-conv.js'); + +var _utf8Conv2 = _interopRequireDefault(_utf8Conv); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var le = function () { + var buf = new ArrayBuffer(2); + new DataView(buf).setInt16(0, 256, true); // little-endian write + return new Int16Array(buf)[0] === 256; // platform-spec read, if equal then LE +}(); + +var AMF = function () { + function AMF() { + _classCallCheck(this, AMF); + } + + _createClass(AMF, null, [{ + key: 'parseScriptData', + value: function parseScriptData(arrayBuffer, dataOffset, dataSize) { + var data = {}; + + try { + var name = AMF.parseValue(arrayBuffer, dataOffset, dataSize); + var value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size); + + data[name.data] = value.data; + } catch (e) { + _logger2.default.e('AMF', e.toString()); + } + + return data; + } + }, { + key: 'parseObject', + value: function parseObject(arrayBuffer, dataOffset, dataSize) { + if (dataSize < 3) { + throw new _exception.IllegalStateException('Data not enough when parse ScriptDataObject'); + } + var name = AMF.parseString(arrayBuffer, dataOffset, dataSize); + var value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size); + var isObjectEnd = value.objectEnd; + + return { + data: { + name: name.data, + value: value.data + }, + size: name.size + value.size, + objectEnd: isObjectEnd + }; + } + }, { + key: 'parseVariable', + value: function parseVariable(arrayBuffer, dataOffset, dataSize) { + return AMF.parseObject(arrayBuffer, dataOffset, dataSize); + } + }, { + key: 'parseString', + value: function parseString(arrayBuffer, dataOffset, dataSize) { + if (dataSize < 2) { + throw new _exception.IllegalStateException('Data not enough when parse String'); + } + var v = new DataView(arrayBuffer, dataOffset, dataSize); + var length = v.getUint16(0, !le); + + var str = void 0; + if (length > 0) { + str = (0, _utf8Conv2.default)(new Uint8Array(arrayBuffer, dataOffset + 2, length)); + } else { + str = ''; + } + + return { + data: str, + size: 2 + length + }; + } + }, { + key: 'parseLongString', + value: function parseLongString(arrayBuffer, dataOffset, dataSize) { + if (dataSize < 4) { + throw new _exception.IllegalStateException('Data not enough when parse LongString'); + } + var v = new DataView(arrayBuffer, dataOffset, dataSize); + var length = v.getUint32(0, !le); + + var str = void 0; + if (length > 0) { + str = (0, _utf8Conv2.default)(new Uint8Array(arrayBuffer, dataOffset + 4, length)); + } else { + str = ''; + } + + return { + data: str, + size: 4 + length + }; + } + }, { + key: 'parseDate', + value: function parseDate(arrayBuffer, dataOffset, dataSize) { + if (dataSize < 10) { + throw new _exception.IllegalStateException('Data size invalid when parse Date'); + } + var v = new DataView(arrayBuffer, dataOffset, dataSize); + var timestamp = v.getFloat64(0, !le); + var localTimeOffset = v.getInt16(8, !le); + timestamp += localTimeOffset * 60 * 1000; // get UTC time + + return { + data: new Date(timestamp), + size: 8 + 2 + }; + } + }, { + key: 'parseValue', + value: function parseValue(arrayBuffer, dataOffset, dataSize) { + if (dataSize < 1) { + throw new _exception.IllegalStateException('Data not enough when parse Value'); + } + + var v = new DataView(arrayBuffer, dataOffset, dataSize); + + var offset = 1; + var type = v.getUint8(0); + var value = void 0; + var objectEnd = false; + + try { + switch (type) { + case 0: + // Number(Double) type + value = v.getFloat64(1, !le); + offset += 8; + break; + case 1: + { + // Boolean type + var b = v.getUint8(1); + value = b ? true : false; + offset += 1; + break; + } + case 2: + { + // String type + var amfstr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1); + value = amfstr.data; + offset += amfstr.size; + break; + } + case 3: + { + // Object(s) type + value = {}; + var terminal = 0; // workaround for malformed Objects which has missing ScriptDataObjectEnd + if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) { + terminal = 3; + } + while (offset < dataSize - 4) { + // 4 === type(UI8) + ScriptDataObjectEnd(UI24) + var amfobj = AMF.parseObject(arrayBuffer, dataOffset + offset, dataSize - offset - terminal); + if (amfobj.objectEnd) break; + value[amfobj.data.name] = amfobj.data.value; + offset += amfobj.size; + } + if (offset <= dataSize - 3) { + var marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF; + if (marker === 9) { + offset += 3; + } + } + break; + } + case 8: + { + // ECMA array type (Mixed array) + value = {}; + offset += 4; // ECMAArrayLength(UI32) + var _terminal = 0; // workaround for malformed MixedArrays which has missing ScriptDataObjectEnd + if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) { + _terminal = 3; + } + while (offset < dataSize - 8) { + // 8 === type(UI8) + ECMAArrayLength(UI32) + ScriptDataVariableEnd(UI24) + var amfvar = AMF.parseVariable(arrayBuffer, dataOffset + offset, dataSize - offset - _terminal); + if (amfvar.objectEnd) break; + value[amfvar.data.name] = amfvar.data.value; + offset += amfvar.size; + } + if (offset <= dataSize - 3) { + var _marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF; + if (_marker === 9) { + offset += 3; + } + } + break; + } + case 9: + // ScriptDataObjectEnd + value = undefined; + offset = 1; + objectEnd = true; + break; + case 10: + { + // Strict array type + // ScriptDataValue[n]. NOTE: according to video_file_format_spec_v10_1.pdf + value = []; + var strictArrayLength = v.getUint32(1, !le); + offset += 4; + for (var i = 0; i < strictArrayLength; i++) { + var val = AMF.parseValue(arrayBuffer, dataOffset + offset, dataSize - offset); + value.push(val.data); + offset += val.size; + } + break; + } + case 11: + { + // Date type + var date = AMF.parseDate(arrayBuffer, dataOffset + 1, dataSize - 1); + value = date.data; + offset += date.size; + break; + } + case 12: + { + // Long string type + var amfLongStr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1); + value = amfLongStr.data; + offset += amfLongStr.size; + break; + } + default: + // ignore and skip + offset = dataSize; + _logger2.default.w('AMF', 'Unsupported AMF value type ' + type); + } + } catch (e) { + _logger2.default.e('AMF', e.toString()); + } + + return { + data: value, + size: offset, + objectEnd: objectEnd + }; + } + }]); + + return AMF; +}(); + +exports.default = AMF; + +},{"../utils/exception.js":40,"../utils/logger.js":41,"../utils/utf8-conv.js":44}],16:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var DemuxErrors = { + OK: 'OK', + FORMAT_ERROR: 'FormatError', + FORMAT_UNSUPPORTED: 'FormatUnsupported', + CODEC_UNSUPPORTED: 'CodecUnsupported' +}; + +exports.default = DemuxErrors; + +},{}],17:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _exception = _dereq_('../utils/exception.js'); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +// Exponential-Golomb buffer decoder +var ExpGolomb = function () { + function ExpGolomb(uint8array) { + _classCallCheck(this, ExpGolomb); + + this.TAG = 'ExpGolomb'; + + this._buffer = uint8array; + this._buffer_index = 0; + this._total_bytes = uint8array.byteLength; + this._total_bits = uint8array.byteLength * 8; + this._current_word = 0; + this._current_word_bits_left = 0; + } + + _createClass(ExpGolomb, [{ + key: 'destroy', + value: function destroy() { + this._buffer = null; + } + }, { + key: '_fillCurrentWord', + value: function _fillCurrentWord() { + var buffer_bytes_left = this._total_bytes - this._buffer_index; + if (buffer_bytes_left <= 0) throw new _exception.IllegalStateException('ExpGolomb: _fillCurrentWord() but no bytes available'); + + var bytes_read = Math.min(4, buffer_bytes_left); + var word = new Uint8Array(4); + word.set(this._buffer.subarray(this._buffer_index, this._buffer_index + bytes_read)); + this._current_word = new DataView(word.buffer).getUint32(0, false); + + this._buffer_index += bytes_read; + this._current_word_bits_left = bytes_read * 8; + } + }, { + key: 'readBits', + value: function readBits(bits) { + if (bits > 32) throw new _exception.InvalidArgumentException('ExpGolomb: readBits() bits exceeded max 32bits!'); + + if (bits <= this._current_word_bits_left) { + var _result = this._current_word >>> 32 - bits; + this._current_word <<= bits; + this._current_word_bits_left -= bits; + return _result; + } + + var result = this._current_word_bits_left ? this._current_word : 0; + result = result >>> 32 - this._current_word_bits_left; + var bits_need_left = bits - this._current_word_bits_left; + + this._fillCurrentWord(); + var bits_read_next = Math.min(bits_need_left, this._current_word_bits_left); + + var result2 = this._current_word >>> 32 - bits_read_next; + this._current_word <<= bits_read_next; + this._current_word_bits_left -= bits_read_next; + + result = result << bits_read_next | result2; + return result; + } + }, { + key: 'readBool', + value: function readBool() { + return this.readBits(1) === 1; + } + }, { + key: 'readByte', + value: function readByte() { + return this.readBits(8); + } + }, { + key: '_skipLeadingZero', + value: function _skipLeadingZero() { + var zero_count = void 0; + for (zero_count = 0; zero_count < this._current_word_bits_left; zero_count++) { + if (0 !== (this._current_word & 0x80000000 >>> zero_count)) { + this._current_word <<= zero_count; + this._current_word_bits_left -= zero_count; + return zero_count; + } + } + this._fillCurrentWord(); + return zero_count + this._skipLeadingZero(); + } + }, { + key: 'readUEG', + value: function readUEG() { + // unsigned exponential golomb + var leading_zeros = this._skipLeadingZero(); + return this.readBits(leading_zeros + 1) - 1; + } + }, { + key: 'readSEG', + value: function readSEG() { + // signed exponential golomb + var value = this.readUEG(); + if (value & 0x01) { + return value + 1 >>> 1; + } else { + return -1 * (value >>> 1); + } + } + }]); + + return ExpGolomb; +}(); + +exports.default = ExpGolomb; + +},{"../utils/exception.js":40}],18:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _amfParser = _dereq_('./amf-parser.js'); + +var _amfParser2 = _interopRequireDefault(_amfParser); + +var _spsParser = _dereq_('./sps-parser.js'); + +var _spsParser2 = _interopRequireDefault(_spsParser); + +var _demuxErrors = _dereq_('./demux-errors.js'); + +var _demuxErrors2 = _interopRequireDefault(_demuxErrors); + +var _mediaInfo = _dereq_('../core/media-info.js'); + +var _mediaInfo2 = _interopRequireDefault(_mediaInfo); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function Swap16(src) { + return src >>> 8 & 0xFF | (src & 0xFF) << 8; +} + +function Swap32(src) { + return (src & 0xFF000000) >>> 24 | (src & 0x00FF0000) >>> 8 | (src & 0x0000FF00) << 8 | (src & 0x000000FF) << 24; +} + +function ReadBig32(array, index) { + return array[index] << 24 | array[index + 1] << 16 | array[index + 2] << 8 | array[index + 3]; +} + +var FLVDemuxer = function () { + function FLVDemuxer(probeData, config) { + _classCallCheck(this, FLVDemuxer); + + this.TAG = 'FLVDemuxer'; + + this._config = config; + + this._onError = null; + this._onMediaInfo = null; + this._onMetaDataArrived = null; + this._onScriptDataArrived = null; + this._onTrackMetadata = null; + this._onDataAvailable = null; + + this._dataOffset = probeData.dataOffset; + this._firstParse = true; + this._dispatch = false; + + this._hasAudio = probeData.hasAudioTrack; + this._hasVideo = probeData.hasVideoTrack; + + this._hasAudioFlagOverrided = false; + this._hasVideoFlagOverrided = false; + + this._audioInitialMetadataDispatched = false; + this._videoInitialMetadataDispatched = false; + + this._mediaInfo = new _mediaInfo2.default(); + this._mediaInfo.hasAudio = this._hasAudio; + this._mediaInfo.hasVideo = this._hasVideo; + this._metadata = null; + this._audioMetadata = null; + this._videoMetadata = null; + + this._naluLengthSize = 4; + this._timestampBase = 0; // int32, in milliseconds + this._timescale = 1000; + this._duration = 0; // int32, in milliseconds + this._durationOverrided = false; + this._referenceFrameRate = { + fixed: true, + fps: 23.976, + fps_num: 23976, + fps_den: 1000 + }; + + this._flvSoundRateTable = [5500, 11025, 22050, 44100, 48000]; + + this._mpegSamplingRates = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; + + this._mpegAudioV10SampleRateTable = [44100, 48000, 32000, 0]; + this._mpegAudioV20SampleRateTable = [22050, 24000, 16000, 0]; + this._mpegAudioV25SampleRateTable = [11025, 12000, 8000, 0]; + + this._mpegAudioL1BitRateTable = [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1]; + this._mpegAudioL2BitRateTable = [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1]; + this._mpegAudioL3BitRateTable = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1]; + + this._videoTrack = { type: 'video', id: 1, sequenceNumber: 0, samples: [], length: 0 }; + this._audioTrack = { type: 'audio', id: 2, sequenceNumber: 0, samples: [], length: 0 }; + + this._littleEndian = function () { + var buf = new ArrayBuffer(2); + new DataView(buf).setInt16(0, 256, true); // little-endian write + return new Int16Array(buf)[0] === 256; // platform-spec read, if equal then LE + }(); + } + + _createClass(FLVDemuxer, [{ + key: 'destroy', + value: function destroy() { + this._mediaInfo = null; + this._metadata = null; + this._audioMetadata = null; + this._videoMetadata = null; + this._videoTrack = null; + this._audioTrack = null; + + this._onError = null; + this._onMediaInfo = null; + this._onMetaDataArrived = null; + this._onScriptDataArrived = null; + this._onTrackMetadata = null; + this._onDataAvailable = null; + } + }, { + key: 'bindDataSource', + value: function bindDataSource(loader) { + loader.onDataArrival = this.parseChunks.bind(this); + return this; + } + + // prototype: function(type: string, metadata: any): void + + }, { + key: 'resetMediaInfo', + value: function resetMediaInfo() { + this._mediaInfo = new _mediaInfo2.default(); + } + }, { + key: '_isInitialMetadataDispatched', + value: function _isInitialMetadataDispatched() { + if (this._hasAudio && this._hasVideo) { + // both audio & video + return this._audioInitialMetadataDispatched && this._videoInitialMetadataDispatched; + } + if (this._hasAudio && !this._hasVideo) { + // audio only + return this._audioInitialMetadataDispatched; + } + if (!this._hasAudio && this._hasVideo) { + // video only + return this._videoInitialMetadataDispatched; + } + return false; + } + + // function parseChunks(chunk: ArrayBuffer, byteStart: number): number; + + }, { + key: 'parseChunks', + value: function parseChunks(chunk, byteStart) { + if (!this._onError || !this._onMediaInfo || !this._onTrackMetadata || !this._onDataAvailable) { + throw new _exception.IllegalStateException('Flv: onError & onMediaInfo & onTrackMetadata & onDataAvailable callback must be specified'); + } + + var offset = 0; + var le = this._littleEndian; + + if (byteStart === 0) { + // buffer with FLV header + if (chunk.byteLength > 13) { + var probeData = FLVDemuxer.probe(chunk); + offset = probeData.dataOffset; + } else { + return 0; + } + } + + if (this._firstParse) { + // handle PreviousTagSize0 before Tag1 + this._firstParse = false; + if (byteStart + offset !== this._dataOffset) { + _logger2.default.w(this.TAG, 'First time parsing but chunk byteStart invalid!'); + } + + var v = new DataView(chunk, offset); + var prevTagSize0 = v.getUint32(0, !le); + if (prevTagSize0 !== 0) { + _logger2.default.w(this.TAG, 'PrevTagSize0 !== 0 !!!'); + } + offset += 4; + } + + while (offset < chunk.byteLength) { + this._dispatch = true; + + var _v = new DataView(chunk, offset); + + if (offset + 11 + 4 > chunk.byteLength) { + // data not enough for parsing an flv tag + break; + } + + var tagType = _v.getUint8(0); + var dataSize = _v.getUint32(0, !le) & 0x00FFFFFF; + + if (offset + 11 + dataSize + 4 > chunk.byteLength) { + // data not enough for parsing actual data body + break; + } + + if (tagType !== 8 && tagType !== 9 && tagType !== 18) { + _logger2.default.w(this.TAG, 'Unsupported tag type ' + tagType + ', skipped'); + // consume the whole tag (skip it) + offset += 11 + dataSize + 4; + continue; + } + + var ts2 = _v.getUint8(4); + var ts1 = _v.getUint8(5); + var ts0 = _v.getUint8(6); + var ts3 = _v.getUint8(7); + + var timestamp = ts0 | ts1 << 8 | ts2 << 16 | ts3 << 24; + + var streamId = _v.getUint32(7, !le) & 0x00FFFFFF; + if (streamId !== 0) { + _logger2.default.w(this.TAG, 'Meet tag which has StreamID != 0!'); + } + + var dataOffset = offset + 11; + + switch (tagType) { + case 8: + // Audio + this._parseAudioData(chunk, dataOffset, dataSize, timestamp); + break; + case 9: + // Video + this._parseVideoData(chunk, dataOffset, dataSize, timestamp, byteStart + offset); + break; + case 18: + // ScriptDataObject + this._parseScriptData(chunk, dataOffset, dataSize); + break; + } + + var prevTagSize = _v.getUint32(11 + dataSize, !le); + if (prevTagSize !== 11 + dataSize) { + _logger2.default.w(this.TAG, 'Invalid PrevTagSize ' + prevTagSize); + } + + offset += 11 + dataSize + 4; // tagBody + dataSize + prevTagSize + } + + // dispatch parsed frames to consumer (typically, the remuxer) + if (this._isInitialMetadataDispatched()) { + if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) { + this._onDataAvailable(this._audioTrack, this._videoTrack); + } + } + + return offset; // consumed bytes, just equals latest offset index + } + }, { + key: '_parseScriptData', + value: function _parseScriptData(arrayBuffer, dataOffset, dataSize) { + var scriptData = _amfParser2.default.parseScriptData(arrayBuffer, dataOffset, dataSize); + + if (scriptData.hasOwnProperty('onMetaData')) { + if (scriptData.onMetaData == null || _typeof(scriptData.onMetaData) !== 'object') { + _logger2.default.w(this.TAG, 'Invalid onMetaData structure!'); + return; + } + if (this._metadata) { + _logger2.default.w(this.TAG, 'Found another onMetaData tag!'); + } + this._metadata = scriptData; + var onMetaData = this._metadata.onMetaData; + + if (this._onMetaDataArrived) { + this._onMetaDataArrived(Object.assign({}, onMetaData)); + } + + if (typeof onMetaData.hasAudio === 'boolean') { + // hasAudio + if (this._hasAudioFlagOverrided === false) { + this._hasAudio = onMetaData.hasAudio; + this._mediaInfo.hasAudio = this._hasAudio; + } + } + if (typeof onMetaData.hasVideo === 'boolean') { + // hasVideo + if (this._hasVideoFlagOverrided === false) { + this._hasVideo = onMetaData.hasVideo; + this._mediaInfo.hasVideo = this._hasVideo; + } + } + if (typeof onMetaData.audiodatarate === 'number') { + // audiodatarate + this._mediaInfo.audioDataRate = onMetaData.audiodatarate; + } + if (typeof onMetaData.videodatarate === 'number') { + // videodatarate + this._mediaInfo.videoDataRate = onMetaData.videodatarate; + } + if (typeof onMetaData.width === 'number') { + // width + this._mediaInfo.width = onMetaData.width; + } + if (typeof onMetaData.height === 'number') { + // height + this._mediaInfo.height = onMetaData.height; + } + if (typeof onMetaData.duration === 'number') { + // duration + if (!this._durationOverrided) { + var duration = Math.floor(onMetaData.duration * this._timescale); + this._duration = duration; + this._mediaInfo.duration = duration; + } + } else { + this._mediaInfo.duration = 0; + } + if (typeof onMetaData.framerate === 'number') { + // framerate + var fps_num = Math.floor(onMetaData.framerate * 1000); + if (fps_num > 0) { + var fps = fps_num / 1000; + this._referenceFrameRate.fixed = true; + this._referenceFrameRate.fps = fps; + this._referenceFrameRate.fps_num = fps_num; + this._referenceFrameRate.fps_den = 1000; + this._mediaInfo.fps = fps; + } + } + if (_typeof(onMetaData.keyframes) === 'object') { + // keyframes + this._mediaInfo.hasKeyframesIndex = true; + var keyframes = onMetaData.keyframes; + this._mediaInfo.keyframesIndex = this._parseKeyframesIndex(keyframes); + onMetaData.keyframes = null; // keyframes has been extracted, remove it + } else { + this._mediaInfo.hasKeyframesIndex = false; + } + this._dispatch = false; + this._mediaInfo.metadata = onMetaData; + _logger2.default.v(this.TAG, 'Parsed onMetaData'); + if (this._mediaInfo.isComplete()) { + this._onMediaInfo(this._mediaInfo); + } + } + + if (Object.keys(scriptData).length > 0) { + if (this._onScriptDataArrived) { + this._onScriptDataArrived(Object.assign({}, scriptData)); + } + } + } + }, { + key: '_parseKeyframesIndex', + value: function _parseKeyframesIndex(keyframes) { + var times = []; + var filepositions = []; + + // ignore first keyframe which is actually AVC Sequence Header (AVCDecoderConfigurationRecord) + for (var i = 1; i < keyframes.times.length; i++) { + var time = this._timestampBase + Math.floor(keyframes.times[i] * 1000); + times.push(time); + filepositions.push(keyframes.filepositions[i]); + } + + return { + times: times, + filepositions: filepositions + }; + } + }, { + key: '_parseAudioData', + value: function _parseAudioData(arrayBuffer, dataOffset, dataSize, tagTimestamp) { + if (dataSize <= 1) { + _logger2.default.w(this.TAG, 'Flv: Invalid audio packet, missing SoundData payload!'); + return; + } + + if (this._hasAudioFlagOverrided === true && this._hasAudio === false) { + // If hasAudio: false indicated explicitly in MediaDataSource, + // Ignore all the audio packets + return; + } + + var le = this._littleEndian; + var v = new DataView(arrayBuffer, dataOffset, dataSize); + + var soundSpec = v.getUint8(0); + + var soundFormat = soundSpec >>> 4; + if (soundFormat !== 2 && soundFormat !== 10) { + // MP3 or AAC + this._onError(_demuxErrors2.default.CODEC_UNSUPPORTED, 'Flv: Unsupported audio codec idx: ' + soundFormat); + return; + } + + var soundRate = 0; + var soundRateIndex = (soundSpec & 12) >>> 2; + if (soundRateIndex >= 0 && soundRateIndex <= 4) { + soundRate = this._flvSoundRateTable[soundRateIndex]; + } else { + this._onError(_demuxErrors2.default.FORMAT_ERROR, 'Flv: Invalid audio sample rate idx: ' + soundRateIndex); + return; + } + + var soundSize = (soundSpec & 2) >>> 1; // unused + var soundType = soundSpec & 1; + + var meta = this._audioMetadata; + var track = this._audioTrack; + + if (!meta) { + if (this._hasAudio === false && this._hasAudioFlagOverrided === false) { + this._hasAudio = true; + this._mediaInfo.hasAudio = true; + } + + // initial metadata + meta = this._audioMetadata = {}; + meta.type = 'audio'; + meta.id = track.id; + meta.timescale = this._timescale; + meta.duration = this._duration; + meta.audioSampleRate = soundRate; + meta.channelCount = soundType === 0 ? 1 : 2; + } + + if (soundFormat === 10) { + // AAC + var aacData = this._parseAACAudioData(arrayBuffer, dataOffset + 1, dataSize - 1); + if (aacData == undefined) { + return; + } + + if (aacData.packetType === 0) { + // AAC sequence header (AudioSpecificConfig) + if (meta.config) { + _logger2.default.w(this.TAG, 'Found another AudioSpecificConfig!'); + } + var misc = aacData.data; + meta.audioSampleRate = misc.samplingRate; + meta.channelCount = misc.channelCount; + meta.codec = misc.codec; + meta.originalCodec = misc.originalCodec; + meta.config = misc.config; + // The decode result of an aac sample is 1024 PCM samples + meta.refSampleDuration = 1024 / meta.audioSampleRate * meta.timescale; + _logger2.default.v(this.TAG, 'Parsed AudioSpecificConfig'); + + if (this._isInitialMetadataDispatched()) { + // Non-initial metadata, force dispatch (or flush) parsed frames to remuxer + if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) { + this._onDataAvailable(this._audioTrack, this._videoTrack); + } + } else { + this._audioInitialMetadataDispatched = true; + } + // then notify new metadata + this._dispatch = false; + this._onTrackMetadata('audio', meta); + + var mi = this._mediaInfo; + mi.audioCodec = meta.originalCodec; + mi.audioSampleRate = meta.audioSampleRate; + mi.audioChannelCount = meta.channelCount; + if (mi.hasVideo) { + if (mi.videoCodec != null) { + mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"'; + } + } else { + mi.mimeType = 'video/x-flv; codecs="' + mi.audioCodec + '"'; + } + if (mi.isComplete()) { + this._onMediaInfo(mi); + } + } else if (aacData.packetType === 1) { + // AAC raw frame data + var dts = this._timestampBase + tagTimestamp; + var aacSample = { unit: aacData.data, length: aacData.data.byteLength, dts: dts, pts: dts }; + track.samples.push(aacSample); + track.length += aacData.data.length; + } else { + _logger2.default.e(this.TAG, 'Flv: Unsupported AAC data type ' + aacData.packetType); + } + } else if (soundFormat === 2) { + // MP3 + if (!meta.codec) { + // We need metadata for mp3 audio track, extract info from frame header + var _misc = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, true); + if (_misc == undefined) { + return; + } + meta.audioSampleRate = _misc.samplingRate; + meta.channelCount = _misc.channelCount; + meta.codec = _misc.codec; + meta.originalCodec = _misc.originalCodec; + // The decode result of an mp3 sample is 1152 PCM samples + meta.refSampleDuration = 1152 / meta.audioSampleRate * meta.timescale; + _logger2.default.v(this.TAG, 'Parsed MPEG Audio Frame Header'); + + this._audioInitialMetadataDispatched = true; + this._onTrackMetadata('audio', meta); + + var _mi = this._mediaInfo; + _mi.audioCodec = meta.codec; + _mi.audioSampleRate = meta.audioSampleRate; + _mi.audioChannelCount = meta.channelCount; + _mi.audioDataRate = _misc.bitRate; + if (_mi.hasVideo) { + if (_mi.videoCodec != null) { + _mi.mimeType = 'video/x-flv; codecs="' + _mi.videoCodec + ',' + _mi.audioCodec + '"'; + } + } else { + _mi.mimeType = 'video/x-flv; codecs="' + _mi.audioCodec + '"'; + } + if (_mi.isComplete()) { + this._onMediaInfo(_mi); + } + } + + // This packet is always a valid audio packet, extract it + var data = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, false); + if (data == undefined) { + return; + } + var _dts = this._timestampBase + tagTimestamp; + var mp3Sample = { unit: data, length: data.byteLength, dts: _dts, pts: _dts }; + track.samples.push(mp3Sample); + track.length += data.length; + } + } + }, { + key: '_parseAACAudioData', + value: function _parseAACAudioData(arrayBuffer, dataOffset, dataSize) { + if (dataSize <= 1) { + _logger2.default.w(this.TAG, 'Flv: Invalid AAC packet, missing AACPacketType or/and Data!'); + return; + } + + var result = {}; + var array = new Uint8Array(arrayBuffer, dataOffset, dataSize); + + result.packetType = array[0]; + + if (array[0] === 0) { + result.data = this._parseAACAudioSpecificConfig(arrayBuffer, dataOffset + 1, dataSize - 1); + } else { + result.data = array.subarray(1); + } + + return result; + } + }, { + key: '_parseAACAudioSpecificConfig', + value: function _parseAACAudioSpecificConfig(arrayBuffer, dataOffset, dataSize) { + var array = new Uint8Array(arrayBuffer, dataOffset, dataSize); + var config = null; + + /* Audio Object Type: + 0: Null + 1: AAC Main + 2: AAC LC + 3: AAC SSR (Scalable Sample Rate) + 4: AAC LTP (Long Term Prediction) + 5: HE-AAC / SBR (Spectral Band Replication) + 6: AAC Scalable + */ + + var audioObjectType = 0; + var originalAudioObjectType = 0; + var audioExtensionObjectType = null; + var samplingIndex = 0; + var extensionSamplingIndex = null; + + // 5 bits + audioObjectType = originalAudioObjectType = array[0] >>> 3; + // 4 bits + samplingIndex = (array[0] & 0x07) << 1 | array[1] >>> 7; + if (samplingIndex < 0 || samplingIndex >= this._mpegSamplingRates.length) { + this._onError(_demuxErrors2.default.FORMAT_ERROR, 'Flv: AAC invalid sampling frequency index!'); + return; + } + + var samplingFrequence = this._mpegSamplingRates[samplingIndex]; + + // 4 bits + var channelConfig = (array[1] & 0x78) >>> 3; + if (channelConfig < 0 || channelConfig >= 8) { + this._onError(_demuxErrors2.default.FORMAT_ERROR, 'Flv: AAC invalid channel configuration'); + return; + } + + if (audioObjectType === 5) { + // HE-AAC? + // 4 bits + extensionSamplingIndex = (array[1] & 0x07) << 1 | array[2] >>> 7; + // 5 bits + audioExtensionObjectType = (array[2] & 0x7C) >>> 2; + } + + // workarounds for various browsers + var userAgent = self.navigator.userAgent.toLowerCase(); + + if (userAgent.indexOf('firefox') !== -1) { + // firefox: use SBR (HE-AAC) if freq less than 24kHz + if (samplingIndex >= 6) { + audioObjectType = 5; + config = new Array(4); + extensionSamplingIndex = samplingIndex - 3; + } else { + // use LC-AAC + audioObjectType = 2; + config = new Array(2); + extensionSamplingIndex = samplingIndex; + } + } else if (userAgent.indexOf('android') !== -1) { + // android: always use LC-AAC + audioObjectType = 2; + config = new Array(2); + extensionSamplingIndex = samplingIndex; + } else { + // for other browsers, e.g. chrome... + // Always use HE-AAC to make it easier to switch aac codec profile + audioObjectType = 5; + extensionSamplingIndex = samplingIndex; + config = new Array(4); + + if (samplingIndex >= 6) { + extensionSamplingIndex = samplingIndex - 3; + } else if (channelConfig === 1) { + // Mono channel + audioObjectType = 2; + config = new Array(2); + extensionSamplingIndex = samplingIndex; + } + } + + config[0] = audioObjectType << 3; + config[0] |= (samplingIndex & 0x0F) >>> 1; + config[1] = (samplingIndex & 0x0F) << 7; + config[1] |= (channelConfig & 0x0F) << 3; + if (audioObjectType === 5) { + config[1] |= (extensionSamplingIndex & 0x0F) >>> 1; + config[2] = (extensionSamplingIndex & 0x01) << 7; + // extended audio object type: force to 2 (LC-AAC) + config[2] |= 2 << 2; + config[3] = 0; + } + + return { + config: config, + samplingRate: samplingFrequence, + channelCount: channelConfig, + codec: 'mp4a.40.' + audioObjectType, + originalCodec: 'mp4a.40.' + originalAudioObjectType + }; + } + }, { + key: '_parseMP3AudioData', + value: function _parseMP3AudioData(arrayBuffer, dataOffset, dataSize, requestHeader) { + if (dataSize < 4) { + _logger2.default.w(this.TAG, 'Flv: Invalid MP3 packet, header missing!'); + return; + } + + var le = this._littleEndian; + var array = new Uint8Array(arrayBuffer, dataOffset, dataSize); + var result = null; + + if (requestHeader) { + if (array[0] !== 0xFF) { + return; + } + var ver = array[1] >>> 3 & 0x03; + var layer = (array[1] & 0x06) >> 1; + + var bitrate_index = (array[2] & 0xF0) >>> 4; + var sampling_freq_index = (array[2] & 0x0C) >>> 2; + + var channel_mode = array[3] >>> 6 & 0x03; + var channel_count = channel_mode !== 3 ? 2 : 1; + + var sample_rate = 0; + var bit_rate = 0; + var object_type = 34; // Layer-3, listed in MPEG-4 Audio Object Types + + var codec = 'mp3'; + + switch (ver) { + case 0: + // MPEG 2.5 + sample_rate = this._mpegAudioV25SampleRateTable[sampling_freq_index]; + break; + case 2: + // MPEG 2 + sample_rate = this._mpegAudioV20SampleRateTable[sampling_freq_index]; + break; + case 3: + // MPEG 1 + sample_rate = this._mpegAudioV10SampleRateTable[sampling_freq_index]; + break; + } + + switch (layer) { + case 1: + // Layer 3 + object_type = 34; + if (bitrate_index < this._mpegAudioL3BitRateTable.length) { + bit_rate = this._mpegAudioL3BitRateTable[bitrate_index]; + } + break; + case 2: + // Layer 2 + object_type = 33; + if (bitrate_index < this._mpegAudioL2BitRateTable.length) { + bit_rate = this._mpegAudioL2BitRateTable[bitrate_index]; + } + break; + case 3: + // Layer 1 + object_type = 32; + if (bitrate_index < this._mpegAudioL1BitRateTable.length) { + bit_rate = this._mpegAudioL1BitRateTable[bitrate_index]; + } + break; + } + + result = { + bitRate: bit_rate, + samplingRate: sample_rate, + channelCount: channel_count, + codec: codec, + originalCodec: codec + }; + } else { + result = array; + } + + return result; + } + }, { + key: '_parseVideoData', + value: function _parseVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition) { + if (dataSize <= 1) { + _logger2.default.w(this.TAG, 'Flv: Invalid video packet, missing VideoData payload!'); + return; + } + + if (this._hasVideoFlagOverrided === true && this._hasVideo === false) { + // If hasVideo: false indicated explicitly in MediaDataSource, + // Ignore all the video packets + return; + } + + var spec = new Uint8Array(arrayBuffer, dataOffset, dataSize)[0]; + + var frameType = (spec & 240) >>> 4; + var codecId = spec & 15; + + if (codecId !== 7) { + this._onError(_demuxErrors2.default.CODEC_UNSUPPORTED, 'Flv: Unsupported codec in video frame: ' + codecId); + return; + } + + this._parseAVCVideoPacket(arrayBuffer, dataOffset + 1, dataSize - 1, tagTimestamp, tagPosition, frameType); + } + }, { + key: '_parseAVCVideoPacket', + value: function _parseAVCVideoPacket(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition, frameType) { + if (dataSize < 4) { + _logger2.default.w(this.TAG, 'Flv: Invalid AVC packet, missing AVCPacketType or/and CompositionTime'); + return; + } + + var le = this._littleEndian; + var v = new DataView(arrayBuffer, dataOffset, dataSize); + + var packetType = v.getUint8(0); + var cts_unsigned = v.getUint32(0, !le) & 0x00FFFFFF; + var cts = cts_unsigned << 8 >> 8; // convert to 24-bit signed int + + if (packetType === 0) { + // AVCDecoderConfigurationRecord + this._parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset + 4, dataSize - 4); + } else if (packetType === 1) { + // One or more Nalus + this._parseAVCVideoData(arrayBuffer, dataOffset + 4, dataSize - 4, tagTimestamp, tagPosition, frameType, cts); + } else if (packetType === 2) { + // empty, AVC end of sequence + } else { + this._onError(_demuxErrors2.default.FORMAT_ERROR, 'Flv: Invalid video packet type ' + packetType); + return; + } + } + }, { + key: '_parseAVCDecoderConfigurationRecord', + value: function _parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset, dataSize) { + if (dataSize < 7) { + _logger2.default.w(this.TAG, 'Flv: Invalid AVCDecoderConfigurationRecord, lack of data!'); + return; + } + + var meta = this._videoMetadata; + var track = this._videoTrack; + var le = this._littleEndian; + var v = new DataView(arrayBuffer, dataOffset, dataSize); + + if (!meta) { + if (this._hasVideo === false && this._hasVideoFlagOverrided === false) { + this._hasVideo = true; + this._mediaInfo.hasVideo = true; + } + + meta = this._videoMetadata = {}; + meta.type = 'video'; + meta.id = track.id; + meta.timescale = this._timescale; + meta.duration = this._duration; + } else { + if (typeof meta.avcc !== 'undefined') { + _logger2.default.w(this.TAG, 'Found another AVCDecoderConfigurationRecord!'); + } + } + + var version = v.getUint8(0); // configurationVersion + var avcProfile = v.getUint8(1); // avcProfileIndication + var profileCompatibility = v.getUint8(2); // profile_compatibility + var avcLevel = v.getUint8(3); // AVCLevelIndication + + if (version !== 1 || avcProfile === 0) { + this._onError(_demuxErrors2.default.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord'); + return; + } + + this._naluLengthSize = (v.getUint8(4) & 3) + 1; // lengthSizeMinusOne + if (this._naluLengthSize !== 3 && this._naluLengthSize !== 4) { + // holy shit!!! + this._onError(_demuxErrors2.default.FORMAT_ERROR, 'Flv: Strange NaluLengthSizeMinusOne: ' + (this._naluLengthSize - 1)); + return; + } + + var spsCount = v.getUint8(5) & 31; // numOfSequenceParameterSets + if (spsCount === 0) { + this._onError(_demuxErrors2.default.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No SPS'); + return; + } else if (spsCount > 1) { + _logger2.default.w(this.TAG, 'Flv: Strange AVCDecoderConfigurationRecord: SPS Count = ' + spsCount); + } + + var offset = 6; + + for (var i = 0; i < spsCount; i++) { + var len = v.getUint16(offset, !le); // sequenceParameterSetLength + offset += 2; + + if (len === 0) { + continue; + } + + // Notice: Nalu without startcode header (00 00 00 01) + var sps = new Uint8Array(arrayBuffer, dataOffset + offset, len); + offset += len; + + var config = _spsParser2.default.parseSPS(sps); + if (i !== 0) { + // ignore other sps's config + continue; + } + + meta.codecWidth = config.codec_size.width; + meta.codecHeight = config.codec_size.height; + meta.presentWidth = config.present_size.width; + meta.presentHeight = config.present_size.height; + + meta.profile = config.profile_string; + meta.level = config.level_string; + meta.bitDepth = config.bit_depth; + meta.chromaFormat = config.chroma_format; + meta.sarRatio = config.sar_ratio; + meta.frameRate = config.frame_rate; + + if (config.frame_rate.fixed === false || config.frame_rate.fps_num === 0 || config.frame_rate.fps_den === 0) { + meta.frameRate = this._referenceFrameRate; + } + + var fps_den = meta.frameRate.fps_den; + var fps_num = meta.frameRate.fps_num; + meta.refSampleDuration = meta.timescale * (fps_den / fps_num); + + var codecArray = sps.subarray(1, 4); + var codecString = 'avc1.'; + for (var j = 0; j < 3; j++) { + var h = codecArray[j].toString(16); + if (h.length < 2) { + h = '0' + h; + } + codecString += h; + } + meta.codec = codecString; + + var mi = this._mediaInfo; + mi.width = meta.codecWidth; + mi.height = meta.codecHeight; + mi.fps = meta.frameRate.fps; + mi.profile = meta.profile; + mi.level = meta.level; + mi.refFrames = config.ref_frames; + mi.chromaFormat = config.chroma_format_string; + mi.sarNum = meta.sarRatio.width; + mi.sarDen = meta.sarRatio.height; + mi.videoCodec = codecString; + + if (mi.hasAudio) { + if (mi.audioCodec != null) { + mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"'; + } + } else { + mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + '"'; + } + if (mi.isComplete()) { + this._onMediaInfo(mi); + } + } + + var ppsCount = v.getUint8(offset); // numOfPictureParameterSets + if (ppsCount === 0) { + this._onError(_demuxErrors2.default.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No PPS'); + return; + } else if (ppsCount > 1) { + _logger2.default.w(this.TAG, 'Flv: Strange AVCDecoderConfigurationRecord: PPS Count = ' + ppsCount); + } + + offset++; + + for (var _i = 0; _i < ppsCount; _i++) { + var _len = v.getUint16(offset, !le); // pictureParameterSetLength + offset += 2; + + if (_len === 0) { + continue; + } + + // pps is useless for extracting video information + offset += _len; + } + + meta.avcc = new Uint8Array(dataSize); + meta.avcc.set(new Uint8Array(arrayBuffer, dataOffset, dataSize), 0); + _logger2.default.v(this.TAG, 'Parsed AVCDecoderConfigurationRecord'); + + if (this._isInitialMetadataDispatched()) { + // flush parsed frames + if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) { + this._onDataAvailable(this._audioTrack, this._videoTrack); + } + } else { + this._videoInitialMetadataDispatched = true; + } + // notify new metadata + this._dispatch = false; + this._onTrackMetadata('video', meta); + } + }, { + key: '_parseAVCVideoData', + value: function _parseAVCVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition, frameType, cts) { + var le = this._littleEndian; + var v = new DataView(arrayBuffer, dataOffset, dataSize); + + var units = [], + length = 0; + + var offset = 0; + var lengthSize = this._naluLengthSize; + var dts = this._timestampBase + tagTimestamp; + var keyframe = frameType === 1; // from FLV Frame Type constants + + while (offset < dataSize) { + if (offset + 4 >= dataSize) { + _logger2.default.w(this.TAG, 'Malformed Nalu near timestamp ' + dts + ', offset = ' + offset + ', dataSize = ' + dataSize); + break; // data not enough for next Nalu + } + // Nalu with length-header (AVC1) + var naluSize = v.getUint32(offset, !le); // Big-Endian read + if (lengthSize === 3) { + naluSize >>>= 8; + } + if (naluSize > dataSize - lengthSize) { + _logger2.default.w(this.TAG, 'Malformed Nalus near timestamp ' + dts + ', NaluSize > DataSize!'); + return; + } + + var unitType = v.getUint8(offset + lengthSize) & 0x1F; + + if (unitType === 5) { + // IDR + keyframe = true; + } + + var data = new Uint8Array(arrayBuffer, dataOffset + offset, lengthSize + naluSize); + var unit = { type: unitType, data: data }; + units.push(unit); + length += data.byteLength; + + offset += lengthSize + naluSize; + } + + if (units.length) { + var track = this._videoTrack; + var avcSample = { + units: units, + length: length, + isKeyframe: keyframe, + dts: dts, + cts: cts, + pts: dts + cts + }; + if (keyframe) { + avcSample.fileposition = tagPosition; + } + track.samples.push(avcSample); + track.length += length; + } + } + }, { + key: 'onTrackMetadata', + get: function get() { + return this._onTrackMetadata; + }, + set: function set(callback) { + this._onTrackMetadata = callback; + } + + // prototype: function(mediaInfo: MediaInfo): void + + }, { + key: 'onMediaInfo', + get: function get() { + return this._onMediaInfo; + }, + set: function set(callback) { + this._onMediaInfo = callback; + } + }, { + key: 'onMetaDataArrived', + get: function get() { + return this._onMetaDataArrived; + }, + set: function set(callback) { + this._onMetaDataArrived = callback; + } + }, { + key: 'onScriptDataArrived', + get: function get() { + return this._onScriptDataArrived; + }, + set: function set(callback) { + this._onScriptDataArrived = callback; + } + + // prototype: function(type: number, info: string): void + + }, { + key: 'onError', + get: function get() { + return this._onError; + }, + set: function set(callback) { + this._onError = callback; + } + + // prototype: function(videoTrack: any, audioTrack: any): void + + }, { + key: 'onDataAvailable', + get: function get() { + return this._onDataAvailable; + }, + set: function set(callback) { + this._onDataAvailable = callback; + } + + // timestamp base for output samples, must be in milliseconds + + }, { + key: 'timestampBase', + get: function get() { + return this._timestampBase; + }, + set: function set(base) { + this._timestampBase = base; + } + }, { + key: 'overridedDuration', + get: function get() { + return this._duration; + } + + // Force-override media duration. Must be in milliseconds, int32 + , + set: function set(duration) { + this._durationOverrided = true; + this._duration = duration; + this._mediaInfo.duration = duration; + } + + // Force-override audio track present flag, boolean + + }, { + key: 'overridedHasAudio', + set: function set(hasAudio) { + this._hasAudioFlagOverrided = true; + this._hasAudio = hasAudio; + this._mediaInfo.hasAudio = hasAudio; + } + + // Force-override video track present flag, boolean + + }, { + key: 'overridedHasVideo', + set: function set(hasVideo) { + this._hasVideoFlagOverrided = true; + this._hasVideo = hasVideo; + this._mediaInfo.hasVideo = hasVideo; + } + }], [{ + key: 'probe', + value: function probe(buffer) { + var data = new Uint8Array(buffer); + var mismatch = { match: false }; + + if (data[0] !== 0x46 || data[1] !== 0x4C || data[2] !== 0x56 || data[3] !== 0x01) { + return mismatch; + } + + var hasAudio = (data[4] & 4) >>> 2 !== 0; + var hasVideo = (data[4] & 1) !== 0; + + var offset = ReadBig32(data, 5); + + if (offset < 9) { + return mismatch; + } + + return { + match: true, + consumed: offset, + dataOffset: offset, + hasAudioTrack: hasAudio, + hasVideoTrack: hasVideo + }; + } + }]); + + return FLVDemuxer; +}(); + +exports.default = FLVDemuxer; + +},{"../core/media-info.js":7,"../utils/exception.js":40,"../utils/logger.js":41,"./amf-parser.js":15,"./demux-errors.js":16,"./sps-parser.js":19}],19:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _expGolomb = _dereq_('./exp-golomb.js'); + +var _expGolomb2 = _interopRequireDefault(_expGolomb); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var SPSParser = function () { + function SPSParser() { + _classCallCheck(this, SPSParser); + } + + _createClass(SPSParser, null, [{ + key: '_ebsp2rbsp', + value: function _ebsp2rbsp(uint8array) { + var src = uint8array; + var src_length = src.byteLength; + var dst = new Uint8Array(src_length); + var dst_idx = 0; + + for (var i = 0; i < src_length; i++) { + if (i >= 2) { + // Unescape: Skip 0x03 after 00 00 + if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) { + continue; + } + } + dst[dst_idx] = src[i]; + dst_idx++; + } + + return new Uint8Array(dst.buffer, 0, dst_idx); + } + }, { + key: 'parseSPS', + value: function parseSPS(uint8array) { + var rbsp = SPSParser._ebsp2rbsp(uint8array); + var gb = new _expGolomb2.default(rbsp); + + gb.readByte(); + var profile_idc = gb.readByte(); // profile_idc + gb.readByte(); // constraint_set_flags[5] + reserved_zero[3] + var level_idc = gb.readByte(); // level_idc + gb.readUEG(); // seq_parameter_set_id + + var profile_string = SPSParser.getProfileString(profile_idc); + var level_string = SPSParser.getLevelString(level_idc); + var chroma_format_idc = 1; + var chroma_format = 420; + var chroma_format_table = [0, 420, 422, 444]; + var bit_depth = 8; + + if (profile_idc === 100 || profile_idc === 110 || profile_idc === 122 || profile_idc === 244 || profile_idc === 44 || profile_idc === 83 || profile_idc === 86 || profile_idc === 118 || profile_idc === 128 || profile_idc === 138 || profile_idc === 144) { + + chroma_format_idc = gb.readUEG(); + if (chroma_format_idc === 3) { + gb.readBits(1); // separate_colour_plane_flag + } + if (chroma_format_idc <= 3) { + chroma_format = chroma_format_table[chroma_format_idc]; + } + + bit_depth = gb.readUEG() + 8; // bit_depth_luma_minus8 + gb.readUEG(); // bit_depth_chroma_minus8 + gb.readBits(1); // qpprime_y_zero_transform_bypass_flag + if (gb.readBool()) { + // seq_scaling_matrix_present_flag + var scaling_list_count = chroma_format_idc !== 3 ? 8 : 12; + for (var i = 0; i < scaling_list_count; i++) { + if (gb.readBool()) { + // seq_scaling_list_present_flag + if (i < 6) { + SPSParser._skipScalingList(gb, 16); + } else { + SPSParser._skipScalingList(gb, 64); + } + } + } + } + } + gb.readUEG(); // log2_max_frame_num_minus4 + var pic_order_cnt_type = gb.readUEG(); + if (pic_order_cnt_type === 0) { + gb.readUEG(); // log2_max_pic_order_cnt_lsb_minus_4 + } else if (pic_order_cnt_type === 1) { + gb.readBits(1); // delta_pic_order_always_zero_flag + gb.readSEG(); // offset_for_non_ref_pic + gb.readSEG(); // offset_for_top_to_bottom_field + var num_ref_frames_in_pic_order_cnt_cycle = gb.readUEG(); + for (var _i = 0; _i < num_ref_frames_in_pic_order_cnt_cycle; _i++) { + gb.readSEG(); // offset_for_ref_frame + } + } + var ref_frames = gb.readUEG(); // max_num_ref_frames + gb.readBits(1); // gaps_in_frame_num_value_allowed_flag + + var pic_width_in_mbs_minus1 = gb.readUEG(); + var pic_height_in_map_units_minus1 = gb.readUEG(); + + var frame_mbs_only_flag = gb.readBits(1); + if (frame_mbs_only_flag === 0) { + gb.readBits(1); // mb_adaptive_frame_field_flag + } + gb.readBits(1); // direct_8x8_inference_flag + + var frame_crop_left_offset = 0; + var frame_crop_right_offset = 0; + var frame_crop_top_offset = 0; + var frame_crop_bottom_offset = 0; + + var frame_cropping_flag = gb.readBool(); + if (frame_cropping_flag) { + frame_crop_left_offset = gb.readUEG(); + frame_crop_right_offset = gb.readUEG(); + frame_crop_top_offset = gb.readUEG(); + frame_crop_bottom_offset = gb.readUEG(); + } + + var sar_width = 1, + sar_height = 1; + var fps = 0, + fps_fixed = true, + fps_num = 0, + fps_den = 0; + + var vui_parameters_present_flag = gb.readBool(); + if (vui_parameters_present_flag) { + if (gb.readBool()) { + // aspect_ratio_info_present_flag + var aspect_ratio_idc = gb.readByte(); + var sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2]; + var sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1]; + + if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) { + sar_width = sar_w_table[aspect_ratio_idc - 1]; + sar_height = sar_h_table[aspect_ratio_idc - 1]; + } else if (aspect_ratio_idc === 255) { + sar_width = gb.readByte() << 8 | gb.readByte(); + sar_height = gb.readByte() << 8 | gb.readByte(); + } + } + + if (gb.readBool()) { + // overscan_info_present_flag + gb.readBool(); // overscan_appropriate_flag + } + if (gb.readBool()) { + // video_signal_type_present_flag + gb.readBits(4); // video_format & video_full_range_flag + if (gb.readBool()) { + // colour_description_present_flag + gb.readBits(24); // colour_primaries & transfer_characteristics & matrix_coefficients + } + } + if (gb.readBool()) { + // chroma_loc_info_present_flag + gb.readUEG(); // chroma_sample_loc_type_top_field + gb.readUEG(); // chroma_sample_loc_type_bottom_field + } + if (gb.readBool()) { + // timing_info_present_flag + var num_units_in_tick = gb.readBits(32); + var time_scale = gb.readBits(32); + fps_fixed = gb.readBool(); // fixed_frame_rate_flag + + fps_num = time_scale; + fps_den = num_units_in_tick * 2; + fps = fps_num / fps_den; + } + } + + var sarScale = 1; + if (sar_width !== 1 || sar_height !== 1) { + sarScale = sar_width / sar_height; + } + + var crop_unit_x = 0, + crop_unit_y = 0; + if (chroma_format_idc === 0) { + crop_unit_x = 1; + crop_unit_y = 2 - frame_mbs_only_flag; + } else { + var sub_wc = chroma_format_idc === 3 ? 1 : 2; + var sub_hc = chroma_format_idc === 1 ? 2 : 1; + crop_unit_x = sub_wc; + crop_unit_y = sub_hc * (2 - frame_mbs_only_flag); + } + + var codec_width = (pic_width_in_mbs_minus1 + 1) * 16; + var codec_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 + 1) * 16); + + codec_width -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x; + codec_height -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y; + + var present_width = Math.ceil(codec_width * sarScale); + + gb.destroy(); + gb = null; + + return { + profile_string: profile_string, // baseline, high, high10, ... + level_string: level_string, // 3, 3.1, 4, 4.1, 5, 5.1, ... + bit_depth: bit_depth, // 8bit, 10bit, ... + ref_frames: ref_frames, + chroma_format: chroma_format, // 4:2:0, 4:2:2, ... + chroma_format_string: SPSParser.getChromaFormatString(chroma_format), + + frame_rate: { + fixed: fps_fixed, + fps: fps, + fps_den: fps_den, + fps_num: fps_num + }, + + sar_ratio: { + width: sar_width, + height: sar_height + }, + + codec_size: { + width: codec_width, + height: codec_height + }, + + present_size: { + width: present_width, + height: codec_height + } + }; + } + }, { + key: '_skipScalingList', + value: function _skipScalingList(gb, count) { + var last_scale = 8, + next_scale = 8; + var delta_scale = 0; + for (var i = 0; i < count; i++) { + if (next_scale !== 0) { + delta_scale = gb.readSEG(); + next_scale = (last_scale + delta_scale + 256) % 256; + } + last_scale = next_scale === 0 ? last_scale : next_scale; + } + } + }, { + key: 'getProfileString', + value: function getProfileString(profile_idc) { + switch (profile_idc) { + case 66: + return 'Baseline'; + case 77: + return 'Main'; + case 88: + return 'Extended'; + case 100: + return 'High'; + case 110: + return 'High10'; + case 122: + return 'High422'; + case 244: + return 'High444'; + default: + return 'Unknown'; + } + } + }, { + key: 'getLevelString', + value: function getLevelString(level_idc) { + return (level_idc / 10).toFixed(1); + } + }, { + key: 'getChromaFormatString', + value: function getChromaFormatString(chroma) { + switch (chroma) { + case 420: + return '4:2:0'; + case 422: + return '4:2:2'; + case 444: + return '4:4:4'; + default: + return 'Unknown'; + } + } + }]); + + return SPSParser; +}(); + +exports.default = SPSParser; + +},{"./exp-golomb.js":17}],20:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _polyfill = _dereq_('./utils/polyfill.js'); + +var _polyfill2 = _interopRequireDefault(_polyfill); + +var _features = _dereq_('./core/features.js'); + +var _features2 = _interopRequireDefault(_features); + +var _loader = _dereq_('./io/loader.js'); + +var _flvPlayer = _dereq_('./player/flv-player.js'); + +var _flvPlayer2 = _interopRequireDefault(_flvPlayer); + +var _nativePlayer = _dereq_('./player/native-player.js'); + +var _nativePlayer2 = _interopRequireDefault(_nativePlayer); + +var _playerEvents = _dereq_('./player/player-events.js'); + +var _playerEvents2 = _interopRequireDefault(_playerEvents); + +var _playerErrors = _dereq_('./player/player-errors.js'); + +var _loggingControl = _dereq_('./utils/logging-control.js'); + +var _loggingControl2 = _interopRequireDefault(_loggingControl); + +var _exception = _dereq_('./utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// here are all the interfaces + +// install polyfills +_polyfill2.default.install(); + +// factory method +function createPlayer(mediaDataSource, optionalConfig) { + var mds = mediaDataSource; + if (mds == null || (typeof mds === 'undefined' ? 'undefined' : _typeof(mds)) !== 'object') { + throw new _exception.InvalidArgumentException('MediaDataSource must be an javascript object!'); + } + + if (!mds.hasOwnProperty('type')) { + throw new _exception.InvalidArgumentException('MediaDataSource must has type field to indicate video file type!'); + } + + switch (mds.type) { + case 'flv': + return new _flvPlayer2.default(mds, optionalConfig); + default: + return new _nativePlayer2.default(mds, optionalConfig); + } +} + +// feature detection +function isSupported() { + return _features2.default.supportMSEH264Playback(); +} + +function getFeatureList() { + return _features2.default.getFeatureList(); +} + +// interfaces +var flvjs = {}; + +flvjs.createPlayer = createPlayer; +flvjs.isSupported = isSupported; +flvjs.getFeatureList = getFeatureList; + +flvjs.BaseLoader = _loader.BaseLoader; +flvjs.LoaderStatus = _loader.LoaderStatus; +flvjs.LoaderErrors = _loader.LoaderErrors; + +flvjs.Events = _playerEvents2.default; +flvjs.ErrorTypes = _playerErrors.ErrorTypes; +flvjs.ErrorDetails = _playerErrors.ErrorDetails; + +flvjs.FlvPlayer = _flvPlayer2.default; +flvjs.NativePlayer = _nativePlayer2.default; +flvjs.LoggingControl = _loggingControl2.default; + +Object.defineProperty(flvjs, 'version', { + enumerable: true, + get: function get() { + // replaced by browserify-versionify transform + return '1.5.0'; + } +}); + +exports.default = flvjs; + +},{"./core/features.js":6,"./io/loader.js":24,"./player/flv-player.js":32,"./player/native-player.js":33,"./player/player-errors.js":34,"./player/player-events.js":35,"./utils/exception.js":40,"./utils/logging-control.js":42,"./utils/polyfill.js":43}],21:[function(_dereq_,module,exports){ +'use strict'; + +// entry/index file + +// make it compatible with browserify's umd wrapper +module.exports = _dereq_('./flv.js').default; + +},{"./flv.js":20}],22:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _browser = _dereq_('../utils/browser.js'); + +var _browser2 = _interopRequireDefault(_browser); + +var _loader = _dereq_('./loader.js'); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* fetch + stream IO loader. Currently working on chrome 43+. + * fetch provides a better alternative http API to XMLHttpRequest + * + * fetch spec https://fetch.spec.whatwg.org/ + * stream spec https://streams.spec.whatwg.org/ + */ +var FetchStreamLoader = function (_BaseLoader) { + _inherits(FetchStreamLoader, _BaseLoader); + + _createClass(FetchStreamLoader, null, [{ + key: 'isSupported', + value: function isSupported() { + try { + // fetch + stream is broken on Microsoft Edge. Disable before build 15048. + // see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8196907/ + // Fixed in Jan 10, 2017. Build 15048+ removed from blacklist. + var isWorkWellEdge = _browser2.default.msedge && _browser2.default.version.minor >= 15048; + var browserNotBlacklisted = _browser2.default.msedge ? isWorkWellEdge : true; + return self.fetch && self.ReadableStream && browserNotBlacklisted; + } catch (e) { + return false; + } + } + }]); + + function FetchStreamLoader(seekHandler, config) { + _classCallCheck(this, FetchStreamLoader); + + var _this = _possibleConstructorReturn(this, (FetchStreamLoader.__proto__ || Object.getPrototypeOf(FetchStreamLoader)).call(this, 'fetch-stream-loader')); + + _this.TAG = 'FetchStreamLoader'; + + _this._seekHandler = seekHandler; + _this._config = config; + _this._needStash = true; + + _this._requestAbort = false; + _this._contentLength = null; + _this._receivedLength = 0; + return _this; + } + + _createClass(FetchStreamLoader, [{ + key: 'destroy', + value: function destroy() { + if (this.isWorking()) { + this.abort(); + } + _get(FetchStreamLoader.prototype.__proto__ || Object.getPrototypeOf(FetchStreamLoader.prototype), 'destroy', this).call(this); + } + }, { + key: 'open', + value: function open(dataSource, range) { + var _this2 = this; + + this._dataSource = dataSource; + this._range = range; + + var sourceURL = dataSource.url; + if (this._config.reuseRedirectedURL && dataSource.redirectedURL != undefined) { + sourceURL = dataSource.redirectedURL; + } + + var seekConfig = this._seekHandler.getConfig(sourceURL, range); + + var headers = new self.Headers(); + + if (_typeof(seekConfig.headers) === 'object') { + var configHeaders = seekConfig.headers; + for (var key in configHeaders) { + if (configHeaders.hasOwnProperty(key)) { + headers.append(key, configHeaders[key]); + } + } + } + + var params = { + method: 'GET', + headers: headers, + mode: 'cors', + cache: 'default', + // The default policy of Fetch API in the whatwg standard + // Safari incorrectly indicates 'no-referrer' as default policy, fuck it + referrerPolicy: 'no-referrer-when-downgrade' + }; + + // add additional headers + if (_typeof(this._config.headers) === 'object') { + for (var _key in this._config.headers) { + headers.append(_key, this._config.headers[_key]); + } + } + + // cors is enabled by default + if (dataSource.cors === false) { + // no-cors means 'disregard cors policy', which can only be used in ServiceWorker + params.mode = 'same-origin'; + } + + // withCredentials is disabled by default + if (dataSource.withCredentials) { + params.credentials = 'include'; + } + + // referrerPolicy from config + if (dataSource.referrerPolicy) { + params.referrerPolicy = dataSource.referrerPolicy; + } + + this._status = _loader.LoaderStatus.kConnecting; + self.fetch(seekConfig.url, params).then(function (res) { + if (_this2._requestAbort) { + _this2._requestAbort = false; + _this2._status = _loader.LoaderStatus.kIdle; + return; + } + if (res.ok && res.status >= 200 && res.status <= 299) { + if (res.url !== seekConfig.url) { + if (_this2._onURLRedirect) { + var redirectedURL = _this2._seekHandler.removeURLParameters(res.url); + _this2._onURLRedirect(redirectedURL); + } + } + + var lengthHeader = res.headers.get('Content-Length'); + if (lengthHeader != null) { + _this2._contentLength = parseInt(lengthHeader); + if (_this2._contentLength !== 0) { + if (_this2._onContentLengthKnown) { + _this2._onContentLengthKnown(_this2._contentLength); + } + } + } + + return _this2._pump.call(_this2, res.body.getReader()); + } else { + _this2._status = _loader.LoaderStatus.kError; + if (_this2._onError) { + _this2._onError(_loader.LoaderErrors.HTTP_STATUS_CODE_INVALID, { code: res.status, msg: res.statusText }); + } else { + throw new _exception.RuntimeException('FetchStreamLoader: Http code invalid, ' + res.status + ' ' + res.statusText); + } + } + }).catch(function (e) { + _this2._status = _loader.LoaderStatus.kError; + if (_this2._onError) { + _this2._onError(_loader.LoaderErrors.EXCEPTION, { code: -1, msg: e.message }); + } else { + throw e; + } + }); + } + }, { + key: 'abort', + value: function abort() { + this._requestAbort = true; + } + }, { + key: '_pump', + value: function _pump(reader) { + var _this3 = this; + + // ReadableStreamReader + return reader.read().then(function (result) { + if (result.done) { + // First check received length + if (_this3._contentLength !== null && _this3._receivedLength < _this3._contentLength) { + // Report Early-EOF + _this3._status = _loader.LoaderStatus.kError; + var type = _loader.LoaderErrors.EARLY_EOF; + var info = { code: -1, msg: 'Fetch stream meet Early-EOF' }; + if (_this3._onError) { + _this3._onError(type, info); + } else { + throw new _exception.RuntimeException(info.msg); + } + } else { + // OK. Download complete + _this3._status = _loader.LoaderStatus.kComplete; + if (_this3._onComplete) { + _this3._onComplete(_this3._range.from, _this3._range.from + _this3._receivedLength - 1); + } + } + } else { + if (_this3._requestAbort === true) { + _this3._requestAbort = false; + _this3._status = _loader.LoaderStatus.kComplete; + return reader.cancel(); + } + + _this3._status = _loader.LoaderStatus.kBuffering; + + var chunk = result.value.buffer; + var byteStart = _this3._range.from + _this3._receivedLength; + _this3._receivedLength += chunk.byteLength; + + if (_this3._onDataArrival) { + _this3._onDataArrival(chunk, byteStart, _this3._receivedLength); + } + + _this3._pump(reader); + } + }).catch(function (e) { + if (e.code === 11 && _browser2.default.msedge) { + // InvalidStateError on Microsoft Edge + // Workaround: Edge may throw InvalidStateError after ReadableStreamReader.cancel() call + // Ignore the unknown exception. + // Related issue: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11265202/ + return; + } + + _this3._status = _loader.LoaderStatus.kError; + var type = 0; + var info = null; + + if ((e.code === 19 || e.message === 'network error') && ( // NETWORK_ERR + _this3._contentLength === null || _this3._contentLength !== null && _this3._receivedLength < _this3._contentLength)) { + type = _loader.LoaderErrors.EARLY_EOF; + info = { code: e.code, msg: 'Fetch stream meet Early-EOF' }; + } else { + type = _loader.LoaderErrors.EXCEPTION; + info = { code: e.code, msg: e.message }; + } + + if (_this3._onError) { + _this3._onError(type, info); + } else { + throw new _exception.RuntimeException(info.msg); + } + }); + } + }]); + + return FetchStreamLoader; +}(_loader.BaseLoader); + +exports.default = FetchStreamLoader; + +},{"../utils/browser.js":39,"../utils/exception.js":40,"../utils/logger.js":41,"./loader.js":24}],23:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _speedSampler = _dereq_('./speed-sampler.js'); + +var _speedSampler2 = _interopRequireDefault(_speedSampler); + +var _loader = _dereq_('./loader.js'); + +var _fetchStreamLoader = _dereq_('./fetch-stream-loader.js'); + +var _fetchStreamLoader2 = _interopRequireDefault(_fetchStreamLoader); + +var _xhrMozChunkedLoader = _dereq_('./xhr-moz-chunked-loader.js'); + +var _xhrMozChunkedLoader2 = _interopRequireDefault(_xhrMozChunkedLoader); + +var _xhrMsstreamLoader = _dereq_('./xhr-msstream-loader.js'); + +var _xhrMsstreamLoader2 = _interopRequireDefault(_xhrMsstreamLoader); + +var _xhrRangeLoader = _dereq_('./xhr-range-loader.js'); + +var _xhrRangeLoader2 = _interopRequireDefault(_xhrRangeLoader); + +var _websocketLoader = _dereq_('./websocket-loader.js'); + +var _websocketLoader2 = _interopRequireDefault(_websocketLoader); + +var _rangeSeekHandler = _dereq_('./range-seek-handler.js'); + +var _rangeSeekHandler2 = _interopRequireDefault(_rangeSeekHandler); + +var _paramSeekHandler = _dereq_('./param-seek-handler.js'); + +var _paramSeekHandler2 = _interopRequireDefault(_paramSeekHandler); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * DataSource: { + * url: string, + * filesize: number, + * cors: boolean, + * withCredentials: boolean + * } + * + */ + +// Manage IO Loaders +var IOController = function () { + function IOController(dataSource, config, extraData) { + _classCallCheck(this, IOController); + + this.TAG = 'IOController'; + + this._config = config; + this._extraData = extraData; + + this._stashInitialSize = 1024 * 384; // default initial size: 384KB + if (config.stashInitialSize != undefined && config.stashInitialSize > 0) { + // apply from config + this._stashInitialSize = config.stashInitialSize; + } + + this._stashUsed = 0; + this._stashSize = this._stashInitialSize; + this._bufferSize = 1024 * 1024 * 3; // initial size: 3MB + this._stashBuffer = new ArrayBuffer(this._bufferSize); + this._stashByteStart = 0; + this._enableStash = true; + if (config.enableStashBuffer === false) { + this._enableStash = false; + } + + this._loader = null; + this._loaderClass = null; + this._seekHandler = null; + + this._dataSource = dataSource; + this._isWebSocketURL = /wss?:\/\/(.+?)/.test(dataSource.url); + this._refTotalLength = dataSource.filesize ? dataSource.filesize : null; + this._totalLength = this._refTotalLength; + this._fullRequestFlag = false; + this._currentRange = null; + this._redirectedURL = null; + + this._speedNormalized = 0; + this._speedSampler = new _speedSampler2.default(); + this._speedNormalizeList = [64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096]; + + this._isEarlyEofReconnecting = false; + + this._paused = false; + this._resumeFrom = 0; + + this._onDataArrival = null; + this._onSeeked = null; + this._onError = null; + this._onComplete = null; + this._onRedirect = null; + this._onRecoveredEarlyEof = null; + + this._selectSeekHandler(); + this._selectLoader(); + this._createLoader(); + } + + _createClass(IOController, [{ + key: 'destroy', + value: function destroy() { + if (this._loader.isWorking()) { + this._loader.abort(); + } + this._loader.destroy(); + this._loader = null; + this._loaderClass = null; + this._dataSource = null; + this._stashBuffer = null; + this._stashUsed = this._stashSize = this._bufferSize = this._stashByteStart = 0; + this._currentRange = null; + this._speedSampler = null; + + this._isEarlyEofReconnecting = false; + + this._onDataArrival = null; + this._onSeeked = null; + this._onError = null; + this._onComplete = null; + this._onRedirect = null; + this._onRecoveredEarlyEof = null; + + this._extraData = null; + } + }, { + key: 'isWorking', + value: function isWorking() { + return this._loader && this._loader.isWorking() && !this._paused; + } + }, { + key: 'isPaused', + value: function isPaused() { + return this._paused; + } + }, { + key: '_selectSeekHandler', + value: function _selectSeekHandler() { + var config = this._config; + + if (config.seekType === 'range') { + this._seekHandler = new _rangeSeekHandler2.default(this._config.rangeLoadZeroStart); + } else if (config.seekType === 'param') { + var paramStart = config.seekParamStart || 'bstart'; + var paramEnd = config.seekParamEnd || 'bend'; + + this._seekHandler = new _paramSeekHandler2.default(paramStart, paramEnd); + } else if (config.seekType === 'custom') { + if (typeof config.customSeekHandler !== 'function') { + throw new _exception.InvalidArgumentException('Custom seekType specified in config but invalid customSeekHandler!'); + } + this._seekHandler = new config.customSeekHandler(); + } else { + throw new _exception.InvalidArgumentException('Invalid seekType in config: ' + config.seekType); + } + } + }, { + key: '_selectLoader', + value: function _selectLoader() { + if (this._config.customLoader != null) { + this._loaderClass = this._config.customLoader; + } else if (this._isWebSocketURL) { + this._loaderClass = _websocketLoader2.default; + } else if (_fetchStreamLoader2.default.isSupported()) { + this._loaderClass = _fetchStreamLoader2.default; + } else if (_xhrMozChunkedLoader2.default.isSupported()) { + this._loaderClass = _xhrMozChunkedLoader2.default; + } else if (_xhrRangeLoader2.default.isSupported()) { + this._loaderClass = _xhrRangeLoader2.default; + } else { + throw new _exception.RuntimeException('Your browser doesn\'t support xhr with arraybuffer responseType!'); + } + } + }, { + key: '_createLoader', + value: function _createLoader() { + this._loader = new this._loaderClass(this._seekHandler, this._config); + if (this._loader.needStashBuffer === false) { + this._enableStash = false; + } + this._loader.onContentLengthKnown = this._onContentLengthKnown.bind(this); + this._loader.onURLRedirect = this._onURLRedirect.bind(this); + this._loader.onDataArrival = this._onLoaderChunkArrival.bind(this); + this._loader.onComplete = this._onLoaderComplete.bind(this); + this._loader.onError = this._onLoaderError.bind(this); + } + }, { + key: 'open', + value: function open(optionalFrom) { + this._currentRange = { from: 0, to: -1 }; + if (optionalFrom) { + this._currentRange.from = optionalFrom; + } + + this._speedSampler.reset(); + if (!optionalFrom) { + this._fullRequestFlag = true; + } + + this._loader.open(this._dataSource, Object.assign({}, this._currentRange)); + } + }, { + key: 'abort', + value: function abort() { + this._loader.abort(); + + if (this._paused) { + this._paused = false; + this._resumeFrom = 0; + } + } + }, { + key: 'pause', + value: function pause() { + if (this.isWorking()) { + this._loader.abort(); + + if (this._stashUsed !== 0) { + this._resumeFrom = this._stashByteStart; + this._currentRange.to = this._stashByteStart - 1; + } else { + this._resumeFrom = this._currentRange.to + 1; + } + this._stashUsed = 0; + this._stashByteStart = 0; + this._paused = true; + } + } + }, { + key: 'resume', + value: function resume() { + if (this._paused) { + this._paused = false; + var bytes = this._resumeFrom; + this._resumeFrom = 0; + this._internalSeek(bytes, true); + } + } + }, { + key: 'seek', + value: function seek(bytes) { + this._paused = false; + this._stashUsed = 0; + this._stashByteStart = 0; + this._internalSeek(bytes, true); + } + + /** + * When seeking request is from media seeking, unconsumed stash data should be dropped + * However, stash data shouldn't be dropped if seeking requested from http reconnection + * + * @dropUnconsumed: Ignore and discard all unconsumed data in stash buffer + */ + + }, { + key: '_internalSeek', + value: function _internalSeek(bytes, dropUnconsumed) { + if (this._loader.isWorking()) { + this._loader.abort(); + } + + // dispatch & flush stash buffer before seek + this._flushStashBuffer(dropUnconsumed); + + this._loader.destroy(); + this._loader = null; + + var requestRange = { from: bytes, to: -1 }; + this._currentRange = { from: requestRange.from, to: -1 }; + + this._speedSampler.reset(); + this._stashSize = this._stashInitialSize; + this._createLoader(); + this._loader.open(this._dataSource, requestRange); + + if (this._onSeeked) { + this._onSeeked(); + } + } + }, { + key: 'updateUrl', + value: function updateUrl(url) { + if (!url || typeof url !== 'string' || url.length === 0) { + throw new _exception.InvalidArgumentException('Url must be a non-empty string!'); + } + + this._dataSource.url = url; + + // TODO: replace with new url + } + }, { + key: '_expandBuffer', + value: function _expandBuffer(expectedBytes) { + var bufferNewSize = this._stashSize; + while (bufferNewSize + 1024 * 1024 * 1 < expectedBytes) { + bufferNewSize *= 2; + } + + bufferNewSize += 1024 * 1024 * 1; // bufferSize = stashSize + 1MB + if (bufferNewSize === this._bufferSize) { + return; + } + + var newBuffer = new ArrayBuffer(bufferNewSize); + + if (this._stashUsed > 0) { + // copy existing data into new buffer + var stashOldArray = new Uint8Array(this._stashBuffer, 0, this._stashUsed); + var stashNewArray = new Uint8Array(newBuffer, 0, bufferNewSize); + stashNewArray.set(stashOldArray, 0); + } + + this._stashBuffer = newBuffer; + this._bufferSize = bufferNewSize; + } + }, { + key: '_normalizeSpeed', + value: function _normalizeSpeed(input) { + var list = this._speedNormalizeList; + var last = list.length - 1; + var mid = 0; + var lbound = 0; + var ubound = last; + + if (input < list[0]) { + return list[0]; + } + + // binary search + while (lbound <= ubound) { + mid = lbound + Math.floor((ubound - lbound) / 2); + if (mid === last || input >= list[mid] && input < list[mid + 1]) { + return list[mid]; + } else if (list[mid] < input) { + lbound = mid + 1; + } else { + ubound = mid - 1; + } + } + } + }, { + key: '_adjustStashSize', + value: function _adjustStashSize(normalized) { + var stashSizeKB = 0; + + if (this._config.isLive) { + // live stream: always use single normalized speed for size of stashSizeKB + stashSizeKB = normalized; + } else { + if (normalized < 512) { + stashSizeKB = normalized; + } else if (normalized >= 512 && normalized <= 1024) { + stashSizeKB = Math.floor(normalized * 1.5); + } else { + stashSizeKB = normalized * 2; + } + } + + if (stashSizeKB > 8192) { + stashSizeKB = 8192; + } + + var bufferSize = stashSizeKB * 1024 + 1024 * 1024 * 1; // stashSize + 1MB + if (this._bufferSize < bufferSize) { + this._expandBuffer(bufferSize); + } + this._stashSize = stashSizeKB * 1024; + } + }, { + key: '_dispatchChunks', + value: function _dispatchChunks(chunks, byteStart) { + this._currentRange.to = byteStart + chunks.byteLength - 1; + return this._onDataArrival(chunks, byteStart); + } + }, { + key: '_onURLRedirect', + value: function _onURLRedirect(redirectedURL) { + this._redirectedURL = redirectedURL; + if (this._onRedirect) { + this._onRedirect(redirectedURL); + } + } + }, { + key: '_onContentLengthKnown', + value: function _onContentLengthKnown(contentLength) { + if (contentLength && this._fullRequestFlag) { + this._totalLength = contentLength; + this._fullRequestFlag = false; + } + } + }, { + key: '_onLoaderChunkArrival', + value: function _onLoaderChunkArrival(chunk, byteStart, receivedLength) { + if (!this._onDataArrival) { + throw new _exception.IllegalStateException('IOController: No existing consumer (onDataArrival) callback!'); + } + if (this._paused) { + return; + } + if (this._isEarlyEofReconnecting) { + // Auto-reconnect for EarlyEof succeed, notify to upper-layer by callback + this._isEarlyEofReconnecting = false; + if (this._onRecoveredEarlyEof) { + this._onRecoveredEarlyEof(); + } + } + + this._speedSampler.addBytes(chunk.byteLength); + + // adjust stash buffer size according to network speed dynamically + var KBps = this._speedSampler.lastSecondKBps; + if (KBps !== 0) { + var normalized = this._normalizeSpeed(KBps); + if (this._speedNormalized !== normalized) { + this._speedNormalized = normalized; + this._adjustStashSize(normalized); + } + } + + if (!this._enableStash) { + // disable stash + if (this._stashUsed === 0) { + // dispatch chunk directly to consumer; + // check ret value (consumed bytes) and stash unconsumed to stashBuffer + var consumed = this._dispatchChunks(chunk, byteStart); + if (consumed < chunk.byteLength) { + // unconsumed data remain. + var remain = chunk.byteLength - consumed; + if (remain > this._bufferSize) { + this._expandBuffer(remain); + } + var stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize); + stashArray.set(new Uint8Array(chunk, consumed), 0); + this._stashUsed += remain; + this._stashByteStart = byteStart + consumed; + } + } else { + // else: Merge chunk into stashBuffer, and dispatch stashBuffer to consumer. + if (this._stashUsed + chunk.byteLength > this._bufferSize) { + this._expandBuffer(this._stashUsed + chunk.byteLength); + } + var _stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize); + _stashArray.set(new Uint8Array(chunk), this._stashUsed); + this._stashUsed += chunk.byteLength; + var _consumed = this._dispatchChunks(this._stashBuffer.slice(0, this._stashUsed), this._stashByteStart); + if (_consumed < this._stashUsed && _consumed > 0) { + // unconsumed data remain + var remainArray = new Uint8Array(this._stashBuffer, _consumed); + _stashArray.set(remainArray, 0); + } + this._stashUsed -= _consumed; + this._stashByteStart += _consumed; + } + } else { + // enable stash + if (this._stashUsed === 0 && this._stashByteStart === 0) { + // seeked? or init chunk? + // This is the first chunk after seek action + this._stashByteStart = byteStart; + } + if (this._stashUsed + chunk.byteLength <= this._stashSize) { + // just stash + var _stashArray2 = new Uint8Array(this._stashBuffer, 0, this._stashSize); + _stashArray2.set(new Uint8Array(chunk), this._stashUsed); + this._stashUsed += chunk.byteLength; + } else { + // stashUsed + chunkSize > stashSize, size limit exceeded + var _stashArray3 = new Uint8Array(this._stashBuffer, 0, this._bufferSize); + if (this._stashUsed > 0) { + // There're stash datas in buffer + // dispatch the whole stashBuffer, and stash remain data + // then append chunk to stashBuffer (stash) + var buffer = this._stashBuffer.slice(0, this._stashUsed); + var _consumed2 = this._dispatchChunks(buffer, this._stashByteStart); + if (_consumed2 < buffer.byteLength) { + if (_consumed2 > 0) { + var _remainArray = new Uint8Array(buffer, _consumed2); + _stashArray3.set(_remainArray, 0); + this._stashUsed = _remainArray.byteLength; + this._stashByteStart += _consumed2; + } + } else { + this._stashUsed = 0; + this._stashByteStart += _consumed2; + } + if (this._stashUsed + chunk.byteLength > this._bufferSize) { + this._expandBuffer(this._stashUsed + chunk.byteLength); + _stashArray3 = new Uint8Array(this._stashBuffer, 0, this._bufferSize); + } + _stashArray3.set(new Uint8Array(chunk), this._stashUsed); + this._stashUsed += chunk.byteLength; + } else { + // stash buffer empty, but chunkSize > stashSize (oh, holy shit) + // dispatch chunk directly and stash remain data + var _consumed3 = this._dispatchChunks(chunk, byteStart); + if (_consumed3 < chunk.byteLength) { + var _remain = chunk.byteLength - _consumed3; + if (_remain > this._bufferSize) { + this._expandBuffer(_remain); + _stashArray3 = new Uint8Array(this._stashBuffer, 0, this._bufferSize); + } + _stashArray3.set(new Uint8Array(chunk, _consumed3), 0); + this._stashUsed += _remain; + this._stashByteStart = byteStart + _consumed3; + } + } + } + } + } + }, { + key: '_flushStashBuffer', + value: function _flushStashBuffer(dropUnconsumed) { + if (this._stashUsed > 0) { + var buffer = this._stashBuffer.slice(0, this._stashUsed); + var consumed = this._dispatchChunks(buffer, this._stashByteStart); + var remain = buffer.byteLength - consumed; + + if (consumed < buffer.byteLength) { + if (dropUnconsumed) { + _logger2.default.w(this.TAG, remain + ' bytes unconsumed data remain when flush buffer, dropped'); + } else { + if (consumed > 0) { + var stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize); + var remainArray = new Uint8Array(buffer, consumed); + stashArray.set(remainArray, 0); + this._stashUsed = remainArray.byteLength; + this._stashByteStart += consumed; + } + return 0; + } + } + this._stashUsed = 0; + this._stashByteStart = 0; + return remain; + } + return 0; + } + }, { + key: '_onLoaderComplete', + value: function _onLoaderComplete(from, to) { + // Force-flush stash buffer, and drop unconsumed data + this._flushStashBuffer(true); + + if (this._onComplete) { + this._onComplete(this._extraData); + } + } + }, { + key: '_onLoaderError', + value: function _onLoaderError(type, data) { + _logger2.default.e(this.TAG, 'Loader error, code = ' + data.code + ', msg = ' + data.msg); + + this._flushStashBuffer(false); + + if (this._isEarlyEofReconnecting) { + // Auto-reconnect for EarlyEof failed, throw UnrecoverableEarlyEof error to upper-layer + this._isEarlyEofReconnecting = false; + type = _loader.LoaderErrors.UNRECOVERABLE_EARLY_EOF; + } + + switch (type) { + case _loader.LoaderErrors.EARLY_EOF: + { + if (!this._config.isLive) { + // Do internal http reconnect if not live stream + if (this._totalLength) { + var nextFrom = this._currentRange.to + 1; + if (nextFrom < this._totalLength) { + _logger2.default.w(this.TAG, 'Connection lost, trying reconnect...'); + this._isEarlyEofReconnecting = true; + this._internalSeek(nextFrom, false); + } + return; + } + // else: We don't know totalLength, throw UnrecoverableEarlyEof + } + // live stream: throw UnrecoverableEarlyEof error to upper-layer + type = _loader.LoaderErrors.UNRECOVERABLE_EARLY_EOF; + break; + } + case _loader.LoaderErrors.UNRECOVERABLE_EARLY_EOF: + case _loader.LoaderErrors.CONNECTING_TIMEOUT: + case _loader.LoaderErrors.HTTP_STATUS_CODE_INVALID: + case _loader.LoaderErrors.EXCEPTION: + break; + } + + if (this._onError) { + this._onError(type, data); + } else { + throw new _exception.RuntimeException('IOException: ' + data.msg); + } + } + }, { + key: 'status', + get: function get() { + return this._loader.status; + } + }, { + key: 'extraData', + get: function get() { + return this._extraData; + }, + set: function set(data) { + this._extraData = data; + } + + // prototype: function onDataArrival(chunks: ArrayBuffer, byteStart: number): number + + }, { + key: 'onDataArrival', + get: function get() { + return this._onDataArrival; + }, + set: function set(callback) { + this._onDataArrival = callback; + } + }, { + key: 'onSeeked', + get: function get() { + return this._onSeeked; + }, + set: function set(callback) { + this._onSeeked = callback; + } + + // prototype: function onError(type: number, info: {code: number, msg: string}): void + + }, { + key: 'onError', + get: function get() { + return this._onError; + }, + set: function set(callback) { + this._onError = callback; + } + }, { + key: 'onComplete', + get: function get() { + return this._onComplete; + }, + set: function set(callback) { + this._onComplete = callback; + } + }, { + key: 'onRedirect', + get: function get() { + return this._onRedirect; + }, + set: function set(callback) { + this._onRedirect = callback; + } + }, { + key: 'onRecoveredEarlyEof', + get: function get() { + return this._onRecoveredEarlyEof; + }, + set: function set(callback) { + this._onRecoveredEarlyEof = callback; + } + }, { + key: 'currentURL', + get: function get() { + return this._dataSource.url; + } + }, { + key: 'hasRedirect', + get: function get() { + return this._redirectedURL != null || this._dataSource.redirectedURL != undefined; + } + }, { + key: 'currentRedirectedURL', + get: function get() { + return this._redirectedURL || this._dataSource.redirectedURL; + } + + // in KB/s + + }, { + key: 'currentSpeed', + get: function get() { + if (this._loaderClass === _xhrRangeLoader2.default) { + // SpeedSampler is inaccuracy if loader is RangeLoader + return this._loader.currentSpeed; + } + return this._speedSampler.lastSecondKBps; + } + }, { + key: 'loaderType', + get: function get() { + return this._loader.type; + } + }]); + + return IOController; +}(); + +exports.default = IOController; + +},{"../utils/exception.js":40,"../utils/logger.js":41,"./fetch-stream-loader.js":22,"./loader.js":24,"./param-seek-handler.js":25,"./range-seek-handler.js":26,"./speed-sampler.js":27,"./websocket-loader.js":28,"./xhr-moz-chunked-loader.js":29,"./xhr-msstream-loader.js":30,"./xhr-range-loader.js":31}],24:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.BaseLoader = exports.LoaderErrors = exports.LoaderStatus = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _exception = _dereq_('../utils/exception.js'); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var LoaderStatus = exports.LoaderStatus = { + kIdle: 0, + kConnecting: 1, + kBuffering: 2, + kError: 3, + kComplete: 4 +}; + +var LoaderErrors = exports.LoaderErrors = { + OK: 'OK', + EXCEPTION: 'Exception', + HTTP_STATUS_CODE_INVALID: 'HttpStatusCodeInvalid', + CONNECTING_TIMEOUT: 'ConnectingTimeout', + EARLY_EOF: 'EarlyEof', + UNRECOVERABLE_EARLY_EOF: 'UnrecoverableEarlyEof' +}; + +/* Loader has callbacks which have following prototypes: + * function onContentLengthKnown(contentLength: number): void + * function onURLRedirect(url: string): void + * function onDataArrival(chunk: ArrayBuffer, byteStart: number, receivedLength: number): void + * function onError(errorType: number, errorInfo: {code: number, msg: string}): void + * function onComplete(rangeFrom: number, rangeTo: number): void + */ + +var BaseLoader = exports.BaseLoader = function () { + function BaseLoader(typeName) { + _classCallCheck(this, BaseLoader); + + this._type = typeName || 'undefined'; + this._status = LoaderStatus.kIdle; + this._needStash = false; + // callbacks + this._onContentLengthKnown = null; + this._onURLRedirect = null; + this._onDataArrival = null; + this._onError = null; + this._onComplete = null; + } + + _createClass(BaseLoader, [{ + key: 'destroy', + value: function destroy() { + this._status = LoaderStatus.kIdle; + this._onContentLengthKnown = null; + this._onURLRedirect = null; + this._onDataArrival = null; + this._onError = null; + this._onComplete = null; + } + }, { + key: 'isWorking', + value: function isWorking() { + return this._status === LoaderStatus.kConnecting || this._status === LoaderStatus.kBuffering; + } + }, { + key: 'open', + + + // pure virtual + value: function open(dataSource, range) { + throw new _exception.NotImplementedException('Unimplemented abstract function!'); + } + }, { + key: 'abort', + value: function abort() { + throw new _exception.NotImplementedException('Unimplemented abstract function!'); + } + }, { + key: 'type', + get: function get() { + return this._type; + } + }, { + key: 'status', + get: function get() { + return this._status; + } + }, { + key: 'needStashBuffer', + get: function get() { + return this._needStash; + } + }, { + key: 'onContentLengthKnown', + get: function get() { + return this._onContentLengthKnown; + }, + set: function set(callback) { + this._onContentLengthKnown = callback; + } + }, { + key: 'onURLRedirect', + get: function get() { + return this._onURLRedirect; + }, + set: function set(callback) { + this._onURLRedirect = callback; + } + }, { + key: 'onDataArrival', + get: function get() { + return this._onDataArrival; + }, + set: function set(callback) { + this._onDataArrival = callback; + } + }, { + key: 'onError', + get: function get() { + return this._onError; + }, + set: function set(callback) { + this._onError = callback; + } + }, { + key: 'onComplete', + get: function get() { + return this._onComplete; + }, + set: function set(callback) { + this._onComplete = callback; + } + }]); + + return BaseLoader; +}(); + +},{"../utils/exception.js":40}],25:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var ParamSeekHandler = function () { + function ParamSeekHandler(paramStart, paramEnd) { + _classCallCheck(this, ParamSeekHandler); + + this._startName = paramStart; + this._endName = paramEnd; + } + + _createClass(ParamSeekHandler, [{ + key: 'getConfig', + value: function getConfig(baseUrl, range) { + var url = baseUrl; + + if (range.from !== 0 || range.to !== -1) { + var needAnd = true; + if (url.indexOf('?') === -1) { + url += '?'; + needAnd = false; + } + + if (needAnd) { + url += '&'; + } + + url += this._startName + '=' + range.from.toString(); + + if (range.to !== -1) { + url += '&' + this._endName + '=' + range.to.toString(); + } + } + + return { + url: url, + headers: {} + }; + } + }, { + key: 'removeURLParameters', + value: function removeURLParameters(seekedURL) { + var baseURL = seekedURL.split('?')[0]; + var params = undefined; + + var queryIndex = seekedURL.indexOf('?'); + if (queryIndex !== -1) { + params = seekedURL.substring(queryIndex + 1); + } + + var resultParams = ''; + + if (params != undefined && params.length > 0) { + var pairs = params.split('&'); + + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i].split('='); + var requireAnd = i > 0; + + if (pair[0] !== this._startName && pair[0] !== this._endName) { + if (requireAnd) { + resultParams += '&'; + } + resultParams += pairs[i]; + } + } + } + + return resultParams.length === 0 ? baseURL : baseURL + '?' + resultParams; + } + }]); + + return ParamSeekHandler; +}(); + +exports.default = ParamSeekHandler; + +},{}],26:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var RangeSeekHandler = function () { + function RangeSeekHandler(zeroStart) { + _classCallCheck(this, RangeSeekHandler); + + this._zeroStart = zeroStart || false; + } + + _createClass(RangeSeekHandler, [{ + key: 'getConfig', + value: function getConfig(url, range) { + var headers = {}; + + if (range.from !== 0 || range.to !== -1) { + var param = void 0; + if (range.to !== -1) { + param = 'bytes=' + range.from.toString() + '-' + range.to.toString(); + } else { + param = 'bytes=' + range.from.toString() + '-'; + } + headers['Range'] = param; + } else if (this._zeroStart) { + headers['Range'] = 'bytes=0-'; + } + + return { + url: url, + headers: headers + }; + } + }, { + key: 'removeURLParameters', + value: function removeURLParameters(seekedURL) { + return seekedURL; + } + }]); + + return RangeSeekHandler; +}(); + +exports.default = RangeSeekHandler; + +},{}],27:[function(_dereq_,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Utility class to calculate realtime network I/O speed +var SpeedSampler = function () { + function SpeedSampler() { + _classCallCheck(this, SpeedSampler); + + // milliseconds + this._firstCheckpoint = 0; + this._lastCheckpoint = 0; + this._intervalBytes = 0; + this._totalBytes = 0; + this._lastSecondBytes = 0; + + // compatibility detection + if (self.performance && self.performance.now) { + this._now = self.performance.now.bind(self.performance); + } else { + this._now = Date.now; + } + } + + _createClass(SpeedSampler, [{ + key: "reset", + value: function reset() { + this._firstCheckpoint = this._lastCheckpoint = 0; + this._totalBytes = this._intervalBytes = 0; + this._lastSecondBytes = 0; + } + }, { + key: "addBytes", + value: function addBytes(bytes) { + if (this._firstCheckpoint === 0) { + this._firstCheckpoint = this._now(); + this._lastCheckpoint = this._firstCheckpoint; + this._intervalBytes += bytes; + this._totalBytes += bytes; + } else if (this._now() - this._lastCheckpoint < 1000) { + this._intervalBytes += bytes; + this._totalBytes += bytes; + } else { + // duration >= 1000 + this._lastSecondBytes = this._intervalBytes; + this._intervalBytes = bytes; + this._totalBytes += bytes; + this._lastCheckpoint = this._now(); + } + } + }, { + key: "currentKBps", + get: function get() { + this.addBytes(0); + + var durationSeconds = (this._now() - this._lastCheckpoint) / 1000; + if (durationSeconds == 0) durationSeconds = 1; + return this._intervalBytes / durationSeconds / 1024; + } + }, { + key: "lastSecondKBps", + get: function get() { + this.addBytes(0); + + if (this._lastSecondBytes !== 0) { + return this._lastSecondBytes / 1024; + } else { + // lastSecondBytes === 0 + if (this._now() - this._lastCheckpoint >= 500) { + // if time interval since last checkpoint has exceeded 500ms + // the speed is nearly accurate + return this.currentKBps; + } else { + // We don't know + return 0; + } + } + } + }, { + key: "averageKBps", + get: function get() { + var durationSeconds = (this._now() - this._firstCheckpoint) / 1000; + return this._totalBytes / durationSeconds / 1024; + } + }]); + + return SpeedSampler; +}(); + +exports.default = SpeedSampler; + +},{}],28:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _loader = _dereq_('./loader.js'); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// For FLV over WebSocket live stream +var WebSocketLoader = function (_BaseLoader) { + _inherits(WebSocketLoader, _BaseLoader); + + _createClass(WebSocketLoader, null, [{ + key: 'isSupported', + value: function isSupported() { + try { + return typeof self.WebSocket !== 'undefined'; + } catch (e) { + return false; + } + } + }]); + + function WebSocketLoader() { + _classCallCheck(this, WebSocketLoader); + + var _this = _possibleConstructorReturn(this, (WebSocketLoader.__proto__ || Object.getPrototypeOf(WebSocketLoader)).call(this, 'websocket-loader')); + + _this.TAG = 'WebSocketLoader'; + + _this._needStash = true; + + _this._ws = null; + _this._requestAbort = false; + _this._receivedLength = 0; + return _this; + } + + _createClass(WebSocketLoader, [{ + key: 'destroy', + value: function destroy() { + if (this._ws) { + this.abort(); + } + _get(WebSocketLoader.prototype.__proto__ || Object.getPrototypeOf(WebSocketLoader.prototype), 'destroy', this).call(this); + } + }, { + key: 'open', + value: function open(dataSource) { + try { + var ws = this._ws = new self.WebSocket(dataSource.url); + ws.binaryType = 'arraybuffer'; + ws.onopen = this._onWebSocketOpen.bind(this); + ws.onclose = this._onWebSocketClose.bind(this); + ws.onmessage = this._onWebSocketMessage.bind(this); + ws.onerror = this._onWebSocketError.bind(this); + + this._status = _loader.LoaderStatus.kConnecting; + } catch (e) { + this._status = _loader.LoaderStatus.kError; + + var info = { code: e.code, msg: e.message }; + + if (this._onError) { + this._onError(_loader.LoaderErrors.EXCEPTION, info); + } else { + throw new _exception.RuntimeException(info.msg); + } + } + } + }, { + key: 'abort', + value: function abort() { + var ws = this._ws; + if (ws && (ws.readyState === 0 || ws.readyState === 1)) { + // CONNECTING || OPEN + this._requestAbort = true; + ws.close(); + } + + this._ws = null; + this._status = _loader.LoaderStatus.kComplete; + } + }, { + key: '_onWebSocketOpen', + value: function _onWebSocketOpen(e) { + this._status = _loader.LoaderStatus.kBuffering; + } + }, { + key: '_onWebSocketClose', + value: function _onWebSocketClose(e) { + if (this._requestAbort === true) { + this._requestAbort = false; + return; + } + + this._status = _loader.LoaderStatus.kComplete; + + if (this._onComplete) { + this._onComplete(0, this._receivedLength - 1); + } + } + }, { + key: '_onWebSocketMessage', + value: function _onWebSocketMessage(e) { + var _this2 = this; + + if (e.data instanceof ArrayBuffer) { + this._dispatchArrayBuffer(e.data); + } else if (e.data instanceof Blob) { + var reader = new FileReader(); + reader.onload = function () { + _this2._dispatchArrayBuffer(reader.result); + }; + reader.readAsArrayBuffer(e.data); + } else { + this._status = _loader.LoaderStatus.kError; + var info = { code: -1, msg: 'Unsupported WebSocket message type: ' + e.data.constructor.name }; + + if (this._onError) { + this._onError(_loader.LoaderErrors.EXCEPTION, info); + } else { + throw new _exception.RuntimeException(info.msg); + } + } + } + }, { + key: '_dispatchArrayBuffer', + value: function _dispatchArrayBuffer(arraybuffer) { + var chunk = arraybuffer; + var byteStart = this._receivedLength; + this._receivedLength += chunk.byteLength; + + if (this._onDataArrival) { + this._onDataArrival(chunk, byteStart, this._receivedLength); + } + } + }, { + key: '_onWebSocketError', + value: function _onWebSocketError(e) { + this._status = _loader.LoaderStatus.kError; + + var info = { + code: e.code, + msg: e.message + }; + + if (this._onError) { + this._onError(_loader.LoaderErrors.EXCEPTION, info); + } else { + throw new _exception.RuntimeException(info.msg); + } + } + }]); + + return WebSocketLoader; +}(_loader.BaseLoader); + +exports.default = WebSocketLoader; + +},{"../utils/exception.js":40,"../utils/logger.js":41,"./loader.js":24}],29:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _loader = _dereq_('./loader.js'); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// For FireFox browser which supports `xhr.responseType = 'moz-chunked-arraybuffer'` +var MozChunkedLoader = function (_BaseLoader) { + _inherits(MozChunkedLoader, _BaseLoader); + + _createClass(MozChunkedLoader, null, [{ + key: 'isSupported', + value: function isSupported() { + try { + var xhr = new XMLHttpRequest(); + // Firefox 37- requires .open() to be called before setting responseType + xhr.open('GET', 'https://example.com', true); + xhr.responseType = 'moz-chunked-arraybuffer'; + return xhr.responseType === 'moz-chunked-arraybuffer'; + } catch (e) { + _logger2.default.w('MozChunkedLoader', e.message); + return false; + } + } + }]); + + function MozChunkedLoader(seekHandler, config) { + _classCallCheck(this, MozChunkedLoader); + + var _this = _possibleConstructorReturn(this, (MozChunkedLoader.__proto__ || Object.getPrototypeOf(MozChunkedLoader)).call(this, 'xhr-moz-chunked-loader')); + + _this.TAG = 'MozChunkedLoader'; + + _this._seekHandler = seekHandler; + _this._config = config; + _this._needStash = true; + + _this._xhr = null; + _this._requestAbort = false; + _this._contentLength = null; + _this._receivedLength = 0; + return _this; + } + + _createClass(MozChunkedLoader, [{ + key: 'destroy', + value: function destroy() { + if (this.isWorking()) { + this.abort(); + } + if (this._xhr) { + this._xhr.onreadystatechange = null; + this._xhr.onprogress = null; + this._xhr.onloadend = null; + this._xhr.onerror = null; + this._xhr = null; + } + _get(MozChunkedLoader.prototype.__proto__ || Object.getPrototypeOf(MozChunkedLoader.prototype), 'destroy', this).call(this); + } + }, { + key: 'open', + value: function open(dataSource, range) { + this._dataSource = dataSource; + this._range = range; + + var sourceURL = dataSource.url; + if (this._config.reuseRedirectedURL && dataSource.redirectedURL != undefined) { + sourceURL = dataSource.redirectedURL; + } + + var seekConfig = this._seekHandler.getConfig(sourceURL, range); + this._requestURL = seekConfig.url; + + var xhr = this._xhr = new XMLHttpRequest(); + xhr.open('GET', seekConfig.url, true); + xhr.responseType = 'moz-chunked-arraybuffer'; + xhr.onreadystatechange = this._onReadyStateChange.bind(this); + xhr.onprogress = this._onProgress.bind(this); + xhr.onloadend = this._onLoadEnd.bind(this); + xhr.onerror = this._onXhrError.bind(this); + + // cors is auto detected and enabled by xhr + + // withCredentials is disabled by default + if (dataSource.withCredentials) { + xhr.withCredentials = true; + } + + if (_typeof(seekConfig.headers) === 'object') { + var headers = seekConfig.headers; + + for (var key in headers) { + if (headers.hasOwnProperty(key)) { + xhr.setRequestHeader(key, headers[key]); + } + } + } + + // add additional headers + if (_typeof(this._config.headers) === 'object') { + var _headers = this._config.headers; + + for (var _key in _headers) { + if (_headers.hasOwnProperty(_key)) { + xhr.setRequestHeader(_key, _headers[_key]); + } + } + } + + this._status = _loader.LoaderStatus.kConnecting; + xhr.send(); + } + }, { + key: 'abort', + value: function abort() { + this._requestAbort = true; + if (this._xhr) { + this._xhr.abort(); + } + this._status = _loader.LoaderStatus.kComplete; + } + }, { + key: '_onReadyStateChange', + value: function _onReadyStateChange(e) { + var xhr = e.target; + + if (xhr.readyState === 2) { + // HEADERS_RECEIVED + if (xhr.responseURL != undefined && xhr.responseURL !== this._requestURL) { + if (this._onURLRedirect) { + var redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL); + this._onURLRedirect(redirectedURL); + } + } + + if (xhr.status !== 0 && (xhr.status < 200 || xhr.status > 299)) { + this._status = _loader.LoaderStatus.kError; + if (this._onError) { + this._onError(_loader.LoaderErrors.HTTP_STATUS_CODE_INVALID, { code: xhr.status, msg: xhr.statusText }); + } else { + throw new _exception.RuntimeException('MozChunkedLoader: Http code invalid, ' + xhr.status + ' ' + xhr.statusText); + } + } else { + this._status = _loader.LoaderStatus.kBuffering; + } + } + } + }, { + key: '_onProgress', + value: function _onProgress(e) { + if (this._status === _loader.LoaderStatus.kError) { + // Ignore error response + return; + } + + if (this._contentLength === null) { + if (e.total !== null && e.total !== 0) { + this._contentLength = e.total; + if (this._onContentLengthKnown) { + this._onContentLengthKnown(this._contentLength); + } + } + } + + var chunk = e.target.response; + var byteStart = this._range.from + this._receivedLength; + this._receivedLength += chunk.byteLength; + + if (this._onDataArrival) { + this._onDataArrival(chunk, byteStart, this._receivedLength); + } + } + }, { + key: '_onLoadEnd', + value: function _onLoadEnd(e) { + if (this._requestAbort === true) { + this._requestAbort = false; + return; + } else if (this._status === _loader.LoaderStatus.kError) { + return; + } + + this._status = _loader.LoaderStatus.kComplete; + if (this._onComplete) { + this._onComplete(this._range.from, this._range.from + this._receivedLength - 1); + } + } + }, { + key: '_onXhrError', + value: function _onXhrError(e) { + this._status = _loader.LoaderStatus.kError; + var type = 0; + var info = null; + + if (this._contentLength && e.loaded < this._contentLength) { + type = _loader.LoaderErrors.EARLY_EOF; + info = { code: -1, msg: 'Moz-Chunked stream meet Early-Eof' }; + } else { + type = _loader.LoaderErrors.EXCEPTION; + info = { code: -1, msg: e.constructor.name + ' ' + e.type }; + } + + if (this._onError) { + this._onError(type, info); + } else { + throw new _exception.RuntimeException(info.msg); + } + } + }]); + + return MozChunkedLoader; +}(_loader.BaseLoader); + +exports.default = MozChunkedLoader; + +},{"../utils/exception.js":40,"../utils/logger.js":41,"./loader.js":24}],30:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _loader = _dereq_('./loader.js'); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Notice: ms-stream may cause IE/Edge browser crash if seek too frequently!!! + * The browser may crash in wininet.dll. Disable for now. + * + * For IE11/Edge browser by microsoft which supports `xhr.responseType = 'ms-stream'` + * Notice that ms-stream API sucks. The buffer is always expanding along with downloading. + * + * We need to abort the xhr if buffer size exceeded limit size (e.g. 16 MiB), then do reconnect. + * in order to release previous ArrayBuffer to avoid memory leak + * + * Otherwise, the ArrayBuffer will increase to a terrible size that equals final file size. + */ +var MSStreamLoader = function (_BaseLoader) { + _inherits(MSStreamLoader, _BaseLoader); + + _createClass(MSStreamLoader, null, [{ + key: 'isSupported', + value: function isSupported() { + try { + if (typeof self.MSStream === 'undefined' || typeof self.MSStreamReader === 'undefined') { + return false; + } + + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'https://example.com', true); + xhr.responseType = 'ms-stream'; + return xhr.responseType === 'ms-stream'; + } catch (e) { + _logger2.default.w('MSStreamLoader', e.message); + return false; + } + } + }]); + + function MSStreamLoader(seekHandler, config) { + _classCallCheck(this, MSStreamLoader); + + var _this = _possibleConstructorReturn(this, (MSStreamLoader.__proto__ || Object.getPrototypeOf(MSStreamLoader)).call(this, 'xhr-msstream-loader')); + + _this.TAG = 'MSStreamLoader'; + + _this._seekHandler = seekHandler; + _this._config = config; + _this._needStash = true; + + _this._xhr = null; + _this._reader = null; // MSStreamReader + + _this._totalRange = null; + _this._currentRange = null; + + _this._currentRequestURL = null; + _this._currentRedirectedURL = null; + + _this._contentLength = null; + _this._receivedLength = 0; + + _this._bufferLimit = 16 * 1024 * 1024; // 16MB + _this._lastTimeBufferSize = 0; + _this._isReconnecting = false; + return _this; + } + + _createClass(MSStreamLoader, [{ + key: 'destroy', + value: function destroy() { + if (this.isWorking()) { + this.abort(); + } + if (this._reader) { + this._reader.onprogress = null; + this._reader.onload = null; + this._reader.onerror = null; + this._reader = null; + } + if (this._xhr) { + this._xhr.onreadystatechange = null; + this._xhr = null; + } + _get(MSStreamLoader.prototype.__proto__ || Object.getPrototypeOf(MSStreamLoader.prototype), 'destroy', this).call(this); + } + }, { + key: 'open', + value: function open(dataSource, range) { + this._internalOpen(dataSource, range, false); + } + }, { + key: '_internalOpen', + value: function _internalOpen(dataSource, range, isSubrange) { + this._dataSource = dataSource; + + if (!isSubrange) { + this._totalRange = range; + } else { + this._currentRange = range; + } + + var sourceURL = dataSource.url; + if (this._config.reuseRedirectedURL) { + if (this._currentRedirectedURL != undefined) { + sourceURL = this._currentRedirectedURL; + } else if (dataSource.redirectedURL != undefined) { + sourceURL = dataSource.redirectedURL; + } + } + + var seekConfig = this._seekHandler.getConfig(sourceURL, range); + this._currentRequestURL = seekConfig.url; + + var reader = this._reader = new self.MSStreamReader(); + reader.onprogress = this._msrOnProgress.bind(this); + reader.onload = this._msrOnLoad.bind(this); + reader.onerror = this._msrOnError.bind(this); + + var xhr = this._xhr = new XMLHttpRequest(); + xhr.open('GET', seekConfig.url, true); + xhr.responseType = 'ms-stream'; + xhr.onreadystatechange = this._xhrOnReadyStateChange.bind(this); + xhr.onerror = this._xhrOnError.bind(this); + + if (dataSource.withCredentials) { + xhr.withCredentials = true; + } + + if (_typeof(seekConfig.headers) === 'object') { + var headers = seekConfig.headers; + + for (var key in headers) { + if (headers.hasOwnProperty(key)) { + xhr.setRequestHeader(key, headers[key]); + } + } + } + + // add additional headers + if (_typeof(this._config.headers) === 'object') { + var _headers = this._config.headers; + + for (var _key in _headers) { + if (_headers.hasOwnProperty(_key)) { + xhr.setRequestHeader(_key, _headers[_key]); + } + } + } + + if (this._isReconnecting) { + this._isReconnecting = false; + } else { + this._status = _loader.LoaderStatus.kConnecting; + } + xhr.send(); + } + }, { + key: 'abort', + value: function abort() { + this._internalAbort(); + this._status = _loader.LoaderStatus.kComplete; + } + }, { + key: '_internalAbort', + value: function _internalAbort() { + if (this._reader) { + if (this._reader.readyState === 1) { + // LOADING + this._reader.abort(); + } + this._reader.onprogress = null; + this._reader.onload = null; + this._reader.onerror = null; + this._reader = null; + } + if (this._xhr) { + this._xhr.abort(); + this._xhr.onreadystatechange = null; + this._xhr = null; + } + } + }, { + key: '_xhrOnReadyStateChange', + value: function _xhrOnReadyStateChange(e) { + var xhr = e.target; + + if (xhr.readyState === 2) { + // HEADERS_RECEIVED + if (xhr.status >= 200 && xhr.status <= 299) { + this._status = _loader.LoaderStatus.kBuffering; + + if (xhr.responseURL != undefined) { + var redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL); + if (xhr.responseURL !== this._currentRequestURL && redirectedURL !== this._currentRedirectedURL) { + this._currentRedirectedURL = redirectedURL; + if (this._onURLRedirect) { + this._onURLRedirect(redirectedURL); + } + } + } + + var lengthHeader = xhr.getResponseHeader('Content-Length'); + if (lengthHeader != null && this._contentLength == null) { + var length = parseInt(lengthHeader); + if (length > 0) { + this._contentLength = length; + if (this._onContentLengthKnown) { + this._onContentLengthKnown(this._contentLength); + } + } + } + } else { + this._status = _loader.LoaderStatus.kError; + if (this._onError) { + this._onError(_loader.LoaderErrors.HTTP_STATUS_CODE_INVALID, { code: xhr.status, msg: xhr.statusText }); + } else { + throw new _exception.RuntimeException('MSStreamLoader: Http code invalid, ' + xhr.status + ' ' + xhr.statusText); + } + } + } else if (xhr.readyState === 3) { + // LOADING + if (xhr.status >= 200 && xhr.status <= 299) { + this._status = _loader.LoaderStatus.kBuffering; + + var msstream = xhr.response; + this._reader.readAsArrayBuffer(msstream); + } + } + } + }, { + key: '_xhrOnError', + value: function _xhrOnError(e) { + this._status = _loader.LoaderStatus.kError; + var type = _loader.LoaderErrors.EXCEPTION; + var info = { code: -1, msg: e.constructor.name + ' ' + e.type }; + + if (this._onError) { + this._onError(type, info); + } else { + throw new _exception.RuntimeException(info.msg); + } + } + }, { + key: '_msrOnProgress', + value: function _msrOnProgress(e) { + var reader = e.target; + var bigbuffer = reader.result; + if (bigbuffer == null) { + // result may be null, workaround for buggy M$ + this._doReconnectIfNeeded(); + return; + } + + var slice = bigbuffer.slice(this._lastTimeBufferSize); + this._lastTimeBufferSize = bigbuffer.byteLength; + var byteStart = this._totalRange.from + this._receivedLength; + this._receivedLength += slice.byteLength; + + if (this._onDataArrival) { + this._onDataArrival(slice, byteStart, this._receivedLength); + } + + if (bigbuffer.byteLength >= this._bufferLimit) { + _logger2.default.v(this.TAG, 'MSStream buffer exceeded max size near ' + (byteStart + slice.byteLength) + ', reconnecting...'); + this._doReconnectIfNeeded(); + } + } + }, { + key: '_doReconnectIfNeeded', + value: function _doReconnectIfNeeded() { + if (this._contentLength == null || this._receivedLength < this._contentLength) { + this._isReconnecting = true; + this._lastTimeBufferSize = 0; + this._internalAbort(); + + var range = { + from: this._totalRange.from + this._receivedLength, + to: -1 + }; + this._internalOpen(this._dataSource, range, true); + } + } + }, { + key: '_msrOnLoad', + value: function _msrOnLoad(e) { + // actually it is onComplete event + this._status = _loader.LoaderStatus.kComplete; + if (this._onComplete) { + this._onComplete(this._totalRange.from, this._totalRange.from + this._receivedLength - 1); + } + } + }, { + key: '_msrOnError', + value: function _msrOnError(e) { + this._status = _loader.LoaderStatus.kError; + var type = 0; + var info = null; + + if (this._contentLength && this._receivedLength < this._contentLength) { + type = _loader.LoaderErrors.EARLY_EOF; + info = { code: -1, msg: 'MSStream meet Early-Eof' }; + } else { + type = _loader.LoaderErrors.EARLY_EOF; + info = { code: -1, msg: e.constructor.name + ' ' + e.type }; + } + + if (this._onError) { + this._onError(type, info); + } else { + throw new _exception.RuntimeException(info.msg); + } + } + }]); + + return MSStreamLoader; +}(_loader.BaseLoader); + +exports.default = MSStreamLoader; + +},{"../utils/exception.js":40,"../utils/logger.js":41,"./loader.js":24}],31:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _speedSampler = _dereq_('./speed-sampler.js'); + +var _speedSampler2 = _interopRequireDefault(_speedSampler); + +var _loader = _dereq_('./loader.js'); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Universal IO Loader, implemented by adding Range header in xhr's request header +var RangeLoader = function (_BaseLoader) { + _inherits(RangeLoader, _BaseLoader); + + _createClass(RangeLoader, null, [{ + key: 'isSupported', + value: function isSupported() { + try { + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'https://example.com', true); + xhr.responseType = 'arraybuffer'; + return xhr.responseType === 'arraybuffer'; + } catch (e) { + _logger2.default.w('RangeLoader', e.message); + return false; + } + } + }]); + + function RangeLoader(seekHandler, config) { + _classCallCheck(this, RangeLoader); + + var _this = _possibleConstructorReturn(this, (RangeLoader.__proto__ || Object.getPrototypeOf(RangeLoader)).call(this, 'xhr-range-loader')); + + _this.TAG = 'RangeLoader'; + + _this._seekHandler = seekHandler; + _this._config = config; + _this._needStash = false; + + _this._chunkSizeKBList = [128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192]; + _this._currentChunkSizeKB = 384; + _this._currentSpeedNormalized = 0; + _this._zeroSpeedChunkCount = 0; + + _this._xhr = null; + _this._speedSampler = new _speedSampler2.default(); + + _this._requestAbort = false; + _this._waitForTotalLength = false; + _this._totalLengthReceived = false; + + _this._currentRequestURL = null; + _this._currentRedirectedURL = null; + _this._currentRequestRange = null; + _this._totalLength = null; // size of the entire file + _this._contentLength = null; // Content-Length of entire request range + _this._receivedLength = 0; // total received bytes + _this._lastTimeLoaded = 0; // received bytes of current request sub-range + return _this; + } + + _createClass(RangeLoader, [{ + key: 'destroy', + value: function destroy() { + if (this.isWorking()) { + this.abort(); + } + if (this._xhr) { + this._xhr.onreadystatechange = null; + this._xhr.onprogress = null; + this._xhr.onload = null; + this._xhr.onerror = null; + this._xhr = null; + } + _get(RangeLoader.prototype.__proto__ || Object.getPrototypeOf(RangeLoader.prototype), 'destroy', this).call(this); + } + }, { + key: 'open', + value: function open(dataSource, range) { + this._dataSource = dataSource; + this._range = range; + this._status = _loader.LoaderStatus.kConnecting; + + var useRefTotalLength = false; + if (this._dataSource.filesize != undefined && this._dataSource.filesize !== 0) { + useRefTotalLength = true; + this._totalLength = this._dataSource.filesize; + } + + if (!this._totalLengthReceived && !useRefTotalLength) { + // We need total filesize + this._waitForTotalLength = true; + this._internalOpen(this._dataSource, { from: 0, to: -1 }); + } else { + // We have filesize, start loading + this._openSubRange(); + } + } + }, { + key: '_openSubRange', + value: function _openSubRange() { + var chunkSize = this._currentChunkSizeKB * 1024; + + var from = this._range.from + this._receivedLength; + var to = from + chunkSize; + + if (this._contentLength != null) { + if (to - this._range.from >= this._contentLength) { + to = this._range.from + this._contentLength - 1; + } + } + + this._currentRequestRange = { from: from, to: to }; + this._internalOpen(this._dataSource, this._currentRequestRange); + } + }, { + key: '_internalOpen', + value: function _internalOpen(dataSource, range) { + this._lastTimeLoaded = 0; + + var sourceURL = dataSource.url; + if (this._config.reuseRedirectedURL) { + if (this._currentRedirectedURL != undefined) { + sourceURL = this._currentRedirectedURL; + } else if (dataSource.redirectedURL != undefined) { + sourceURL = dataSource.redirectedURL; + } + } + + var seekConfig = this._seekHandler.getConfig(sourceURL, range); + this._currentRequestURL = seekConfig.url; + + var xhr = this._xhr = new XMLHttpRequest(); + xhr.open('GET', seekConfig.url, true); + xhr.responseType = 'arraybuffer'; + xhr.onreadystatechange = this._onReadyStateChange.bind(this); + xhr.onprogress = this._onProgress.bind(this); + xhr.onload = this._onLoad.bind(this); + xhr.onerror = this._onXhrError.bind(this); + + if (dataSource.withCredentials) { + xhr.withCredentials = true; + } + + if (_typeof(seekConfig.headers) === 'object') { + var headers = seekConfig.headers; + + for (var key in headers) { + if (headers.hasOwnProperty(key)) { + xhr.setRequestHeader(key, headers[key]); + } + } + } + + // add additional headers + if (_typeof(this._config.headers) === 'object') { + var _headers = this._config.headers; + + for (var _key in _headers) { + if (_headers.hasOwnProperty(_key)) { + xhr.setRequestHeader(_key, _headers[_key]); + } + } + } + + xhr.send(); + } + }, { + key: 'abort', + value: function abort() { + this._requestAbort = true; + this._internalAbort(); + this._status = _loader.LoaderStatus.kComplete; + } + }, { + key: '_internalAbort', + value: function _internalAbort() { + if (this._xhr) { + this._xhr.onreadystatechange = null; + this._xhr.onprogress = null; + this._xhr.onload = null; + this._xhr.onerror = null; + this._xhr.abort(); + this._xhr = null; + } + } + }, { + key: '_onReadyStateChange', + value: function _onReadyStateChange(e) { + var xhr = e.target; + + if (xhr.readyState === 2) { + // HEADERS_RECEIVED + if (xhr.responseURL != undefined) { + // if the browser support this property + var redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL); + if (xhr.responseURL !== this._currentRequestURL && redirectedURL !== this._currentRedirectedURL) { + this._currentRedirectedURL = redirectedURL; + if (this._onURLRedirect) { + this._onURLRedirect(redirectedURL); + } + } + } + + if (xhr.status >= 200 && xhr.status <= 299) { + if (this._waitForTotalLength) { + return; + } + this._status = _loader.LoaderStatus.kBuffering; + } else { + this._status = _loader.LoaderStatus.kError; + if (this._onError) { + this._onError(_loader.LoaderErrors.HTTP_STATUS_CODE_INVALID, { code: xhr.status, msg: xhr.statusText }); + } else { + throw new _exception.RuntimeException('RangeLoader: Http code invalid, ' + xhr.status + ' ' + xhr.statusText); + } + } + } + } + }, { + key: '_onProgress', + value: function _onProgress(e) { + if (this._status === _loader.LoaderStatus.kError) { + // Ignore error response + return; + } + + if (this._contentLength === null) { + var openNextRange = false; + + if (this._waitForTotalLength) { + this._waitForTotalLength = false; + this._totalLengthReceived = true; + openNextRange = true; + + var total = e.total; + this._internalAbort(); + if (total != null & total !== 0) { + this._totalLength = total; + } + } + + // calculate currrent request range's contentLength + if (this._range.to === -1) { + this._contentLength = this._totalLength - this._range.from; + } else { + // to !== -1 + this._contentLength = this._range.to - this._range.from + 1; + } + + if (openNextRange) { + this._openSubRange(); + return; + } + if (this._onContentLengthKnown) { + this._onContentLengthKnown(this._contentLength); + } + } + + var delta = e.loaded - this._lastTimeLoaded; + this._lastTimeLoaded = e.loaded; + this._speedSampler.addBytes(delta); + } + }, { + key: '_normalizeSpeed', + value: function _normalizeSpeed(input) { + var list = this._chunkSizeKBList; + var last = list.length - 1; + var mid = 0; + var lbound = 0; + var ubound = last; + + if (input < list[0]) { + return list[0]; + } + + while (lbound <= ubound) { + mid = lbound + Math.floor((ubound - lbound) / 2); + if (mid === last || input >= list[mid] && input < list[mid + 1]) { + return list[mid]; + } else if (list[mid] < input) { + lbound = mid + 1; + } else { + ubound = mid - 1; + } + } + } + }, { + key: '_onLoad', + value: function _onLoad(e) { + if (this._status === _loader.LoaderStatus.kError) { + // Ignore error response + return; + } + + if (this._waitForTotalLength) { + this._waitForTotalLength = false; + return; + } + + this._lastTimeLoaded = 0; + var KBps = this._speedSampler.lastSecondKBps; + if (KBps === 0) { + this._zeroSpeedChunkCount++; + if (this._zeroSpeedChunkCount >= 3) { + // Try get currentKBps after 3 chunks + KBps = this._speedSampler.currentKBps; + } + } + + if (KBps !== 0) { + var normalized = this._normalizeSpeed(KBps); + if (this._currentSpeedNormalized !== normalized) { + this._currentSpeedNormalized = normalized; + this._currentChunkSizeKB = normalized; + } + } + + var chunk = e.target.response; + var byteStart = this._range.from + this._receivedLength; + this._receivedLength += chunk.byteLength; + + var reportComplete = false; + + if (this._contentLength != null && this._receivedLength < this._contentLength) { + // continue load next chunk + this._openSubRange(); + } else { + reportComplete = true; + } + + // dispatch received chunk + if (this._onDataArrival) { + this._onDataArrival(chunk, byteStart, this._receivedLength); + } + + if (reportComplete) { + this._status = _loader.LoaderStatus.kComplete; + if (this._onComplete) { + this._onComplete(this._range.from, this._range.from + this._receivedLength - 1); + } + } + } + }, { + key: '_onXhrError', + value: function _onXhrError(e) { + this._status = _loader.LoaderStatus.kError; + var type = 0; + var info = null; + + if (this._contentLength && this._receivedLength > 0 && this._receivedLength < this._contentLength) { + type = _loader.LoaderErrors.EARLY_EOF; + info = { code: -1, msg: 'RangeLoader meet Early-Eof' }; + } else { + type = _loader.LoaderErrors.EXCEPTION; + info = { code: -1, msg: e.constructor.name + ' ' + e.type }; + } + + if (this._onError) { + this._onError(type, info); + } else { + throw new _exception.RuntimeException(info.msg); + } + } + }, { + key: 'currentSpeed', + get: function get() { + return this._speedSampler.lastSecondKBps; + } + }]); + + return RangeLoader; +}(_loader.BaseLoader); + +exports.default = RangeLoader; + +},{"../utils/exception.js":40,"../utils/logger.js":41,"./loader.js":24,"./speed-sampler.js":27}],32:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _events = _dereq_('events'); + +var _events2 = _interopRequireDefault(_events); + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _browser = _dereq_('../utils/browser.js'); + +var _browser2 = _interopRequireDefault(_browser); + +var _playerEvents = _dereq_('./player-events.js'); + +var _playerEvents2 = _interopRequireDefault(_playerEvents); + +var _transmuxer = _dereq_('../core/transmuxer.js'); + +var _transmuxer2 = _interopRequireDefault(_transmuxer); + +var _transmuxingEvents = _dereq_('../core/transmuxing-events.js'); + +var _transmuxingEvents2 = _interopRequireDefault(_transmuxingEvents); + +var _mseController = _dereq_('../core/mse-controller.js'); + +var _mseController2 = _interopRequireDefault(_mseController); + +var _mseEvents = _dereq_('../core/mse-events.js'); + +var _mseEvents2 = _interopRequireDefault(_mseEvents); + +var _playerErrors = _dereq_('./player-errors.js'); + +var _config = _dereq_('../config.js'); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var FlvPlayer = function () { + function FlvPlayer(mediaDataSource, config) { + _classCallCheck(this, FlvPlayer); + + this.TAG = 'FlvPlayer'; + this._type = 'FlvPlayer'; + this._emitter = new _events2.default(); + + this._config = (0, _config.createDefaultConfig)(); + if ((typeof config === 'undefined' ? 'undefined' : _typeof(config)) === 'object') { + Object.assign(this._config, config); + } + + if (mediaDataSource.type.toLowerCase() !== 'flv') { + throw new _exception.InvalidArgumentException('FlvPlayer requires an flv MediaDataSource input!'); + } + + if (mediaDataSource.isLive === true) { + this._config.isLive = true; + } + + this.e = { + onvLoadedMetadata: this._onvLoadedMetadata.bind(this), + onvSeeking: this._onvSeeking.bind(this), + onvCanPlay: this._onvCanPlay.bind(this), + onvStalled: this._onvStalled.bind(this), + onvProgress: this._onvProgress.bind(this) + }; + + if (self.performance && self.performance.now) { + this._now = self.performance.now.bind(self.performance); + } else { + this._now = Date.now; + } + + this._pendingSeekTime = null; // in seconds + this._requestSetTime = false; + this._seekpointRecord = null; + this._progressChecker = null; + + this._mediaDataSource = mediaDataSource; + this._mediaElement = null; + this._msectl = null; + this._transmuxer = null; + + this._mseSourceOpened = false; + this._hasPendingLoad = false; + this._receivedCanPlay = false; + + this._mediaInfo = null; + this._statisticsInfo = null; + + var chromeNeedIDRFix = _browser2.default.chrome && (_browser2.default.version.major < 50 || _browser2.default.version.major === 50 && _browser2.default.version.build < 2661); + this._alwaysSeekKeyframe = chromeNeedIDRFix || _browser2.default.msedge || _browser2.default.msie ? true : false; + + if (this._alwaysSeekKeyframe) { + this._config.accurateSeek = false; + } + } + + _createClass(FlvPlayer, [{ + key: 'destroy', + value: function destroy() { + if (this._progressChecker != null) { + window.clearInterval(this._progressChecker); + this._progressChecker = null; + } + if (this._transmuxer) { + this.unload(); + } + if (this._mediaElement) { + this.detachMediaElement(); + } + this.e = null; + this._mediaDataSource = null; + + this._emitter.removeAllListeners(); + this._emitter = null; + } + }, { + key: 'on', + value: function on(event, listener) { + var _this = this; + + if (event === _playerEvents2.default.MEDIA_INFO) { + if (this._mediaInfo != null) { + Promise.resolve().then(function () { + _this._emitter.emit(_playerEvents2.default.MEDIA_INFO, _this.mediaInfo); + }); + } + } else if (event === _playerEvents2.default.STATISTICS_INFO) { + if (this._statisticsInfo != null) { + Promise.resolve().then(function () { + _this._emitter.emit(_playerEvents2.default.STATISTICS_INFO, _this.statisticsInfo); + }); + } + } + this._emitter.addListener(event, listener); + } + }, { + key: 'off', + value: function off(event, listener) { + this._emitter.removeListener(event, listener); + } + }, { + key: 'attachMediaElement', + value: function attachMediaElement(mediaElement) { + var _this2 = this; + + this._mediaElement = mediaElement; + mediaElement.addEventListener('loadedmetadata', this.e.onvLoadedMetadata); + mediaElement.addEventListener('seeking', this.e.onvSeeking); + mediaElement.addEventListener('canplay', this.e.onvCanPlay); + mediaElement.addEventListener('stalled', this.e.onvStalled); + mediaElement.addEventListener('progress', this.e.onvProgress); + + this._msectl = new _mseController2.default(this._config); + + this._msectl.on(_mseEvents2.default.UPDATE_END, this._onmseUpdateEnd.bind(this)); + this._msectl.on(_mseEvents2.default.BUFFER_FULL, this._onmseBufferFull.bind(this)); + this._msectl.on(_mseEvents2.default.SOURCE_OPEN, function () { + _this2._mseSourceOpened = true; + if (_this2._hasPendingLoad) { + _this2._hasPendingLoad = false; + _this2.load(); + } + }); + this._msectl.on(_mseEvents2.default.ERROR, function (info) { + _this2._emitter.emit(_playerEvents2.default.ERROR, _playerErrors.ErrorTypes.MEDIA_ERROR, _playerErrors.ErrorDetails.MEDIA_MSE_ERROR, info); + }); + + this._msectl.attachMediaElement(mediaElement); + + if (this._pendingSeekTime != null) { + try { + mediaElement.currentTime = this._pendingSeekTime; + this._pendingSeekTime = null; + } catch (e) { + // IE11 may throw InvalidStateError if readyState === 0 + // We can defer set currentTime operation after loadedmetadata + } + } + } + }, { + key: 'detachMediaElement', + value: function detachMediaElement() { + if (this._mediaElement) { + this._msectl.detachMediaElement(); + this._mediaElement.removeEventListener('loadedmetadata', this.e.onvLoadedMetadata); + this._mediaElement.removeEventListener('seeking', this.e.onvSeeking); + this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay); + this._mediaElement.removeEventListener('stalled', this.e.onvStalled); + this._mediaElement.removeEventListener('progress', this.e.onvProgress); + this._mediaElement = null; + } + if (this._msectl) { + this._msectl.destroy(); + this._msectl = null; + } + } + }, { + key: 'load', + value: function load() { + var _this3 = this; + + if (!this._mediaElement) { + throw new _exception.IllegalStateException('HTMLMediaElement must be attached before load()!'); + } + if (this._transmuxer) { + throw new _exception.IllegalStateException('FlvPlayer.load() has been called, please call unload() first!'); + } + if (this._hasPendingLoad) { + return; + } + + if (this._config.deferLoadAfterSourceOpen && this._mseSourceOpened === false) { + this._hasPendingLoad = true; + return; + } + + if (this._mediaElement.readyState > 0) { + this._requestSetTime = true; + // IE11 may throw InvalidStateError if readyState === 0 + this._mediaElement.currentTime = 0; + } + + this._transmuxer = new _transmuxer2.default(this._mediaDataSource, this._config); + + this._transmuxer.on(_transmuxingEvents2.default.INIT_SEGMENT, function (type, is) { + _this3._msectl.appendInitSegment(is); + }); + this._transmuxer.on(_transmuxingEvents2.default.MEDIA_SEGMENT, function (type, ms) { + _this3._msectl.appendMediaSegment(ms); + + // lazyLoad check + if (_this3._config.lazyLoad && !_this3._config.isLive) { + var currentTime = _this3._mediaElement.currentTime; + if (ms.info.endDts >= (currentTime + _this3._config.lazyLoadMaxDuration) * 1000) { + if (_this3._progressChecker == null) { + _logger2.default.v(_this3.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task'); + _this3._suspendTransmuxer(); + } + } + } + }); + this._transmuxer.on(_transmuxingEvents2.default.LOADING_COMPLETE, function () { + _this3._msectl.endOfStream(); + _this3._emitter.emit(_playerEvents2.default.LOADING_COMPLETE); + }); + this._transmuxer.on(_transmuxingEvents2.default.RECOVERED_EARLY_EOF, function () { + _this3._emitter.emit(_playerEvents2.default.RECOVERED_EARLY_EOF); + }); + this._transmuxer.on(_transmuxingEvents2.default.IO_ERROR, function (detail, info) { + _this3._emitter.emit(_playerEvents2.default.ERROR, _playerErrors.ErrorTypes.NETWORK_ERROR, detail, info); + }); + this._transmuxer.on(_transmuxingEvents2.default.DEMUX_ERROR, function (detail, info) { + _this3._emitter.emit(_playerEvents2.default.ERROR, _playerErrors.ErrorTypes.MEDIA_ERROR, detail, { code: -1, msg: info }); + }); + this._transmuxer.on(_transmuxingEvents2.default.MEDIA_INFO, function (mediaInfo) { + _this3._mediaInfo = mediaInfo; + _this3._emitter.emit(_playerEvents2.default.MEDIA_INFO, Object.assign({}, mediaInfo)); + }); + this._transmuxer.on(_transmuxingEvents2.default.METADATA_ARRIVED, function (metadata) { + _this3._emitter.emit(_playerEvents2.default.METADATA_ARRIVED, metadata); + }); + this._transmuxer.on(_transmuxingEvents2.default.SCRIPTDATA_ARRIVED, function (data) { + _this3._emitter.emit(_playerEvents2.default.SCRIPTDATA_ARRIVED, data); + }); + this._transmuxer.on(_transmuxingEvents2.default.STATISTICS_INFO, function (statInfo) { + _this3._statisticsInfo = _this3._fillStatisticsInfo(statInfo); + _this3._emitter.emit(_playerEvents2.default.STATISTICS_INFO, Object.assign({}, _this3._statisticsInfo)); + }); + this._transmuxer.on(_transmuxingEvents2.default.RECOMMEND_SEEKPOINT, function (milliseconds) { + if (_this3._mediaElement && !_this3._config.accurateSeek) { + _this3._requestSetTime = true; + _this3._mediaElement.currentTime = milliseconds / 1000; + } + }); + + this._transmuxer.open(); + } + }, { + key: 'unload', + value: function unload() { + if (this._mediaElement) { + this._mediaElement.pause(); + } + if (this._msectl) { + this._msectl.seek(0); + } + if (this._transmuxer) { + this._transmuxer.close(); + this._transmuxer.destroy(); + this._transmuxer = null; + } + } + }, { + key: 'play', + value: function play() { + return this._mediaElement.play(); + } + }, { + key: 'pause', + value: function pause() { + this._mediaElement.pause(); + } + }, { + key: '_fillStatisticsInfo', + value: function _fillStatisticsInfo(statInfo) { + statInfo.playerType = this._type; + + if (!(this._mediaElement instanceof HTMLVideoElement)) { + return statInfo; + } + + var hasQualityInfo = true; + var decoded = 0; + var dropped = 0; + + if (this._mediaElement.getVideoPlaybackQuality) { + var quality = this._mediaElement.getVideoPlaybackQuality(); + decoded = quality.totalVideoFrames; + dropped = quality.droppedVideoFrames; + } else if (this._mediaElement.webkitDecodedFrameCount != undefined) { + decoded = this._mediaElement.webkitDecodedFrameCount; + dropped = this._mediaElement.webkitDroppedFrameCount; + } else { + hasQualityInfo = false; + } + + if (hasQualityInfo) { + statInfo.decodedFrames = decoded; + statInfo.droppedFrames = dropped; + } + + return statInfo; + } + }, { + key: '_onmseUpdateEnd', + value: function _onmseUpdateEnd() { + if (!this._config.lazyLoad || this._config.isLive) { + return; + } + + var buffered = this._mediaElement.buffered; + var currentTime = this._mediaElement.currentTime; + var currentRangeStart = 0; + var currentRangeEnd = 0; + + for (var i = 0; i < buffered.length; i++) { + var start = buffered.start(i); + var end = buffered.end(i); + if (start <= currentTime && currentTime < end) { + currentRangeStart = start; + currentRangeEnd = end; + break; + } + } + + if (currentRangeEnd >= currentTime + this._config.lazyLoadMaxDuration && this._progressChecker == null) { + _logger2.default.v(this.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task'); + this._suspendTransmuxer(); + } + } + }, { + key: '_onmseBufferFull', + value: function _onmseBufferFull() { + _logger2.default.v(this.TAG, 'MSE SourceBuffer is full, suspend transmuxing task'); + if (this._progressChecker == null) { + this._suspendTransmuxer(); + } + } + }, { + key: '_suspendTransmuxer', + value: function _suspendTransmuxer() { + if (this._transmuxer) { + this._transmuxer.pause(); + + if (this._progressChecker == null) { + this._progressChecker = window.setInterval(this._checkProgressAndResume.bind(this), 1000); + } + } + } + }, { + key: '_checkProgressAndResume', + value: function _checkProgressAndResume() { + var currentTime = this._mediaElement.currentTime; + var buffered = this._mediaElement.buffered; + + var needResume = false; + + for (var i = 0; i < buffered.length; i++) { + var from = buffered.start(i); + var to = buffered.end(i); + if (currentTime >= from && currentTime < to) { + if (currentTime >= to - this._config.lazyLoadRecoverDuration) { + needResume = true; + } + break; + } + } + + if (needResume) { + window.clearInterval(this._progressChecker); + this._progressChecker = null; + if (needResume) { + _logger2.default.v(this.TAG, 'Continue loading from paused position'); + this._transmuxer.resume(); + } + } + } + }, { + key: '_isTimepointBuffered', + value: function _isTimepointBuffered(seconds) { + var buffered = this._mediaElement.buffered; + + for (var i = 0; i < buffered.length; i++) { + var from = buffered.start(i); + var to = buffered.end(i); + if (seconds >= from && seconds < to) { + return true; + } + } + return false; + } + }, { + key: '_internalSeek', + value: function _internalSeek(seconds) { + var directSeek = this._isTimepointBuffered(seconds); + + var directSeekBegin = false; + var directSeekBeginTime = 0; + + if (seconds < 1.0 && this._mediaElement.buffered.length > 0) { + var videoBeginTime = this._mediaElement.buffered.start(0); + if (videoBeginTime < 1.0 && seconds < videoBeginTime || _browser2.default.safari) { + directSeekBegin = true; + // also workaround for Safari: Seek to 0 may cause video stuck, use 0.1 to avoid + directSeekBeginTime = _browser2.default.safari ? 0.1 : videoBeginTime; + } + } + + if (directSeekBegin) { + // seek to video begin, set currentTime directly if beginPTS buffered + this._requestSetTime = true; + this._mediaElement.currentTime = directSeekBeginTime; + } else if (directSeek) { + // buffered position + if (!this._alwaysSeekKeyframe) { + this._requestSetTime = true; + this._mediaElement.currentTime = seconds; + } else { + var idr = this._msectl.getNearestKeyframe(Math.floor(seconds * 1000)); + this._requestSetTime = true; + if (idr != null) { + this._mediaElement.currentTime = idr.dts / 1000; + } else { + this._mediaElement.currentTime = seconds; + } + } + if (this._progressChecker != null) { + this._checkProgressAndResume(); + } + } else { + if (this._progressChecker != null) { + window.clearInterval(this._progressChecker); + this._progressChecker = null; + } + this._msectl.seek(seconds); + this._transmuxer.seek(Math.floor(seconds * 1000)); // in milliseconds + // no need to set mediaElement.currentTime if non-accurateSeek, + // just wait for the recommend_seekpoint callback + if (this._config.accurateSeek) { + this._requestSetTime = true; + this._mediaElement.currentTime = seconds; + } + } + } + }, { + key: '_checkAndApplyUnbufferedSeekpoint', + value: function _checkAndApplyUnbufferedSeekpoint() { + if (this._seekpointRecord) { + if (this._seekpointRecord.recordTime <= this._now() - 100) { + var target = this._mediaElement.currentTime; + this._seekpointRecord = null; + if (!this._isTimepointBuffered(target)) { + if (this._progressChecker != null) { + window.clearTimeout(this._progressChecker); + this._progressChecker = null; + } + // .currentTime is consists with .buffered timestamp + // Chrome/Edge use DTS, while FireFox/Safari use PTS + this._msectl.seek(target); + this._transmuxer.seek(Math.floor(target * 1000)); + // set currentTime if accurateSeek, or wait for recommend_seekpoint callback + if (this._config.accurateSeek) { + this._requestSetTime = true; + this._mediaElement.currentTime = target; + } + } + } else { + window.setTimeout(this._checkAndApplyUnbufferedSeekpoint.bind(this), 50); + } + } + } + }, { + key: '_checkAndResumeStuckPlayback', + value: function _checkAndResumeStuckPlayback(stalled) { + var media = this._mediaElement; + if (stalled || !this._receivedCanPlay || media.readyState < 2) { + // HAVE_CURRENT_DATA + var buffered = media.buffered; + if (buffered.length > 0 && media.currentTime < buffered.start(0)) { + _logger2.default.w(this.TAG, 'Playback seems stuck at ' + media.currentTime + ', seek to ' + buffered.start(0)); + this._requestSetTime = true; + this._mediaElement.currentTime = buffered.start(0); + this._mediaElement.removeEventListener('progress', this.e.onvProgress); + } + } else { + // Playback didn't stuck, remove progress event listener + this._mediaElement.removeEventListener('progress', this.e.onvProgress); + } + } + }, { + key: '_onvLoadedMetadata', + value: function _onvLoadedMetadata(e) { + if (this._pendingSeekTime != null) { + this._mediaElement.currentTime = this._pendingSeekTime; + this._pendingSeekTime = null; + } + } + }, { + key: '_onvSeeking', + value: function _onvSeeking(e) { + // handle seeking request from browser's progress bar + var target = this._mediaElement.currentTime; + var buffered = this._mediaElement.buffered; + + if (this._requestSetTime) { + this._requestSetTime = false; + return; + } + + if (target < 1.0 && buffered.length > 0) { + // seek to video begin, set currentTime directly if beginPTS buffered + var videoBeginTime = buffered.start(0); + if (videoBeginTime < 1.0 && target < videoBeginTime || _browser2.default.safari) { + this._requestSetTime = true; + // also workaround for Safari: Seek to 0 may cause video stuck, use 0.1 to avoid + this._mediaElement.currentTime = _browser2.default.safari ? 0.1 : videoBeginTime; + return; + } + } + + if (this._isTimepointBuffered(target)) { + if (this._alwaysSeekKeyframe) { + var idr = this._msectl.getNearestKeyframe(Math.floor(target * 1000)); + if (idr != null) { + this._requestSetTime = true; + this._mediaElement.currentTime = idr.dts / 1000; + } + } + if (this._progressChecker != null) { + this._checkProgressAndResume(); + } + return; + } + + this._seekpointRecord = { + seekPoint: target, + recordTime: this._now() + }; + window.setTimeout(this._checkAndApplyUnbufferedSeekpoint.bind(this), 50); + } + }, { + key: '_onvCanPlay', + value: function _onvCanPlay(e) { + this._receivedCanPlay = true; + this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay); + } + }, { + key: '_onvStalled', + value: function _onvStalled(e) { + this._checkAndResumeStuckPlayback(true); + } + }, { + key: '_onvProgress', + value: function _onvProgress(e) { + this._checkAndResumeStuckPlayback(); + } + }, { + key: 'type', + get: function get() { + return this._type; + } + }, { + key: 'buffered', + get: function get() { + return this._mediaElement.buffered; + } + }, { + key: 'duration', + get: function get() { + return this._mediaElement.duration; + } + }, { + key: 'volume', + get: function get() { + return this._mediaElement.volume; + }, + set: function set(value) { + this._mediaElement.volume = value; + } + }, { + key: 'muted', + get: function get() { + return this._mediaElement.muted; + }, + set: function set(muted) { + this._mediaElement.muted = muted; + } + }, { + key: 'currentTime', + get: function get() { + if (this._mediaElement) { + return this._mediaElement.currentTime; + } + return 0; + }, + set: function set(seconds) { + if (this._mediaElement) { + this._internalSeek(seconds); + } else { + this._pendingSeekTime = seconds; + } + } + }, { + key: 'mediaInfo', + get: function get() { + return Object.assign({}, this._mediaInfo); + } + }, { + key: 'statisticsInfo', + get: function get() { + if (this._statisticsInfo == null) { + this._statisticsInfo = {}; + } + this._statisticsInfo = this._fillStatisticsInfo(this._statisticsInfo); + return Object.assign({}, this._statisticsInfo); + } + }]); + + return FlvPlayer; +}(); + +exports.default = FlvPlayer; + +},{"../config.js":5,"../core/mse-controller.js":9,"../core/mse-events.js":10,"../core/transmuxer.js":11,"../core/transmuxing-events.js":13,"../utils/browser.js":39,"../utils/exception.js":40,"../utils/logger.js":41,"./player-errors.js":34,"./player-events.js":35,"events":2}],33:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _events = _dereq_('events'); + +var _events2 = _interopRequireDefault(_events); + +var _playerEvents = _dereq_('./player-events.js'); + +var _playerEvents2 = _interopRequireDefault(_playerEvents); + +var _config = _dereq_('../config.js'); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +// Player wrapper for browser's native player (HTMLVideoElement) without MediaSource src. +var NativePlayer = function () { + function NativePlayer(mediaDataSource, config) { + _classCallCheck(this, NativePlayer); + + this.TAG = 'NativePlayer'; + this._type = 'NativePlayer'; + this._emitter = new _events2.default(); + + this._config = (0, _config.createDefaultConfig)(); + if ((typeof config === 'undefined' ? 'undefined' : _typeof(config)) === 'object') { + Object.assign(this._config, config); + } + + if (mediaDataSource.type.toLowerCase() === 'flv') { + throw new _exception.InvalidArgumentException('NativePlayer does\'t support flv MediaDataSource input!'); + } + if (mediaDataSource.hasOwnProperty('segments')) { + throw new _exception.InvalidArgumentException('NativePlayer(' + mediaDataSource.type + ') doesn\'t support multipart playback!'); + } + + this.e = { + onvLoadedMetadata: this._onvLoadedMetadata.bind(this) + }; + + this._pendingSeekTime = null; + this._statisticsReporter = null; + + this._mediaDataSource = mediaDataSource; + this._mediaElement = null; + } + + _createClass(NativePlayer, [{ + key: 'destroy', + value: function destroy() { + if (this._mediaElement) { + this.unload(); + this.detachMediaElement(); + } + this.e = null; + this._mediaDataSource = null; + this._emitter.removeAllListeners(); + this._emitter = null; + } + }, { + key: 'on', + value: function on(event, listener) { + var _this = this; + + if (event === _playerEvents2.default.MEDIA_INFO) { + if (this._mediaElement != null && this._mediaElement.readyState !== 0) { + // HAVE_NOTHING + Promise.resolve().then(function () { + _this._emitter.emit(_playerEvents2.default.MEDIA_INFO, _this.mediaInfo); + }); + } + } else if (event === _playerEvents2.default.STATISTICS_INFO) { + if (this._mediaElement != null && this._mediaElement.readyState !== 0) { + Promise.resolve().then(function () { + _this._emitter.emit(_playerEvents2.default.STATISTICS_INFO, _this.statisticsInfo); + }); + } + } + this._emitter.addListener(event, listener); + } + }, { + key: 'off', + value: function off(event, listener) { + this._emitter.removeListener(event, listener); + } + }, { + key: 'attachMediaElement', + value: function attachMediaElement(mediaElement) { + this._mediaElement = mediaElement; + mediaElement.addEventListener('loadedmetadata', this.e.onvLoadedMetadata); + + if (this._pendingSeekTime != null) { + try { + mediaElement.currentTime = this._pendingSeekTime; + this._pendingSeekTime = null; + } catch (e) { + // IE11 may throw InvalidStateError if readyState === 0 + // Defer set currentTime operation after loadedmetadata + } + } + } + }, { + key: 'detachMediaElement', + value: function detachMediaElement() { + if (this._mediaElement) { + this._mediaElement.src = ''; + this._mediaElement.removeAttribute('src'); + this._mediaElement.removeEventListener('loadedmetadata', this.e.onvLoadedMetadata); + this._mediaElement = null; + } + if (this._statisticsReporter != null) { + window.clearInterval(this._statisticsReporter); + this._statisticsReporter = null; + } + } + }, { + key: 'load', + value: function load() { + if (!this._mediaElement) { + throw new _exception.IllegalStateException('HTMLMediaElement must be attached before load()!'); + } + this._mediaElement.src = this._mediaDataSource.url; + + if (this._mediaElement.readyState > 0) { + this._mediaElement.currentTime = 0; + } + + this._mediaElement.preload = 'auto'; + this._mediaElement.load(); + this._statisticsReporter = window.setInterval(this._reportStatisticsInfo.bind(this), this._config.statisticsInfoReportInterval); + } + }, { + key: 'unload', + value: function unload() { + if (this._mediaElement) { + this._mediaElement.src = ''; + this._mediaElement.removeAttribute('src'); + } + if (this._statisticsReporter != null) { + window.clearInterval(this._statisticsReporter); + this._statisticsReporter = null; + } + } + }, { + key: 'play', + value: function play() { + return this._mediaElement.play(); + } + }, { + key: 'pause', + value: function pause() { + this._mediaElement.pause(); + } + }, { + key: '_onvLoadedMetadata', + value: function _onvLoadedMetadata(e) { + if (this._pendingSeekTime != null) { + this._mediaElement.currentTime = this._pendingSeekTime; + this._pendingSeekTime = null; + } + this._emitter.emit(_playerEvents2.default.MEDIA_INFO, this.mediaInfo); + } + }, { + key: '_reportStatisticsInfo', + value: function _reportStatisticsInfo() { + this._emitter.emit(_playerEvents2.default.STATISTICS_INFO, this.statisticsInfo); + } + }, { + key: 'type', + get: function get() { + return this._type; + } + }, { + key: 'buffered', + get: function get() { + return this._mediaElement.buffered; + } + }, { + key: 'duration', + get: function get() { + return this._mediaElement.duration; + } + }, { + key: 'volume', + get: function get() { + return this._mediaElement.volume; + }, + set: function set(value) { + this._mediaElement.volume = value; + } + }, { + key: 'muted', + get: function get() { + return this._mediaElement.muted; + }, + set: function set(muted) { + this._mediaElement.muted = muted; + } + }, { + key: 'currentTime', + get: function get() { + if (this._mediaElement) { + return this._mediaElement.currentTime; + } + return 0; + }, + set: function set(seconds) { + if (this._mediaElement) { + this._mediaElement.currentTime = seconds; + } else { + this._pendingSeekTime = seconds; + } + } + }, { + key: 'mediaInfo', + get: function get() { + var mediaPrefix = this._mediaElement instanceof HTMLAudioElement ? 'audio/' : 'video/'; + var info = { + mimeType: mediaPrefix + this._mediaDataSource.type + }; + if (this._mediaElement) { + info.duration = Math.floor(this._mediaElement.duration * 1000); + if (this._mediaElement instanceof HTMLVideoElement) { + info.width = this._mediaElement.videoWidth; + info.height = this._mediaElement.videoHeight; + } + } + return info; + } + }, { + key: 'statisticsInfo', + get: function get() { + var info = { + playerType: this._type, + url: this._mediaDataSource.url + }; + + if (!(this._mediaElement instanceof HTMLVideoElement)) { + return info; + } + + var hasQualityInfo = true; + var decoded = 0; + var dropped = 0; + + if (this._mediaElement.getVideoPlaybackQuality) { + var quality = this._mediaElement.getVideoPlaybackQuality(); + decoded = quality.totalVideoFrames; + dropped = quality.droppedVideoFrames; + } else if (this._mediaElement.webkitDecodedFrameCount != undefined) { + decoded = this._mediaElement.webkitDecodedFrameCount; + dropped = this._mediaElement.webkitDroppedFrameCount; + } else { + hasQualityInfo = false; + } + + if (hasQualityInfo) { + info.decodedFrames = decoded; + info.droppedFrames = dropped; + } + + return info; + } + }]); + + return NativePlayer; +}(); + +exports.default = NativePlayer; + +},{"../config.js":5,"../utils/exception.js":40,"./player-events.js":35,"events":2}],34:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ErrorDetails = exports.ErrorTypes = undefined; + +var _loader = _dereq_('../io/loader.js'); + +var _demuxErrors = _dereq_('../demux/demux-errors.js'); + +var _demuxErrors2 = _interopRequireDefault(_demuxErrors); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var ErrorTypes = exports.ErrorTypes = { + NETWORK_ERROR: 'NetworkError', + MEDIA_ERROR: 'MediaError', + OTHER_ERROR: 'OtherError' +}; + +var ErrorDetails = exports.ErrorDetails = { + NETWORK_EXCEPTION: _loader.LoaderErrors.EXCEPTION, + NETWORK_STATUS_CODE_INVALID: _loader.LoaderErrors.HTTP_STATUS_CODE_INVALID, + NETWORK_TIMEOUT: _loader.LoaderErrors.CONNECTING_TIMEOUT, + NETWORK_UNRECOVERABLE_EARLY_EOF: _loader.LoaderErrors.UNRECOVERABLE_EARLY_EOF, + + MEDIA_MSE_ERROR: 'MediaMSEError', + + MEDIA_FORMAT_ERROR: _demuxErrors2.default.FORMAT_ERROR, + MEDIA_FORMAT_UNSUPPORTED: _demuxErrors2.default.FORMAT_UNSUPPORTED, + MEDIA_CODEC_UNSUPPORTED: _demuxErrors2.default.CODEC_UNSUPPORTED +}; + +},{"../demux/demux-errors.js":16,"../io/loader.js":24}],35:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var PlayerEvents = { + ERROR: 'error', + LOADING_COMPLETE: 'loading_complete', + RECOVERED_EARLY_EOF: 'recovered_early_eof', + MEDIA_INFO: 'media_info', + METADATA_ARRIVED: 'metadata_arrived', + SCRIPTDATA_ARRIVED: 'scriptdata_arrived', + STATISTICS_INFO: 'statistics_info' +}; + +exports.default = PlayerEvents; + +},{}],36:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * This file is modified from dailymotion's hls.js library (hls.js/src/helper/aac.js) + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var AAC = function () { + function AAC() { + _classCallCheck(this, AAC); + } + + _createClass(AAC, null, [{ + key: 'getSilentFrame', + value: function getSilentFrame(codec, channelCount) { + if (codec === 'mp4a.40.2') { + // handle LC-AAC + if (channelCount === 1) { + return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x23, 0x80]); + } else if (channelCount === 2) { + return new Uint8Array([0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80]); + } else if (channelCount === 3) { + return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x8e]); + } else if (channelCount === 4) { + return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x80, 0x2c, 0x80, 0x08, 0x02, 0x38]); + } else if (channelCount === 5) { + return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x38]); + } else if (channelCount === 6) { + return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x00, 0xb2, 0x00, 0x20, 0x08, 0xe0]); + } + } else { + // handle HE-AAC (mp4a.40.5 / mp4a.40.29) + if (channelCount === 1) { + // ffmpeg -y -f lavfi -i "aevalsrc=0:d=0.05" -c:a libfdk_aac -profile:a aac_he -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac + return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x4e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x1c, 0x6, 0xf1, 0xc1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); + } else if (channelCount === 2) { + // ffmpeg -y -f lavfi -i "aevalsrc=0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac + return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); + } else if (channelCount === 3) { + // ffmpeg -y -f lavfi -i "aevalsrc=0|0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac + return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); + } + } + return null; + } + }]); + + return AAC; +}(); + +exports.default = AAC; + +},{}],37:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * This file is derived from dailymotion's hls.js library (hls.js/src/remux/mp4-generator.js) + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// MP4 boxes generator for ISO BMFF (ISO Base Media File Format, defined in ISO/IEC 14496-12) +var MP4 = function () { + function MP4() { + _classCallCheck(this, MP4); + } + + _createClass(MP4, null, [{ + key: 'init', + value: function init() { + MP4.types = { + avc1: [], avcC: [], btrt: [], dinf: [], + dref: [], esds: [], ftyp: [], hdlr: [], + mdat: [], mdhd: [], mdia: [], mfhd: [], + minf: [], moof: [], moov: [], mp4a: [], + mvex: [], mvhd: [], sdtp: [], stbl: [], + stco: [], stsc: [], stsd: [], stsz: [], + stts: [], tfdt: [], tfhd: [], traf: [], + trak: [], trun: [], trex: [], tkhd: [], + vmhd: [], smhd: [], '.mp3': [] + }; + + for (var name in MP4.types) { + if (MP4.types.hasOwnProperty(name)) { + MP4.types[name] = [name.charCodeAt(0), name.charCodeAt(1), name.charCodeAt(2), name.charCodeAt(3)]; + } + } + + var constants = MP4.constants = {}; + + constants.FTYP = new Uint8Array([0x69, 0x73, 0x6F, 0x6D, // major_brand: isom + 0x0, 0x0, 0x0, 0x1, // minor_version: 0x01 + 0x69, 0x73, 0x6F, 0x6D, // isom + 0x61, 0x76, 0x63, 0x31 // avc1 + ]); + + constants.STSD_PREFIX = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x01 // entry_count + ]); + + constants.STTS = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00 // entry_count + ]); + + constants.STSC = constants.STCO = constants.STTS; + + constants.STSZ = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // sample_size + 0x00, 0x00, 0x00, 0x00 // sample_count + ]); + + constants.HDLR_VIDEO = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' + 0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x69, 0x64, 0x65, 0x6F, 0x48, 0x61, 0x6E, 0x64, 0x6C, 0x65, 0x72, 0x00 // name: VideoHandler + ]); + + constants.HDLR_AUDIO = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x73, 0x6F, 0x75, 0x6E, // handler_type: 'soun' + 0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x6F, 0x75, 0x6E, 0x64, 0x48, 0x61, 0x6E, 0x64, 0x6C, 0x65, 0x72, 0x00 // name: SoundHandler + ]); + + constants.DREF = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x01, // entry_count + 0x00, 0x00, 0x00, 0x0C, // entry_size + 0x75, 0x72, 0x6C, 0x20, // type 'url ' + 0x00, 0x00, 0x00, 0x01 // version(0) + flags + ]); + + // Sound media header + constants.SMHD = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00 // balance(2) + reserved(2) + ]); + + // video media header + constants.VMHD = new Uint8Array([0x00, 0x00, 0x00, 0x01, // version(0) + flags + 0x00, 0x00, // graphicsmode: 2 bytes + 0x00, 0x00, 0x00, 0x00, // opcolor: 3 * 2 bytes + 0x00, 0x00]); + } + + // Generate a box + + }, { + key: 'box', + value: function box(type) { + var size = 8; + var result = null; + var datas = Array.prototype.slice.call(arguments, 1); + var arrayCount = datas.length; + + for (var i = 0; i < arrayCount; i++) { + size += datas[i].byteLength; + } + + result = new Uint8Array(size); + result[0] = size >>> 24 & 0xFF; // size + result[1] = size >>> 16 & 0xFF; + result[2] = size >>> 8 & 0xFF; + result[3] = size & 0xFF; + + result.set(type, 4); // type + + var offset = 8; + for (var _i = 0; _i < arrayCount; _i++) { + // data body + result.set(datas[_i], offset); + offset += datas[_i].byteLength; + } + + return result; + } + + // emit ftyp & moov + + }, { + key: 'generateInitSegment', + value: function generateInitSegment(meta) { + var ftyp = MP4.box(MP4.types.ftyp, MP4.constants.FTYP); + var moov = MP4.moov(meta); + + var result = new Uint8Array(ftyp.byteLength + moov.byteLength); + result.set(ftyp, 0); + result.set(moov, ftyp.byteLength); + return result; + } + + // Movie metadata box + + }, { + key: 'moov', + value: function moov(meta) { + var mvhd = MP4.mvhd(meta.timescale, meta.duration); + var trak = MP4.trak(meta); + var mvex = MP4.mvex(meta); + return MP4.box(MP4.types.moov, mvhd, trak, mvex); + } + + // Movie header box + + }, { + key: 'mvhd', + value: function mvhd(timescale, duration) { + return MP4.box(MP4.types.mvhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + timescale >>> 24 & 0xFF, // timescale: 4 bytes + timescale >>> 16 & 0xFF, timescale >>> 8 & 0xFF, timescale & 0xFF, duration >>> 24 & 0xFF, // duration: 4 bytes + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x00, 0x01, 0x00, 0x00, // Preferred rate: 1.0 + 0x01, 0x00, 0x00, 0x00, // PreferredVolume(1.0, 2bytes) + reserved(2bytes) + 0x00, 0x00, 0x00, 0x00, // reserved: 4 + 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix---- + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // ----end composition matrix---- + 0x00, 0x00, 0x00, 0x00, // ----begin pre_defined 6 * 4 bytes---- + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ----end pre_defined 6 * 4 bytes---- + 0xFF, 0xFF, 0xFF, 0xFF // next_track_ID + ])); + } + + // Track box + + }, { + key: 'trak', + value: function trak(meta) { + return MP4.box(MP4.types.trak, MP4.tkhd(meta), MP4.mdia(meta)); + } + + // Track header box + + }, { + key: 'tkhd', + value: function tkhd(meta) { + var trackId = meta.id, + duration = meta.duration; + var width = meta.presentWidth, + height = meta.presentHeight; + + return MP4.box(MP4.types.tkhd, new Uint8Array([0x00, 0x00, 0x00, 0x07, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + trackId >>> 24 & 0xFF, // track_ID: 4 bytes + trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF, 0x00, 0x00, 0x00, 0x00, // reserved: 4 bytes + duration >>> 24 & 0xFF, // duration: 4 bytes + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // layer(2bytes) + alternate_group(2bytes) + 0x00, 0x00, 0x00, 0x00, // volume(2bytes) + reserved(2bytes) + 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix---- + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // ----end composition matrix---- + width >>> 8 & 0xFF, // width and height + width & 0xFF, 0x00, 0x00, height >>> 8 & 0xFF, height & 0xFF, 0x00, 0x00])); + } + + // Media Box + + }, { + key: 'mdia', + value: function mdia(meta) { + return MP4.box(MP4.types.mdia, MP4.mdhd(meta), MP4.hdlr(meta), MP4.minf(meta)); + } + + // Media header box + + }, { + key: 'mdhd', + value: function mdhd(meta) { + var timescale = meta.timescale; + var duration = meta.duration; + return MP4.box(MP4.types.mdhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + timescale >>> 24 & 0xFF, // timescale: 4 bytes + timescale >>> 16 & 0xFF, timescale >>> 8 & 0xFF, timescale & 0xFF, duration >>> 24 & 0xFF, // duration: 4 bytes + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x55, 0xC4, // language: und (undetermined) + 0x00, 0x00 // pre_defined = 0 + ])); + } + + // Media handler reference box + + }, { + key: 'hdlr', + value: function hdlr(meta) { + var data = null; + if (meta.type === 'audio') { + data = MP4.constants.HDLR_AUDIO; + } else { + data = MP4.constants.HDLR_VIDEO; + } + return MP4.box(MP4.types.hdlr, data); + } + + // Media infomation box + + }, { + key: 'minf', + value: function minf(meta) { + var xmhd = null; + if (meta.type === 'audio') { + xmhd = MP4.box(MP4.types.smhd, MP4.constants.SMHD); + } else { + xmhd = MP4.box(MP4.types.vmhd, MP4.constants.VMHD); + } + return MP4.box(MP4.types.minf, xmhd, MP4.dinf(), MP4.stbl(meta)); + } + + // Data infomation box + + }, { + key: 'dinf', + value: function dinf() { + var result = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, MP4.constants.DREF)); + return result; + } + + // Sample table box + + }, { + key: 'stbl', + value: function stbl(meta) { + var result = MP4.box(MP4.types.stbl, // type: stbl + MP4.stsd(meta), // Sample Description Table + MP4.box(MP4.types.stts, MP4.constants.STTS), // Time-To-Sample + MP4.box(MP4.types.stsc, MP4.constants.STSC), // Sample-To-Chunk + MP4.box(MP4.types.stsz, MP4.constants.STSZ), // Sample size + MP4.box(MP4.types.stco, MP4.constants.STCO) // Chunk offset + ); + return result; + } + + // Sample description box + + }, { + key: 'stsd', + value: function stsd(meta) { + if (meta.type === 'audio') { + if (meta.codec === 'mp3') { + return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.mp3(meta)); + } + // else: aac -> mp4a + return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.mp4a(meta)); + } else { + return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.avc1(meta)); + } + } + }, { + key: 'mp3', + value: function mp3(meta) { + var channelCount = meta.channelCount; + var sampleRate = meta.audioSampleRate; + + var data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // reserved(4) + 0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2) + 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, channelCount, // channelCount(2) + 0x00, 0x10, // sampleSize(2) + 0x00, 0x00, 0x00, 0x00, // reserved(4) + sampleRate >>> 8 & 0xFF, // Audio sample rate + sampleRate & 0xFF, 0x00, 0x00]); + + return MP4.box(MP4.types['.mp3'], data); + } + }, { + key: 'mp4a', + value: function mp4a(meta) { + var channelCount = meta.channelCount; + var sampleRate = meta.audioSampleRate; + + var data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // reserved(4) + 0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2) + 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, channelCount, // channelCount(2) + 0x00, 0x10, // sampleSize(2) + 0x00, 0x00, 0x00, 0x00, // reserved(4) + sampleRate >>> 8 & 0xFF, // Audio sample rate + sampleRate & 0xFF, 0x00, 0x00]); + + return MP4.box(MP4.types.mp4a, data, MP4.esds(meta)); + } + }, { + key: 'esds', + value: function esds(meta) { + var config = meta.config || []; + var configSize = config.length; + var data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version 0 + flags + + 0x03, // descriptor_type + 0x17 + configSize, // length3 + 0x00, 0x01, // es_id + 0x00, // stream_priority + + 0x04, // descriptor_type + 0x0F + configSize, // length + 0x40, // codec: mpeg4_audio + 0x15, // stream_type: Audio + 0x00, 0x00, 0x00, // buffer_size + 0x00, 0x00, 0x00, 0x00, // maxBitrate + 0x00, 0x00, 0x00, 0x00, // avgBitrate + + 0x05 // descriptor_type + ].concat([configSize]).concat(config).concat([0x06, 0x01, 0x02 // GASpecificConfig + ])); + return MP4.box(MP4.types.esds, data); + } + }, { + key: 'avc1', + value: function avc1(meta) { + var avcc = meta.avcc; + var width = meta.codecWidth, + height = meta.codecHeight; + + var data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // reserved(4) + 0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2) + 0x00, 0x00, 0x00, 0x00, // pre_defined(2) + reserved(2) + 0x00, 0x00, 0x00, 0x00, // pre_defined: 3 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, width >>> 8 & 0xFF, // width: 2 bytes + width & 0xFF, height >>> 8 & 0xFF, // height: 2 bytes + height & 0xFF, 0x00, 0x48, 0x00, 0x00, // horizresolution: 4 bytes + 0x00, 0x48, 0x00, 0x00, // vertresolution: 4 bytes + 0x00, 0x00, 0x00, 0x00, // reserved: 4 bytes + 0x00, 0x01, // frame_count + 0x0A, // strlen + 0x78, 0x71, 0x71, 0x2F, // compressorname: 32 bytes + 0x66, 0x6C, 0x76, 0x2E, 0x6A, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, // depth + 0xFF, 0xFF // pre_defined = -1 + ]); + return MP4.box(MP4.types.avc1, data, MP4.box(MP4.types.avcC, avcc)); + } + + // Movie Extends box + + }, { + key: 'mvex', + value: function mvex(meta) { + return MP4.box(MP4.types.mvex, MP4.trex(meta)); + } + + // Track Extends box + + }, { + key: 'trex', + value: function trex(meta) { + var trackId = meta.id; + var data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + trackId >>> 24 & 0xFF, // track_ID + trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF, 0x00, 0x00, 0x00, 0x01, // default_sample_description_index + 0x00, 0x00, 0x00, 0x00, // default_sample_duration + 0x00, 0x00, 0x00, 0x00, // default_sample_size + 0x00, 0x01, 0x00, 0x01 // default_sample_flags + ]); + return MP4.box(MP4.types.trex, data); + } + + // Movie fragment box + + }, { + key: 'moof', + value: function moof(track, baseMediaDecodeTime) { + return MP4.box(MP4.types.moof, MP4.mfhd(track.sequenceNumber), MP4.traf(track, baseMediaDecodeTime)); + } + }, { + key: 'mfhd', + value: function mfhd(sequenceNumber) { + var data = new Uint8Array([0x00, 0x00, 0x00, 0x00, sequenceNumber >>> 24 & 0xFF, // sequence_number: int32 + sequenceNumber >>> 16 & 0xFF, sequenceNumber >>> 8 & 0xFF, sequenceNumber & 0xFF]); + return MP4.box(MP4.types.mfhd, data); + } + + // Track fragment box + + }, { + key: 'traf', + value: function traf(track, baseMediaDecodeTime) { + var trackId = track.id; + + // Track fragment header box + var tfhd = MP4.box(MP4.types.tfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) & flags + trackId >>> 24 & 0xFF, // track_ID + trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF])); + // Track Fragment Decode Time + var tfdt = MP4.box(MP4.types.tfdt, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) & flags + baseMediaDecodeTime >>> 24 & 0xFF, // baseMediaDecodeTime: int32 + baseMediaDecodeTime >>> 16 & 0xFF, baseMediaDecodeTime >>> 8 & 0xFF, baseMediaDecodeTime & 0xFF])); + var sdtp = MP4.sdtp(track); + var trun = MP4.trun(track, sdtp.byteLength + 16 + 16 + 8 + 16 + 8 + 8); + + return MP4.box(MP4.types.traf, tfhd, tfdt, trun, sdtp); + } + + // Sample Dependency Type box + + }, { + key: 'sdtp', + value: function sdtp(track) { + var samples = track.samples || []; + var sampleCount = samples.length; + var data = new Uint8Array(4 + sampleCount); + // 0~4 bytes: version(0) & flags + for (var i = 0; i < sampleCount; i++) { + var flags = samples[i].flags; + data[i + 4] = flags.isLeading << 6 | // is_leading: 2 (bit) + flags.dependsOn << 4 // sample_depends_on + | flags.isDependedOn << 2 // sample_is_depended_on + | flags.hasRedundancy; // sample_has_redundancy + } + return MP4.box(MP4.types.sdtp, data); + } + + // Track fragment run box + + }, { + key: 'trun', + value: function trun(track, offset) { + var samples = track.samples || []; + var sampleCount = samples.length; + var dataSize = 12 + 16 * sampleCount; + var data = new Uint8Array(dataSize); + offset += 8 + dataSize; + + data.set([0x00, 0x00, 0x0F, 0x01, // version(0) & flags + sampleCount >>> 24 & 0xFF, // sample_count + sampleCount >>> 16 & 0xFF, sampleCount >>> 8 & 0xFF, sampleCount & 0xFF, offset >>> 24 & 0xFF, // data_offset + offset >>> 16 & 0xFF, offset >>> 8 & 0xFF, offset & 0xFF], 0); + + for (var i = 0; i < sampleCount; i++) { + var duration = samples[i].duration; + var size = samples[i].size; + var flags = samples[i].flags; + var cts = samples[i].cts; + data.set([duration >>> 24 & 0xFF, // sample_duration + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, size >>> 24 & 0xFF, // sample_size + size >>> 16 & 0xFF, size >>> 8 & 0xFF, size & 0xFF, flags.isLeading << 2 | flags.dependsOn, // sample_flags + flags.isDependedOn << 6 | flags.hasRedundancy << 4 | flags.isNonSync, 0x00, 0x00, // sample_degradation_priority + cts >>> 24 & 0xFF, // sample_composition_time_offset + cts >>> 16 & 0xFF, cts >>> 8 & 0xFF, cts & 0xFF], 12 + 16 * i); + } + return MP4.box(MP4.types.trun, data); + } + }, { + key: 'mdat', + value: function mdat(data) { + return MP4.box(MP4.types.mdat, data); + } + }]); + + return MP4; +}(); + +MP4.init(); + +exports.default = MP4; + +},{}],38:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _logger = _dereq_('../utils/logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +var _mp4Generator = _dereq_('./mp4-generator.js'); + +var _mp4Generator2 = _interopRequireDefault(_mp4Generator); + +var _aacSilent = _dereq_('./aac-silent.js'); + +var _aacSilent2 = _interopRequireDefault(_aacSilent); + +var _browser = _dereq_('../utils/browser.js'); + +var _browser2 = _interopRequireDefault(_browser); + +var _mediaSegmentInfo = _dereq_('../core/media-segment-info.js'); + +var _exception = _dereq_('../utils/exception.js'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +// Fragmented mp4 remuxer +var MP4Remuxer = function () { + function MP4Remuxer(config) { + _classCallCheck(this, MP4Remuxer); + + this.TAG = 'MP4Remuxer'; + + this._config = config; + this._isLive = config.isLive === true ? true : false; + + this._dtsBase = -1; + this._dtsBaseInited = false; + this._audioDtsBase = Infinity; + this._videoDtsBase = Infinity; + this._audioNextDts = undefined; + this._videoNextDts = undefined; + this._audioStashedLastSample = null; + this._videoStashedLastSample = null; + + this._audioMeta = null; + this._videoMeta = null; + + this._audioSegmentInfoList = new _mediaSegmentInfo.MediaSegmentInfoList('audio'); + this._videoSegmentInfoList = new _mediaSegmentInfo.MediaSegmentInfoList('video'); + + this._onInitSegment = null; + this._onMediaSegment = null; + + // Workaround for chrome < 50: Always force first sample as a Random Access Point in media segment + // see https://bugs.chromium.org/p/chromium/issues/detail?id=229412 + this._forceFirstIDR = _browser2.default.chrome && (_browser2.default.version.major < 50 || _browser2.default.version.major === 50 && _browser2.default.version.build < 2661) ? true : false; + + // Workaround for IE11/Edge: Fill silent aac frame after keyframe-seeking + // Make audio beginDts equals with video beginDts, in order to fix seek freeze + this._fillSilentAfterSeek = _browser2.default.msedge || _browser2.default.msie; + + // While only FireFox supports 'audio/mp4, codecs="mp3"', use 'audio/mpeg' for chrome, safari, ... + this._mp3UseMpegAudio = !_browser2.default.firefox; + + this._fillAudioTimestampGap = this._config.fixAudioTimestampGap; + } + + _createClass(MP4Remuxer, [{ + key: 'destroy', + value: function destroy() { + this._dtsBase = -1; + this._dtsBaseInited = false; + this._audioMeta = null; + this._videoMeta = null; + this._audioSegmentInfoList.clear(); + this._audioSegmentInfoList = null; + this._videoSegmentInfoList.clear(); + this._videoSegmentInfoList = null; + this._onInitSegment = null; + this._onMediaSegment = null; + } + }, { + key: 'bindDataSource', + value: function bindDataSource(producer) { + producer.onDataAvailable = this.remux.bind(this); + producer.onTrackMetadata = this._onTrackMetadataReceived.bind(this); + return this; + } + + /* prototype: function onInitSegment(type: string, initSegment: ArrayBuffer): void + InitSegment: { + type: string, + data: ArrayBuffer, + codec: string, + container: string + } + */ + + }, { + key: 'insertDiscontinuity', + value: function insertDiscontinuity() { + this._audioNextDts = this._videoNextDts = undefined; + } + }, { + key: 'seek', + value: function seek(originalDts) { + this._audioStashedLastSample = null; + this._videoStashedLastSample = null; + this._videoSegmentInfoList.clear(); + this._audioSegmentInfoList.clear(); + } + }, { + key: 'remux', + value: function remux(audioTrack, videoTrack) { + if (!this._onMediaSegment) { + throw new _exception.IllegalStateException('MP4Remuxer: onMediaSegment callback must be specificed!'); + } + if (!this._dtsBaseInited) { + this._calculateDtsBase(audioTrack, videoTrack); + } + this._remuxVideo(videoTrack); + this._remuxAudio(audioTrack); + } + }, { + key: '_onTrackMetadataReceived', + value: function _onTrackMetadataReceived(type, metadata) { + var metabox = null; + + var container = 'mp4'; + var codec = metadata.codec; + + if (type === 'audio') { + this._audioMeta = metadata; + if (metadata.codec === 'mp3' && this._mp3UseMpegAudio) { + // 'audio/mpeg' for MP3 audio track + container = 'mpeg'; + codec = ''; + metabox = new Uint8Array(); + } else { + // 'audio/mp4, codecs="codec"' + metabox = _mp4Generator2.default.generateInitSegment(metadata); + } + } else if (type === 'video') { + this._videoMeta = metadata; + metabox = _mp4Generator2.default.generateInitSegment(metadata); + } else { + return; + } + + // dispatch metabox (Initialization Segment) + if (!this._onInitSegment) { + throw new _exception.IllegalStateException('MP4Remuxer: onInitSegment callback must be specified!'); + } + this._onInitSegment(type, { + type: type, + data: metabox.buffer, + codec: codec, + container: type + '/' + container, + mediaDuration: metadata.duration // in timescale 1000 (milliseconds) + }); + } + }, { + key: '_calculateDtsBase', + value: function _calculateDtsBase(audioTrack, videoTrack) { + if (this._dtsBaseInited) { + return; + } + + if (audioTrack.samples && audioTrack.samples.length) { + this._audioDtsBase = audioTrack.samples[0].dts; + } + if (videoTrack.samples && videoTrack.samples.length) { + this._videoDtsBase = videoTrack.samples[0].dts; + } + + this._dtsBase = Math.min(this._audioDtsBase, this._videoDtsBase); + this._dtsBaseInited = true; + } + }, { + key: 'flushStashedSamples', + value: function flushStashedSamples() { + var videoSample = this._videoStashedLastSample; + var audioSample = this._audioStashedLastSample; + + var videoTrack = { + type: 'video', + id: 1, + sequenceNumber: 0, + samples: [], + length: 0 + }; + + if (videoSample != null) { + videoTrack.samples.push(videoSample); + videoTrack.length = videoSample.length; + } + + var audioTrack = { + type: 'audio', + id: 2, + sequenceNumber: 0, + samples: [], + length: 0 + }; + + if (audioSample != null) { + audioTrack.samples.push(audioSample); + audioTrack.length = audioSample.length; + } + + this._videoStashedLastSample = null; + this._audioStashedLastSample = null; + + this._remuxVideo(videoTrack, true); + this._remuxAudio(audioTrack, true); + } + }, { + key: '_remuxAudio', + value: function _remuxAudio(audioTrack, force) { + if (this._audioMeta == null) { + return; + } + + var track = audioTrack; + var samples = track.samples; + var dtsCorrection = undefined; + var firstDts = -1, + lastDts = -1, + lastPts = -1; + var refSampleDuration = this._audioMeta.refSampleDuration; + + var mpegRawTrack = this._audioMeta.codec === 'mp3' && this._mp3UseMpegAudio; + var firstSegmentAfterSeek = this._dtsBaseInited && this._audioNextDts === undefined; + + var insertPrefixSilentFrame = false; + + if (!samples || samples.length === 0) { + return; + } + if (samples.length === 1 && !force) { + // If [sample count in current batch] === 1 && (force != true) + // Ignore and keep in demuxer's queue + return; + } // else if (force === true) do remux + + var offset = 0; + var mdatbox = null; + var mdatBytes = 0; + + // calculate initial mdat size + if (mpegRawTrack) { + // for raw mpeg buffer + offset = 0; + mdatBytes = track.length; + } else { + // for fmp4 mdat box + offset = 8; // size + type + mdatBytes = 8 + track.length; + } + + var lastSample = null; + + // Pop the lastSample and waiting for stash + if (samples.length > 1) { + lastSample = samples.pop(); + mdatBytes -= lastSample.length; + } + + // Insert [stashed lastSample in the previous batch] to the front + if (this._audioStashedLastSample != null) { + var sample = this._audioStashedLastSample; + this._audioStashedLastSample = null; + samples.unshift(sample); + mdatBytes += sample.length; + } + + // Stash the lastSample of current batch, waiting for next batch + if (lastSample != null) { + this._audioStashedLastSample = lastSample; + } + + var firstSampleOriginalDts = samples[0].dts - this._dtsBase; + + // calculate dtsCorrection + if (this._audioNextDts) { + dtsCorrection = firstSampleOriginalDts - this._audioNextDts; + } else { + // this._audioNextDts == undefined + if (this._audioSegmentInfoList.isEmpty()) { + dtsCorrection = 0; + if (this._fillSilentAfterSeek && !this._videoSegmentInfoList.isEmpty()) { + if (this._audioMeta.originalCodec !== 'mp3') { + insertPrefixSilentFrame = true; + } + } + } else { + var _lastSample = this._audioSegmentInfoList.getLastSampleBefore(firstSampleOriginalDts); + if (_lastSample != null) { + var distance = firstSampleOriginalDts - (_lastSample.originalDts + _lastSample.duration); + if (distance <= 3) { + distance = 0; + } + var expectedDts = _lastSample.dts + _lastSample.duration + distance; + dtsCorrection = firstSampleOriginalDts - expectedDts; + } else { + // lastSample == null, cannot found + dtsCorrection = 0; + } + } + } + + if (insertPrefixSilentFrame) { + // align audio segment beginDts to match with current video segment's beginDts + var firstSampleDts = firstSampleOriginalDts - dtsCorrection; + var videoSegment = this._videoSegmentInfoList.getLastSegmentBefore(firstSampleOriginalDts); + if (videoSegment != null && videoSegment.beginDts < firstSampleDts) { + var silentUnit = _aacSilent2.default.getSilentFrame(this._audioMeta.originalCodec, this._audioMeta.channelCount); + if (silentUnit) { + var dts = videoSegment.beginDts; + var silentFrameDuration = firstSampleDts - videoSegment.beginDts; + _logger2.default.v(this.TAG, 'InsertPrefixSilentAudio: dts: ' + dts + ', duration: ' + silentFrameDuration); + samples.unshift({ unit: silentUnit, dts: dts, pts: dts }); + mdatBytes += silentUnit.byteLength; + } // silentUnit == null: Cannot generate, skip + } else { + insertPrefixSilentFrame = false; + } + } + + var mp4Samples = []; + + // Correct dts for each sample, and calculate sample duration. Then output to mp4Samples + for (var i = 0; i < samples.length; i++) { + var _sample = samples[i]; + var unit = _sample.unit; + var originalDts = _sample.dts - this._dtsBase; + var _dts = originalDts - dtsCorrection; + + if (firstDts === -1) { + firstDts = _dts; + } + + var sampleDuration = 0; + + if (i !== samples.length - 1) { + var nextDts = samples[i + 1].dts - this._dtsBase - dtsCorrection; + sampleDuration = nextDts - _dts; + } else { + // the last sample + if (lastSample != null) { + // use stashed sample's dts to calculate sample duration + var _nextDts = lastSample.dts - this._dtsBase - dtsCorrection; + sampleDuration = _nextDts - _dts; + } else if (mp4Samples.length >= 1) { + // use second last sample duration + sampleDuration = mp4Samples[mp4Samples.length - 1].duration; + } else { + // the only one sample, use reference sample duration + sampleDuration = Math.floor(refSampleDuration); + } + } + + var needFillSilentFrames = false; + var silentFrames = null; + + // Silent frame generation, if large timestamp gap detected && config.fixAudioTimestampGap + if (sampleDuration > refSampleDuration * 1.5 && this._audioMeta.codec !== 'mp3' && this._fillAudioTimestampGap && !_browser2.default.safari) { + // We need to insert silent frames to fill timestamp gap + needFillSilentFrames = true; + var delta = Math.abs(sampleDuration - refSampleDuration); + var frameCount = Math.ceil(delta / refSampleDuration); + var currentDts = _dts + refSampleDuration; // Notice: in float + + _logger2.default.w(this.TAG, 'Large audio timestamp gap detected, may cause AV sync to drift. ' + 'Silent frames will be generated to avoid unsync.\n' + ('dts: ' + (_dts + sampleDuration) + ' ms, expected: ' + (_dts + Math.round(refSampleDuration)) + ' ms, ') + ('delta: ' + Math.round(delta) + ' ms, generate: ' + frameCount + ' frames')); + + var _silentUnit = _aacSilent2.default.getSilentFrame(this._audioMeta.originalCodec, this._audioMeta.channelCount); + if (_silentUnit == null) { + _logger2.default.w(this.TAG, 'Unable to generate silent frame for ' + (this._audioMeta.originalCodec + ' with ' + this._audioMeta.channelCount + ' channels, repeat last frame')); + // Repeat last frame + _silentUnit = unit; + } + silentFrames = []; + + for (var j = 0; j < frameCount; j++) { + var intDts = Math.round(currentDts); // round to integer + if (silentFrames.length > 0) { + // Set previous frame sample duration + var previousFrame = silentFrames[silentFrames.length - 1]; + previousFrame.duration = intDts - previousFrame.dts; + } + var frame = { + dts: intDts, + pts: intDts, + cts: 0, + unit: _silentUnit, + size: _silentUnit.byteLength, + duration: 0, // wait for next sample + originalDts: originalDts, + flags: { + isLeading: 0, + dependsOn: 1, + isDependedOn: 0, + hasRedundancy: 0 + } + }; + silentFrames.push(frame); + mdatBytes += frame.size; + currentDts += refSampleDuration; + } + + // last frame: align end time to next frame dts + var lastFrame = silentFrames[silentFrames.length - 1]; + lastFrame.duration = _dts + sampleDuration - lastFrame.dts; + + // silentFrames.forEach((frame) => { + // Log.w(this.TAG, `SilentAudio: dts: ${frame.dts}, duration: ${frame.duration}`); + // }); + + // Set correct sample duration for current frame + sampleDuration = Math.round(refSampleDuration); + } + + mp4Samples.push({ + dts: _dts, + pts: _dts, + cts: 0, + unit: _sample.unit, + size: _sample.unit.byteLength, + duration: sampleDuration, + originalDts: originalDts, + flags: { + isLeading: 0, + dependsOn: 1, + isDependedOn: 0, + hasRedundancy: 0 + } + }); + + if (needFillSilentFrames) { + // Silent frames should be inserted after wrong-duration frame + mp4Samples.push.apply(mp4Samples, silentFrames); + } + } + + // allocate mdatbox + if (mpegRawTrack) { + // allocate for raw mpeg buffer + mdatbox = new Uint8Array(mdatBytes); + } else { + // allocate for fmp4 mdat box + mdatbox = new Uint8Array(mdatBytes); + // size field + mdatbox[0] = mdatBytes >>> 24 & 0xFF; + mdatbox[1] = mdatBytes >>> 16 & 0xFF; + mdatbox[2] = mdatBytes >>> 8 & 0xFF; + mdatbox[3] = mdatBytes & 0xFF; + // type field (fourCC) + mdatbox.set(_mp4Generator2.default.types.mdat, 4); + } + + // Write samples into mdatbox + for (var _i = 0; _i < mp4Samples.length; _i++) { + var _unit = mp4Samples[_i].unit; + mdatbox.set(_unit, offset); + offset += _unit.byteLength; + } + + var latest = mp4Samples[mp4Samples.length - 1]; + lastDts = latest.dts + latest.duration; + this._audioNextDts = lastDts; + + // fill media segment info & add to info list + var info = new _mediaSegmentInfo.MediaSegmentInfo(); + info.beginDts = firstDts; + info.endDts = lastDts; + info.beginPts = firstDts; + info.endPts = lastDts; + info.originalBeginDts = mp4Samples[0].originalDts; + info.originalEndDts = latest.originalDts + latest.duration; + info.firstSample = new _mediaSegmentInfo.SampleInfo(mp4Samples[0].dts, mp4Samples[0].pts, mp4Samples[0].duration, mp4Samples[0].originalDts, false); + info.lastSample = new _mediaSegmentInfo.SampleInfo(latest.dts, latest.pts, latest.duration, latest.originalDts, false); + if (!this._isLive) { + this._audioSegmentInfoList.append(info); + } + + track.samples = mp4Samples; + track.sequenceNumber++; + + var moofbox = null; + + if (mpegRawTrack) { + // Generate empty buffer, because useless for raw mpeg + moofbox = new Uint8Array(); + } else { + // Generate moof for fmp4 segment + moofbox = _mp4Generator2.default.moof(track, firstDts); + } + + track.samples = []; + track.length = 0; + + var segment = { + type: 'audio', + data: this._mergeBoxes(moofbox, mdatbox).buffer, + sampleCount: mp4Samples.length, + info: info + }; + + if (mpegRawTrack && firstSegmentAfterSeek) { + // For MPEG audio stream in MSE, if seeking occurred, before appending new buffer + // We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer. + segment.timestampOffset = firstDts; + } + + this._onMediaSegment('audio', segment); + } + }, { + key: '_remuxVideo', + value: function _remuxVideo(videoTrack, force) { + if (this._videoMeta == null) { + return; + } + + var track = videoTrack; + var samples = track.samples; + var dtsCorrection = undefined; + var firstDts = -1, + lastDts = -1; + var firstPts = -1, + lastPts = -1; + + if (!samples || samples.length === 0) { + return; + } + if (samples.length === 1 && !force) { + // If [sample count in current batch] === 1 && (force != true) + // Ignore and keep in demuxer's queue + return; + } // else if (force === true) do remux + + var offset = 8; + var mdatbox = null; + var mdatBytes = 8 + videoTrack.length; + + var lastSample = null; + + // Pop the lastSample and waiting for stash + if (samples.length > 1) { + lastSample = samples.pop(); + mdatBytes -= lastSample.length; + } + + // Insert [stashed lastSample in the previous batch] to the front + if (this._videoStashedLastSample != null) { + var sample = this._videoStashedLastSample; + this._videoStashedLastSample = null; + samples.unshift(sample); + mdatBytes += sample.length; + } + + // Stash the lastSample of current batch, waiting for next batch + if (lastSample != null) { + this._videoStashedLastSample = lastSample; + } + + var firstSampleOriginalDts = samples[0].dts - this._dtsBase; + + // calculate dtsCorrection + if (this._videoNextDts) { + dtsCorrection = firstSampleOriginalDts - this._videoNextDts; + } else { + // this._videoNextDts == undefined + if (this._videoSegmentInfoList.isEmpty()) { + dtsCorrection = 0; + } else { + var _lastSample2 = this._videoSegmentInfoList.getLastSampleBefore(firstSampleOriginalDts); + if (_lastSample2 != null) { + var distance = firstSampleOriginalDts - (_lastSample2.originalDts + _lastSample2.duration); + if (distance <= 3) { + distance = 0; + } + var expectedDts = _lastSample2.dts + _lastSample2.duration + distance; + dtsCorrection = firstSampleOriginalDts - expectedDts; + } else { + // lastSample == null, cannot found + dtsCorrection = 0; + } + } + } + + var info = new _mediaSegmentInfo.MediaSegmentInfo(); + var mp4Samples = []; + + // Correct dts for each sample, and calculate sample duration. Then output to mp4Samples + for (var i = 0; i < samples.length; i++) { + var _sample2 = samples[i]; + var originalDts = _sample2.dts - this._dtsBase; + var isKeyframe = _sample2.isKeyframe; + var dts = originalDts - dtsCorrection; + var cts = _sample2.cts; + var pts = dts + cts; + + if (firstDts === -1) { + firstDts = dts; + firstPts = pts; + } + + var sampleDuration = 0; + + if (i !== samples.length - 1) { + var nextDts = samples[i + 1].dts - this._dtsBase - dtsCorrection; + sampleDuration = nextDts - dts; + } else { + // the last sample + if (lastSample != null) { + // use stashed sample's dts to calculate sample duration + var _nextDts2 = lastSample.dts - this._dtsBase - dtsCorrection; + sampleDuration = _nextDts2 - dts; + } else if (mp4Samples.length >= 1) { + // use second last sample duration + sampleDuration = mp4Samples[mp4Samples.length - 1].duration; + } else { + // the only one sample, use reference sample duration + sampleDuration = Math.floor(this._videoMeta.refSampleDuration); + } + } + + if (isKeyframe) { + var syncPoint = new _mediaSegmentInfo.SampleInfo(dts, pts, sampleDuration, _sample2.dts, true); + syncPoint.fileposition = _sample2.fileposition; + info.appendSyncPoint(syncPoint); + } + + mp4Samples.push({ + dts: dts, + pts: pts, + cts: cts, + units: _sample2.units, + size: _sample2.length, + isKeyframe: isKeyframe, + duration: sampleDuration, + originalDts: originalDts, + flags: { + isLeading: 0, + dependsOn: isKeyframe ? 2 : 1, + isDependedOn: isKeyframe ? 1 : 0, + hasRedundancy: 0, + isNonSync: isKeyframe ? 0 : 1 + } + }); + } + + // allocate mdatbox + mdatbox = new Uint8Array(mdatBytes); + mdatbox[0] = mdatBytes >>> 24 & 0xFF; + mdatbox[1] = mdatBytes >>> 16 & 0xFF; + mdatbox[2] = mdatBytes >>> 8 & 0xFF; + mdatbox[3] = mdatBytes & 0xFF; + mdatbox.set(_mp4Generator2.default.types.mdat, 4); + + // Write samples into mdatbox + for (var _i2 = 0; _i2 < mp4Samples.length; _i2++) { + var units = mp4Samples[_i2].units; + while (units.length) { + var unit = units.shift(); + var data = unit.data; + mdatbox.set(data, offset); + offset += data.byteLength; + } + } + + var latest = mp4Samples[mp4Samples.length - 1]; + lastDts = latest.dts + latest.duration; + lastPts = latest.pts + latest.duration; + this._videoNextDts = lastDts; + + // fill media segment info & add to info list + info.beginDts = firstDts; + info.endDts = lastDts; + info.beginPts = firstPts; + info.endPts = lastPts; + info.originalBeginDts = mp4Samples[0].originalDts; + info.originalEndDts = latest.originalDts + latest.duration; + info.firstSample = new _mediaSegmentInfo.SampleInfo(mp4Samples[0].dts, mp4Samples[0].pts, mp4Samples[0].duration, mp4Samples[0].originalDts, mp4Samples[0].isKeyframe); + info.lastSample = new _mediaSegmentInfo.SampleInfo(latest.dts, latest.pts, latest.duration, latest.originalDts, latest.isKeyframe); + if (!this._isLive) { + this._videoSegmentInfoList.append(info); + } + + track.samples = mp4Samples; + track.sequenceNumber++; + + // workaround for chrome < 50: force first sample as a random access point + // see https://bugs.chromium.org/p/chromium/issues/detail?id=229412 + if (this._forceFirstIDR) { + var flags = mp4Samples[0].flags; + flags.dependsOn = 2; + flags.isNonSync = 0; + } + + var moofbox = _mp4Generator2.default.moof(track, firstDts); + track.samples = []; + track.length = 0; + + this._onMediaSegment('video', { + type: 'video', + data: this._mergeBoxes(moofbox, mdatbox).buffer, + sampleCount: mp4Samples.length, + info: info + }); + } + }, { + key: '_mergeBoxes', + value: function _mergeBoxes(moof, mdat) { + var result = new Uint8Array(moof.byteLength + mdat.byteLength); + result.set(moof, 0); + result.set(mdat, moof.byteLength); + return result; + } + }, { + key: 'onInitSegment', + get: function get() { + return this._onInitSegment; + }, + set: function set(callback) { + this._onInitSegment = callback; + } + + /* prototype: function onMediaSegment(type: string, mediaSegment: MediaSegment): void + MediaSegment: { + type: string, + data: ArrayBuffer, + sampleCount: int32 + info: MediaSegmentInfo + } + */ + + }, { + key: 'onMediaSegment', + get: function get() { + return this._onMediaSegment; + }, + set: function set(callback) { + this._onMediaSegment = callback; + } + }]); + + return MP4Remuxer; +}(); + +exports.default = MP4Remuxer; + +},{"../core/media-segment-info.js":8,"../utils/browser.js":39,"../utils/exception.js":40,"../utils/logger.js":41,"./aac-silent.js":36,"./mp4-generator.js":37}],39:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var Browser = {}; + +function detect() { + // modified from jquery-browser-plugin + + var ua = self.navigator.userAgent.toLowerCase(); + + var match = /(edge)\/([\w.]+)/.exec(ua) || /(opr)[\/]([\w.]+)/.exec(ua) || /(chrome)[ \/]([\w.]+)/.exec(ua) || /(iemobile)[\/]([\w.]+)/.exec(ua) || /(version)(applewebkit)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) || /(webkit)[ \/]([\w.]+).*(version)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) || /(webkit)[ \/]([\w.]+)/.exec(ua) || /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || /(msie) ([\w.]+)/.exec(ua) || ua.indexOf('trident') >= 0 && /(rv)(?::| )([\w.]+)/.exec(ua) || ua.indexOf('compatible') < 0 && /(firefox)[ \/]([\w.]+)/.exec(ua) || []; + + var platform_match = /(ipad)/.exec(ua) || /(ipod)/.exec(ua) || /(windows phone)/.exec(ua) || /(iphone)/.exec(ua) || /(kindle)/.exec(ua) || /(android)/.exec(ua) || /(windows)/.exec(ua) || /(mac)/.exec(ua) || /(linux)/.exec(ua) || /(cros)/.exec(ua) || []; + + var matched = { + browser: match[5] || match[3] || match[1] || '', + version: match[2] || match[4] || '0', + majorVersion: match[4] || match[2] || '0', + platform: platform_match[0] || '' + }; + + var browser = {}; + if (matched.browser) { + browser[matched.browser] = true; + + var versionArray = matched.majorVersion.split('.'); + browser.version = { + major: parseInt(matched.majorVersion, 10), + string: matched.version + }; + if (versionArray.length > 1) { + browser.version.minor = parseInt(versionArray[1], 10); + } + if (versionArray.length > 2) { + browser.version.build = parseInt(versionArray[2], 10); + } + } + + if (matched.platform) { + browser[matched.platform] = true; + } + + if (browser.chrome || browser.opr || browser.safari) { + browser.webkit = true; + } + + // MSIE. IE11 has 'rv' identifer + if (browser.rv || browser.iemobile) { + if (browser.rv) { + delete browser.rv; + } + var msie = 'msie'; + matched.browser = msie; + browser[msie] = true; + } + + // Microsoft Edge + if (browser.edge) { + delete browser.edge; + var msedge = 'msedge'; + matched.browser = msedge; + browser[msedge] = true; + } + + // Opera 15+ + if (browser.opr) { + var opera = 'opera'; + matched.browser = opera; + browser[opera] = true; + } + + // Stock android browsers are marked as Safari + if (browser.safari && browser.android) { + var android = 'android'; + matched.browser = android; + browser[android] = true; + } + + browser.name = matched.browser; + browser.platform = matched.platform; + + for (var key in Browser) { + if (Browser.hasOwnProperty(key)) { + delete Browser[key]; + } + } + Object.assign(Browser, browser); +} + +detect(); + +exports.default = Browser; + +},{}],40:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var RuntimeException = exports.RuntimeException = function () { + function RuntimeException(message) { + _classCallCheck(this, RuntimeException); + + this._message = message; + } + + _createClass(RuntimeException, [{ + key: 'toString', + value: function toString() { + return this.name + ': ' + this.message; + } + }, { + key: 'name', + get: function get() { + return 'RuntimeException'; + } + }, { + key: 'message', + get: function get() { + return this._message; + } + }]); + + return RuntimeException; +}(); + +var IllegalStateException = exports.IllegalStateException = function (_RuntimeException) { + _inherits(IllegalStateException, _RuntimeException); + + function IllegalStateException(message) { + _classCallCheck(this, IllegalStateException); + + return _possibleConstructorReturn(this, (IllegalStateException.__proto__ || Object.getPrototypeOf(IllegalStateException)).call(this, message)); + } + + _createClass(IllegalStateException, [{ + key: 'name', + get: function get() { + return 'IllegalStateException'; + } + }]); + + return IllegalStateException; +}(RuntimeException); + +var InvalidArgumentException = exports.InvalidArgumentException = function (_RuntimeException2) { + _inherits(InvalidArgumentException, _RuntimeException2); + + function InvalidArgumentException(message) { + _classCallCheck(this, InvalidArgumentException); + + return _possibleConstructorReturn(this, (InvalidArgumentException.__proto__ || Object.getPrototypeOf(InvalidArgumentException)).call(this, message)); + } + + _createClass(InvalidArgumentException, [{ + key: 'name', + get: function get() { + return 'InvalidArgumentException'; + } + }]); + + return InvalidArgumentException; +}(RuntimeException); + +var NotImplementedException = exports.NotImplementedException = function (_RuntimeException3) { + _inherits(NotImplementedException, _RuntimeException3); + + function NotImplementedException(message) { + _classCallCheck(this, NotImplementedException); + + return _possibleConstructorReturn(this, (NotImplementedException.__proto__ || Object.getPrototypeOf(NotImplementedException)).call(this, message)); + } + + _createClass(NotImplementedException, [{ + key: 'name', + get: function get() { + return 'NotImplementedException'; + } + }]); + + return NotImplementedException; +}(RuntimeException); + +},{}],41:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _events = _dereq_('events'); + +var _events2 = _interopRequireDefault(_events); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Log = function () { + function Log() { + _classCallCheck(this, Log); + } + + _createClass(Log, null, [{ + key: 'e', + value: function e(tag, msg) { + if (!tag || Log.FORCE_GLOBAL_TAG) tag = Log.GLOBAL_TAG; + + var str = '[' + tag + '] > ' + msg; + + if (Log.ENABLE_CALLBACK) { + Log.emitter.emit('log', 'error', str); + } + + if (!Log.ENABLE_ERROR) { + return; + } + + if (console.error) { + console.error(str); + } else if (console.warn) { + console.warn(str); + } else { + console.log(str); + } + } + }, { + key: 'i', + value: function i(tag, msg) { + if (!tag || Log.FORCE_GLOBAL_TAG) tag = Log.GLOBAL_TAG; + + var str = '[' + tag + '] > ' + msg; + + if (Log.ENABLE_CALLBACK) { + Log.emitter.emit('log', 'info', str); + } + + if (!Log.ENABLE_INFO) { + return; + } + + if (console.info) { + console.info(str); + } else { + console.log(str); + } + } + }, { + key: 'w', + value: function w(tag, msg) { + if (!tag || Log.FORCE_GLOBAL_TAG) tag = Log.GLOBAL_TAG; + + var str = '[' + tag + '] > ' + msg; + + if (Log.ENABLE_CALLBACK) { + Log.emitter.emit('log', 'warn', str); + } + + if (!Log.ENABLE_WARN) { + return; + } + + if (console.warn) { + console.warn(str); + } else { + console.log(str); + } + } + }, { + key: 'd', + value: function d(tag, msg) { + if (!tag || Log.FORCE_GLOBAL_TAG) tag = Log.GLOBAL_TAG; + + var str = '[' + tag + '] > ' + msg; + + if (Log.ENABLE_CALLBACK) { + Log.emitter.emit('log', 'debug', str); + } + + if (!Log.ENABLE_DEBUG) { + return; + } + + if (console.debug) { + console.debug(str); + } else { + console.log(str); + } + } + }, { + key: 'v', + value: function v(tag, msg) { + if (!tag || Log.FORCE_GLOBAL_TAG) tag = Log.GLOBAL_TAG; + + var str = '[' + tag + '] > ' + msg; + + if (Log.ENABLE_CALLBACK) { + Log.emitter.emit('log', 'verbose', str); + } + + if (!Log.ENABLE_VERBOSE) { + return; + } + + console.log(str); + } + }]); + + return Log; +}(); + +Log.GLOBAL_TAG = 'flv.js'; +Log.FORCE_GLOBAL_TAG = false; +Log.ENABLE_ERROR = true; +Log.ENABLE_INFO = true; +Log.ENABLE_WARN = true; +Log.ENABLE_DEBUG = true; +Log.ENABLE_VERBOSE = true; + +Log.ENABLE_CALLBACK = false; + +Log.emitter = new _events2.default(); + +exports.default = Log; + +},{"events":2}],42:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var _events = _dereq_('events'); + +var _events2 = _interopRequireDefault(_events); + +var _logger = _dereq_('./logger.js'); + +var _logger2 = _interopRequireDefault(_logger); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var LoggingControl = function () { + function LoggingControl() { + _classCallCheck(this, LoggingControl); + } + + _createClass(LoggingControl, null, [{ + key: 'getConfig', + value: function getConfig() { + return { + globalTag: _logger2.default.GLOBAL_TAG, + forceGlobalTag: _logger2.default.FORCE_GLOBAL_TAG, + enableVerbose: _logger2.default.ENABLE_VERBOSE, + enableDebug: _logger2.default.ENABLE_DEBUG, + enableInfo: _logger2.default.ENABLE_INFO, + enableWarn: _logger2.default.ENABLE_WARN, + enableError: _logger2.default.ENABLE_ERROR, + enableCallback: _logger2.default.ENABLE_CALLBACK + }; + } + }, { + key: 'applyConfig', + value: function applyConfig(config) { + _logger2.default.GLOBAL_TAG = config.globalTag; + _logger2.default.FORCE_GLOBAL_TAG = config.forceGlobalTag; + _logger2.default.ENABLE_VERBOSE = config.enableVerbose; + _logger2.default.ENABLE_DEBUG = config.enableDebug; + _logger2.default.ENABLE_INFO = config.enableInfo; + _logger2.default.ENABLE_WARN = config.enableWarn; + _logger2.default.ENABLE_ERROR = config.enableError; + _logger2.default.ENABLE_CALLBACK = config.enableCallback; + } + }, { + key: '_notifyChange', + value: function _notifyChange() { + var emitter = LoggingControl.emitter; + + if (emitter.listenerCount('change') > 0) { + var config = LoggingControl.getConfig(); + emitter.emit('change', config); + } + } + }, { + key: 'registerListener', + value: function registerListener(listener) { + LoggingControl.emitter.addListener('change', listener); + } + }, { + key: 'removeListener', + value: function removeListener(listener) { + LoggingControl.emitter.removeListener('change', listener); + } + }, { + key: 'addLogListener', + value: function addLogListener(listener) { + _logger2.default.emitter.addListener('log', listener); + if (_logger2.default.emitter.listenerCount('log') > 0) { + _logger2.default.ENABLE_CALLBACK = true; + LoggingControl._notifyChange(); + } + } + }, { + key: 'removeLogListener', + value: function removeLogListener(listener) { + _logger2.default.emitter.removeListener('log', listener); + if (_logger2.default.emitter.listenerCount('log') === 0) { + _logger2.default.ENABLE_CALLBACK = false; + LoggingControl._notifyChange(); + } + } + }, { + key: 'forceGlobalTag', + get: function get() { + return _logger2.default.FORCE_GLOBAL_TAG; + }, + set: function set(enable) { + _logger2.default.FORCE_GLOBAL_TAG = enable; + LoggingControl._notifyChange(); + } + }, { + key: 'globalTag', + get: function get() { + return _logger2.default.GLOBAL_TAG; + }, + set: function set(tag) { + _logger2.default.GLOBAL_TAG = tag; + LoggingControl._notifyChange(); + } + }, { + key: 'enableAll', + get: function get() { + return _logger2.default.ENABLE_VERBOSE && _logger2.default.ENABLE_DEBUG && _logger2.default.ENABLE_INFO && _logger2.default.ENABLE_WARN && _logger2.default.ENABLE_ERROR; + }, + set: function set(enable) { + _logger2.default.ENABLE_VERBOSE = enable; + _logger2.default.ENABLE_DEBUG = enable; + _logger2.default.ENABLE_INFO = enable; + _logger2.default.ENABLE_WARN = enable; + _logger2.default.ENABLE_ERROR = enable; + LoggingControl._notifyChange(); + } + }, { + key: 'enableDebug', + get: function get() { + return _logger2.default.ENABLE_DEBUG; + }, + set: function set(enable) { + _logger2.default.ENABLE_DEBUG = enable; + LoggingControl._notifyChange(); + } + }, { + key: 'enableVerbose', + get: function get() { + return _logger2.default.ENABLE_VERBOSE; + }, + set: function set(enable) { + _logger2.default.ENABLE_VERBOSE = enable; + LoggingControl._notifyChange(); + } + }, { + key: 'enableInfo', + get: function get() { + return _logger2.default.ENABLE_INFO; + }, + set: function set(enable) { + _logger2.default.ENABLE_INFO = enable; + LoggingControl._notifyChange(); + } + }, { + key: 'enableWarn', + get: function get() { + return _logger2.default.ENABLE_WARN; + }, + set: function set(enable) { + _logger2.default.ENABLE_WARN = enable; + LoggingControl._notifyChange(); + } + }, { + key: 'enableError', + get: function get() { + return _logger2.default.ENABLE_ERROR; + }, + set: function set(enable) { + _logger2.default.ENABLE_ERROR = enable; + LoggingControl._notifyChange(); + } + }]); + + return LoggingControl; +}(); + +LoggingControl.emitter = new _events2.default(); + +exports.default = LoggingControl; + +},{"./logger.js":41,"events":2}],43:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var Polyfill = function () { + function Polyfill() { + _classCallCheck(this, Polyfill); + } + + _createClass(Polyfill, null, [{ + key: 'install', + value: function install() { + // ES6 Object.setPrototypeOf + Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) { + obj.__proto__ = proto; + return obj; + }; + + // ES6 Object.assign + Object.assign = Object.assign || function (target) { + if (target === undefined || target === null) { + throw new TypeError('Cannot convert undefined or null to object'); + } + + var output = Object(target); + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + if (source !== undefined && source !== null) { + for (var key in source) { + if (source.hasOwnProperty(key)) { + output[key] = source[key]; + } + } + } + } + return output; + }; + + // ES6 Promise (missing support in IE11) + if (typeof self.Promise !== 'function') { + _dereq_('es6-promise').polyfill(); + } + } + }]); + + return Polyfill; +}(); + +Polyfill.install(); + +exports.default = Polyfill; + +},{"es6-promise":1}],44:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * This file is derived from C++ project libWinTF8 (https://github.com/m13253/libWinTF8) + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function checkContinuation(uint8array, start, checkLength) { + var array = uint8array; + if (start + checkLength < array.length) { + while (checkLength--) { + if ((array[++start] & 0xC0) !== 0x80) return false; + } + return true; + } else { + return false; + } +} + +function decodeUTF8(uint8array) { + var out = []; + var input = uint8array; + var i = 0; + var length = uint8array.length; + + while (i < length) { + if (input[i] < 0x80) { + out.push(String.fromCharCode(input[i])); + ++i; + continue; + } else if (input[i] < 0xC0) { + // fallthrough + } else if (input[i] < 0xE0) { + if (checkContinuation(input, i, 1)) { + var ucs4 = (input[i] & 0x1F) << 6 | input[i + 1] & 0x3F; + if (ucs4 >= 0x80) { + out.push(String.fromCharCode(ucs4 & 0xFFFF)); + i += 2; + continue; + } + } + } else if (input[i] < 0xF0) { + if (checkContinuation(input, i, 2)) { + var _ucs = (input[i] & 0xF) << 12 | (input[i + 1] & 0x3F) << 6 | input[i + 2] & 0x3F; + if (_ucs >= 0x800 && (_ucs & 0xF800) !== 0xD800) { + out.push(String.fromCharCode(_ucs & 0xFFFF)); + i += 3; + continue; + } + } + } else if (input[i] < 0xF8) { + if (checkContinuation(input, i, 3)) { + var _ucs2 = (input[i] & 0x7) << 18 | (input[i + 1] & 0x3F) << 12 | (input[i + 2] & 0x3F) << 6 | input[i + 3] & 0x3F; + if (_ucs2 > 0x10000 && _ucs2 < 0x110000) { + _ucs2 -= 0x10000; + out.push(String.fromCharCode(_ucs2 >>> 10 | 0xD800)); + out.push(String.fromCharCode(_ucs2 & 0x3FF | 0xDC00)); + i += 4; + continue; + } + } + } + out.push(String.fromCharCode(0xFFFD)); + ++i; + } + + return out.join(''); +} + +exports.default = decodeUTF8; + +},{}]},{},[21])(21) +}); + +//# sourceMappingURL=flv.js.map diff --git a/demos/flv/index.html b/demos/flv/index.html new file mode 100755 index 0000000..f899afa --- /dev/null +++ b/demos/flv/index.html @@ -0,0 +1,229 @@ + + + + + + flv.js demo + + + + + +
+
+
+
+ + + +
+
+ + + + + + + + +
+
+ +
+
+
+ +
+
+
+ + + + + + +
+ +
+ + + + + + + + \ No newline at end of file diff --git a/demos/rtsp/index.html b/demos/rtsp/index.html new file mode 100755 index 0000000..47170cd --- /dev/null +++ b/demos/rtsp/index.html @@ -0,0 +1,155 @@ + + + + + RTSP player example(based rtsp websockcet client) + + + +
+
+ + + +
+
+

Enter your ws link to the stream, for example: "ws://localhost:1554/ws/test/live1"

+
+ +
+
+ +
+
+ +

View HTML5 RTSP video player log

+
+ + + + +

+ + + + + + + + + + diff --git a/demos/rtsp/rtsp.dev.js b/demos/rtsp/rtsp.dev.js new file mode 100755 index 0000000..ad9b48b --- /dev/null +++ b/demos/rtsp/rtsp.dev.js @@ -0,0 +1,4879 @@ +(function () { + 'use strict'; + + // ERROR=0, WARN=1, LOG=2, DEBUG=3 + const LogLevel = { + Error: 0, + Warn: 1, + Log: 2, + Debug: 3 + }; + + let DEFAULT_LOG_LEVEL = LogLevel.Debug; + + function setDefaultLogLevel(level) { + DEFAULT_LOG_LEVEL = level; + } + class Logger { + constructor(level = DEFAULT_LOG_LEVEL, tag) { + this.tag = tag; + this.setLevel(level); + } + + setLevel(level) { + this.level = level; + } + + static get level_map() { return { + [LogLevel.Debug]:'log', + [LogLevel.Log]:'log', + [LogLevel.Warn]:'warn', + [LogLevel.Error]:'error' + }}; + + _log(lvl, args) { + args = Array.prototype.slice.call(args); + if (this.tag) { + args.unshift(`[${this.tag}]`); + } + if (this.level>=lvl) console[Logger.level_map[lvl]].apply(console, args); + } + log(){ + this._log(LogLevel.Log, arguments); + } + debug(){ + this._log(LogLevel.Debug, arguments); + } + error(){ + this._log(LogLevel.Error, arguments); + } + warn(){ + this._log(LogLevel.Warn, arguments); + } + } + + const taggedLoggers = new Map(); + function getTagged(tag) { + if (!taggedLoggers.has(tag)) { + taggedLoggers.set(tag, new Logger(DEFAULT_LOG_LEVEL, tag)); + } + return taggedLoggers.get(tag); + } + const Log = new Logger(); + + class Url { + static parse(url) { + let ret = {}; + + let regex = /^([^:]+):\/\/([^\/]+)(.*)$/; //protocol, login, urlpath + let result = regex.exec(url); + + if (!result) { + throw new Error("bad url"); + } + + ret.full = url; + ret.protocol = result[1]; + ret.urlpath = result[3]; + + let parts = ret.urlpath.split('/'); + ret.basename = parts.pop().split(/\?|#/)[0]; + ret.basepath = parts.join('/'); + + let loginSplit = result[2].split('@'); + let hostport = loginSplit[0].split(':'); + let userpass = [ null, null ]; + if (loginSplit.length === 2) { + userpass = loginSplit[0].split(':'); + hostport = loginSplit[1].split(':'); + } + + ret.user = userpass[0]; + ret.pass = userpass[1]; + ret.host = hostport[0]; + ret.auth = (ret.user && ret.pass) ? `${ret.user}:${ret.pass}` : ''; + + ret.port = (null == hostport[1]) ? Url.protocolDefaultPort(ret.protocol) : hostport[1]; + ret.portDefined = (null != hostport[1]); + ret.location = `${ret.host}:${ret.port}`; + + if (ret.protocol == 'unix') { + ret.socket = ret.port; + ret.port = undefined; + } + + return ret; + } + + static full(parsed) { + return `${parsed.protocol}://${parsed.location}/${parsed.urlpath}`; + } + + static isAbsolute(url) { + return /^[^:]+:\/\//.test(url); + } + + static protocolDefaultPort(protocol) { + switch (protocol) { + case 'rtsp': return 554; + case 'http': return 80; + case 'https': return 443; + } + + return 0; + } + } + + const listener = Symbol("event_listener"); + const listeners = Symbol("event_listeners"); + + class DestructibleEventListener { + constructor(eventListener) { + this[listener] = eventListener; + this[listeners] = new Map(); + } + + clear() { + if (this[listeners]) { + for (let entry of this[listeners]) { + for (let fn of entry[1]) { + this[listener].removeEventListener(entry[0], fn); + } + } } + this[listeners].clear(); + } + + destroy() { + this.clear(); + this[listeners] = null; + } + + on(event, selector, fn) { + if (fn == undefined) { + fn = selector; + selector = null; + } + if (selector) { + return this.addEventListener(event, (e) => { + if (e.target.matches(selector)) { + fn(e); + } + }); + } else { + return this.addEventListener(event, fn); + } + } + + addEventListener(event, fn) { + if (!this[listeners].has(event)) { + this[listeners].set(event, new Set()); + } + this[listeners].get(event).add(fn); + this[listener].addEventListener(event, fn, false); + return fn; + } + + removeEventListener(event, fn) { + this[listener].removeEventListener(event, fn, false); + if (this[listeners].has(event)) { + //this[listeners].set(event, new Set()); + let ev = this[listeners].get(event); + ev.delete(fn); + if (!ev.size) { + this[listeners].delete(event); + } + } + } + + dispatchEvent(event) { + if (this[listener]) { + this[listener].dispatchEvent(event); + } + } + } + + class EventEmitter { + constructor(element=null) { + this[listener] = new DestructibleEventListener(element || document.createElement('div')); + } + + clear() { + if (this[listener]) { + this[listener].clear(); + } + } + + destroy() { + if (this[listener]) { + this[listener].destroy(); + this[listener] = null; + } + } + + on(event, selector, fn) { + if (this[listener]) { + return this[listener].on(event, selector, fn); + } + return null; + } + + addEventListener(event, fn) { + if (this[listener]) { + return this[listener].addEventListener(event, fn, false); + } + return null; + } + + removeEventListener(event, fn) { + if (this[listener]) { + this[listener].removeEventListener(event, fn, false); + } + } + + dispatchEvent(event, data) { + if (this[listener]) { + this[listener].dispatchEvent(new CustomEvent(event, {detail: data})); + } + } + } + + class EventSourceWrapper { + constructor(eventSource) { + this.eventSource = eventSource; + this[listeners] = new Map(); + } + + on(event, selector, fn) { + if (!this[listeners].has(event)) { + this[listeners].set(event, new Set()); + } + let listener = this.eventSource.on(event, selector, fn); + if (listener) { + this[listeners].get(event).add(listener); + } + } + + off(event, fn){ + this.eventSource.removeEventListener(event, fn); + } + + clear() { + this.eventSource.clear(); + this[listeners].clear(); + } + + destroy() { + this.eventSource.clear(); + this[listeners] = null; + this.eventSource = null; + } + } + + /** + * Generate MP4 Box + * got from: https://github.com/dailymotion/hls.js + */ + + class MP4 { + static init() { + MP4.types = { + avc1: [], // codingname + avcC: [], + btrt: [], + dinf: [], + dref: [], + esds: [], + ftyp: [], + hdlr: [], + mdat: [], + mdhd: [], + mdia: [], + mfhd: [], + minf: [], + moof: [], + moov: [], + mp4a: [], + mvex: [], + mvhd: [], + sdtp: [], + stbl: [], + stco: [], + stsc: [], + stsd: [], + stsz: [], + stts: [], + tfdt: [], + tfhd: [], + traf: [], + trak: [], + trun: [], + trex: [], + tkhd: [], + vmhd: [], + smhd: [] + }; + + var i; + for (i in MP4.types) { + if (MP4.types.hasOwnProperty(i)) { + MP4.types[i] = [ + i.charCodeAt(0), + i.charCodeAt(1), + i.charCodeAt(2), + i.charCodeAt(3) + ]; + } + } + + var videoHdlr = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x56, 0x69, 0x64, 0x65, + 0x6f, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' + ]); + + var audioHdlr = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x53, 0x6f, 0x75, 0x6e, + 0x64, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' + ]); + + MP4.HDLR_TYPES = { + 'video': videoHdlr, + 'audio': audioHdlr + }; + + var dref = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01, // entry_count + 0x00, 0x00, 0x00, 0x0c, // entry_size + 0x75, 0x72, 0x6c, 0x20, // 'url' type + 0x00, // version 0 + 0x00, 0x00, 0x01 // entry_flags + ]); + + var stco = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00 // entry_count + ]); + + MP4.STTS = MP4.STSC = MP4.STCO = stco; + + MP4.STSZ = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // sample_size + 0x00, 0x00, 0x00, 0x00, // sample_count + ]); + MP4.VMHD = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x01, // flags + 0x00, 0x00, // graphicsmode + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00 // opcolor + ]); + MP4.SMHD = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, // balance + 0x00, 0x00 // reserved + ]); + + MP4.STSD = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01]);// entry_count + + var majorBrand = new Uint8Array([105,115,111,109]); // isom + var avc1Brand = new Uint8Array([97,118,99,49]); // avc1 + var minorVersion = new Uint8Array([0, 0, 0, 1]); + + MP4.FTYP = MP4.box(MP4.types.ftyp, majorBrand, minorVersion, majorBrand, avc1Brand); + MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, dref)); + } + + static box(type, ...payload) { + var size = 8, + i = payload.length, + len = i, + result; + // calculate the total size we need to allocate + while (i--) { + size += payload[i].byteLength; + } + result = new Uint8Array(size); + result[0] = (size >> 24) & 0xff; + result[1] = (size >> 16) & 0xff; + result[2] = (size >> 8) & 0xff; + result[3] = size & 0xff; + result.set(type, 4); + // copy the payload into the result + for (i = 0, size = 8; i < len; ++i) { + // copy payload[i] array @ offset size + result.set(payload[i], size); + size += payload[i].byteLength; + } + return result; + } + + static hdlr(type) { + return MP4.box(MP4.types.hdlr, MP4.HDLR_TYPES[type]); + } + + static mdat(data) { + return MP4.box(MP4.types.mdat, data); + } + + static mdhd(timescale, duration) { + return MP4.box(MP4.types.mdhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x02, // creation_time + 0x00, 0x00, 0x00, 0x03, // modification_time + (timescale >> 24) & 0xFF, + (timescale >> 16) & 0xFF, + (timescale >> 8) & 0xFF, + timescale & 0xFF, // timescale + (duration >> 24), + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x55, 0xc4, // 'und' language (undetermined) + 0x00, 0x00 + ])); + } + + static mdia(track) { + return MP4.box(MP4.types.mdia, MP4.mdhd(track.timescale, track.duration), MP4.hdlr(track.type), MP4.minf(track)); + } + + static mfhd(sequenceNumber) { + return MP4.box(MP4.types.mfhd, new Uint8Array([ + 0x00, + 0x00, 0x00, 0x00, // flags + (sequenceNumber >> 24), + (sequenceNumber >> 16) & 0xFF, + (sequenceNumber >> 8) & 0xFF, + sequenceNumber & 0xFF, // sequence_number + ])); + } + + static minf(track) { + if (track.type === 'audio') { + return MP4.box(MP4.types.minf, MP4.box(MP4.types.smhd, MP4.SMHD), MP4.DINF, MP4.stbl(track)); + } else { + return MP4.box(MP4.types.minf, MP4.box(MP4.types.vmhd, MP4.VMHD), MP4.DINF, MP4.stbl(track)); + } + } + + static moof(sn, baseMediaDecodeTime, track) { + return MP4.box(MP4.types.moof, MP4.mfhd(sn), MP4.traf(track,baseMediaDecodeTime)); + } + /** + * @param tracks... (optional) {array} the tracks associated with this movie + */ + static moov(tracks, duration, timescale) { + var + i = tracks.length, + boxes = []; + + while (i--) { + boxes[i] = MP4.trak(tracks[i]); + } + + return MP4.box.apply(null, [MP4.types.moov, MP4.mvhd(timescale, duration)].concat(boxes).concat(MP4.mvex(tracks))); + } + + static mvex(tracks) { + var + i = tracks.length, + boxes = []; + + while (i--) { + boxes[i] = MP4.trex(tracks[i]); + } + return MP4.box.apply(null, [MP4.types.mvex].concat(boxes)); + } + + static mvhd(timescale,duration) { + var + bytes = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01, // creation_time + 0x00, 0x00, 0x00, 0x02, // modification_time + (timescale >> 24) & 0xFF, + (timescale >> 16) & 0xFF, + (timescale >> 8) & 0xFF, + timescale & 0xFF, // timescale + (duration >> 24) & 0xFF, + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x00, 0x01, 0x00, 0x00, // 1.0 rate + 0x01, 0x00, // 1.0 volume + 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0xff, 0xff, 0xff, 0xff // next_track_ID + ]); + return MP4.box(MP4.types.mvhd, bytes); + } + + static sdtp(track) { + var + samples = track.samples || [], + bytes = new Uint8Array(4 + samples.length), + flags, + i; + // leave the full box header (4 bytes) all zero + // write the sample table + for (i = 0; i < samples.length; i++) { + flags = samples[i].flags; + bytes[i + 4] = (flags.dependsOn << 4) | + (flags.isDependedOn << 2) | + (flags.hasRedundancy); + } + + return MP4.box(MP4.types.sdtp, bytes); + } + + static stbl(track) { + return MP4.box(MP4.types.stbl, MP4.stsd(track), MP4.box(MP4.types.stts, MP4.STTS), MP4.box(MP4.types.stsc, MP4.STSC), MP4.box(MP4.types.stsz, MP4.STSZ), MP4.box(MP4.types.stco, MP4.STCO)); + } + + static avc1(track) { + var sps = [], pps = [], i, data, len; + // assemble the SPSs + + for (i = 0; i < track.sps.length; i++) { + data = track.sps[i]; + len = data.byteLength; + sps.push((len >>> 8) & 0xFF); + sps.push((len & 0xFF)); + sps = sps.concat(Array.prototype.slice.call(data)); // SPS + } + + // assemble the PPSs + for (i = 0; i < track.pps.length; i++) { + data = track.pps[i]; + len = data.byteLength; + pps.push((len >>> 8) & 0xFF); + pps.push((len & 0xFF)); + pps = pps.concat(Array.prototype.slice.call(data)); + } + + var avcc = MP4.box(MP4.types.avcC, new Uint8Array([ + 0x01, // version + sps[3], // profile + sps[4], // profile compat + sps[5], // level + 0xfc | 3, // lengthSizeMinusOne, hard-coded to 4 bytes + 0xE0 | track.sps.length // 3bit reserved (111) + numOfSequenceParameterSets + ].concat(sps).concat([ + track.pps.length // numOfPictureParameterSets + ]).concat(pps))), // "PPS" + width = track.width, + height = track.height; + //console.log('avcc:' + Hex.hexDump(avcc)); + return MP4.box(MP4.types.avc1, new Uint8Array([ + 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, // pre_defined + 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // pre_defined + (width >> 8) & 0xFF, + width & 0xff, // width + (height >> 8) & 0xFF, + height & 0xff, // height + 0x00, 0x48, 0x00, 0x00, // horizresolution + 0x00, 0x48, 0x00, 0x00, // vertresolution + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // frame_count + 0x12, + 0x62, 0x69, 0x6E, 0x65, //binelpro.ru + 0x6C, 0x70, 0x72, 0x6F, + 0x2E, 0x72, 0x75, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, // compressorname + 0x00, 0x18, // depth = 24 + 0x11, 0x11]), // pre_defined = -1 + avcc, + MP4.box(MP4.types.btrt, new Uint8Array([ + 0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB + 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate + 0x00, 0x2d, 0xc6, 0xc0])) // avgBitrate + ); + } + + static esds(track) { + var configlen = track.config.byteLength; + let data = new Uint8Array(26+configlen+3); + data.set([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + + 0x03, // descriptor_type + 0x17+configlen, // length + 0x00, 0x01, //es_id + 0x00, // stream_priority + + 0x04, // descriptor_type + 0x0f+configlen, // length + 0x40, //codec : mpeg4_audio + 0x15, // stream_type + 0x00, 0x00, 0x00, // buffer_size + 0x00, 0x00, 0x00, 0x00, // maxBitrate + 0x00, 0x00, 0x00, 0x00, // avgBitrate + + 0x05, // descriptor_type + configlen + ]); + data.set(track.config, 26); + data.set([0x06, 0x01, 0x02], 26+configlen); + // return new Uint8Array([ + // 0x00, // version 0 + // 0x00, 0x00, 0x00, // flags + // + // 0x03, // descriptor_type + // 0x17+configlen, // length + // 0x00, 0x01, //es_id + // 0x00, // stream_priority + // + // 0x04, // descriptor_type + // 0x0f+configlen, // length + // 0x40, //codec : mpeg4_audio + // 0x15, // stream_type + // 0x00, 0x00, 0x00, // buffer_size + // 0x00, 0x00, 0x00, 0x00, // maxBitrate + // 0x00, 0x00, 0x00, 0x00, // avgBitrate + // + // 0x05 // descriptor_type + // ].concat([configlen]).concat(track.config).concat([0x06, 0x01, 0x02])); // GASpecificConfig)); // length + audio config descriptor + return data; + } + + static mp4a(track) { + var audiosamplerate = track.audiosamplerate; + return MP4.box(MP4.types.mp4a, new Uint8Array([ + 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, track.channelCount, // channelcount + 0x00, 0x10, // sampleSize:16bits + 0x00, 0x00, // pre_defined + 0x00, 0x00, // reserved2 + (audiosamplerate >> 8) & 0xFF, + audiosamplerate & 0xff, // + 0x00, 0x00]), + MP4.box(MP4.types.esds, MP4.esds(track))); + } + + static stsd(track) { + if (track.type === 'audio') { + return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track)); + } else { + return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track)); + } + } + + static tkhd(track) { + var id = track.id, + duration = track.duration, + width = track.width, + height = track.height, + volume = track.volume; + return MP4.box(MP4.types.tkhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x07, // flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + (id >> 24) & 0xFF, + (id >> 16) & 0xFF, + (id >> 8) & 0xFF, + id & 0xFF, // track_ID + 0x00, 0x00, 0x00, 0x00, // reserved + (duration >> 24), + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, // layer + 0x00, 0x00, // alternate_group + (volume>>0)&0xff, (((volume%1)*10)>>0)&0xff, // track volume // FIXME + 0x00, 0x00, // reserved + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix + (width >> 8) & 0xFF, + width & 0xFF, + 0x00, 0x00, // width + (height >> 8) & 0xFF, + height & 0xFF, + 0x00, 0x00 // height + ])); + } + + static traf(track,baseMediaDecodeTime) { + var sampleDependencyTable = MP4.sdtp(track), + id = track.id; + return MP4.box(MP4.types.traf, + MP4.box(MP4.types.tfhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (id >> 24), + (id >> 16) & 0XFF, + (id >> 8) & 0XFF, + (id & 0xFF) // track_ID + ])), + MP4.box(MP4.types.tfdt, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (baseMediaDecodeTime >>24), + (baseMediaDecodeTime >> 16) & 0XFF, + (baseMediaDecodeTime >> 8) & 0XFF, + (baseMediaDecodeTime & 0xFF) // baseMediaDecodeTime + ])), + MP4.trun(track, + sampleDependencyTable.length + + 16 + // tfhd + 16 + // tfdt + 8 + // traf header + 16 + // mfhd + 8 + // moof header + 8), // mdat header + sampleDependencyTable); + } + + /** + * Generate a track box. + * @param track {object} a track definition + * @return {Uint8Array} the track box + */ + static trak(track) { + track.duration = track.duration || 0xffffffff; + return MP4.box(MP4.types.trak, MP4.tkhd(track), MP4.mdia(track)); + } + + static trex(track) { + var id = track.id; + return MP4.box(MP4.types.trex, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (id >> 24), + (id >> 16) & 0XFF, + (id >> 8) & 0XFF, + (id & 0xFF), // track_ID + 0x00, 0x00, 0x00, 0x01, // default_sample_description_index + 0x00, 0x00, 0x00, 0x00, // default_sample_duration + 0x00, 0x00, 0x00, 0x00, // default_sample_size + 0x00, 0x01, 0x00, 0x01 // default_sample_flags + ])); + } + + static trun(track, offset) { + var samples= track.samples || [], + len = samples.length, + arraylen = 12 + (16 * len), + array = new Uint8Array(arraylen), + i,sample,duration,size,flags,cts; + offset += 8 + arraylen; + array.set([ + 0x00, // version 0 + 0x00, 0x0f, 0x01, // flags + (len >>> 24) & 0xFF, + (len >>> 16) & 0xFF, + (len >>> 8) & 0xFF, + len & 0xFF, // sample_count + (offset >>> 24) & 0xFF, + (offset >>> 16) & 0xFF, + (offset >>> 8) & 0xFF, + offset & 0xFF // data_offset + ],0); + for (i = 0; i < len; i++) { + sample = samples[i]; + duration = sample.duration; + size = sample.size; + flags = sample.flags; + cts = sample.cts; + array.set([ + (duration >>> 24) & 0xFF, + (duration >>> 16) & 0xFF, + (duration >>> 8) & 0xFF, + duration & 0xFF, // sample_duration + (size >>> 24) & 0xFF, + (size >>> 16) & 0xFF, + (size >>> 8) & 0xFF, + size & 0xFF, // sample_size + (flags.isLeading << 2) | flags.dependsOn, + (flags.isDependedOn << 6) | + (flags.hasRedundancy << 4) | + (flags.paddingValue << 1) | + flags.isNonSync, + flags.degradPrio & 0xF0 << 8, + flags.degradPrio & 0x0F, // sample_flags + (cts >>> 24) & 0xFF, + (cts >>> 16) & 0xFF, + (cts >>> 8) & 0xFF, + cts & 0xFF // sample_composition_time_offset + ],12+16*i); + } + return MP4.box(MP4.types.trun, array); + } + + static initSegment(tracks, duration, timescale) { + if (!MP4.types) { + MP4.init(); + } + var movie = MP4.moov(tracks, duration, timescale), result; + result = new Uint8Array(MP4.FTYP.byteLength + movie.byteLength); + result.set(MP4.FTYP); + result.set(movie, MP4.FTYP.byteLength); + return result; + } + } + + //import {MP4Inspect} from '../iso-bmff/mp4-inspector.js'; + + const LOG_TAG = "mse"; + const Log$1 = getTagged(LOG_TAG); + + class MSEBuffer { + constructor(parent, codec) { + this.mediaSource = parent.mediaSource; + this.players = parent.players; + this.cleaning = false; + this.parent = parent; + this.queue = []; + this.cleanResolvers = []; + this.codec = codec; + this.cleanRanges = []; + + Log$1.debug(`Use codec: ${codec}`); + + this.sourceBuffer = this.mediaSource.addSourceBuffer(codec); + this.eventSource = new EventEmitter(this.sourceBuffer); + + this.eventSource.addEventListener('updatestart', (e)=> { + // this.updating = true; + // Log.debug('update start'); + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning start`); + } + }); + + this.eventSource.addEventListener('update', (e)=> { + // this.updating = true; + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning update`); + } + }); + + this.eventSource.addEventListener('updateend', (e)=> { + // Log.debug('update end'); + // this.updating = false; + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning end`); + + try { + if (this.sourceBuffer.buffered.length && this.players[0].currentTime < this.sourceBuffer.buffered.start(0)) { + this.players[0].currentTime = this.sourceBuffer.buffered.start(0); + } + } catch (e) { + // TODO: do something? + } + while (this.cleanResolvers.length) { + let resolver = this.cleanResolvers.shift(); + resolver(); + } + this.cleaning = false; + + if (this.cleanRanges.length) { + this.doCleanup(); + return; + } + } + this.feedNext(); + }); + + this.eventSource.addEventListener('error', (e)=> { + Log$1.debug(`Source buffer error: ${this.mediaSource.readyState}`); + if (this.mediaSource.sourceBuffers.length) { + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + this.parent.eventSource.dispatchEvent('error'); + }); + + this.eventSource.addEventListener('abort', (e)=> { + Log$1.debug(`Source buffer aborted: ${this.mediaSource.readyState}`); + if (this.mediaSource.sourceBuffers.length) { + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + this.parent.eventSource.dispatchEvent('error'); + }); + + if (!this.sourceBuffer.updating) { + this.feedNext(); + } + // TODO: cleanup every hour for live streams + } + + destroy() { + this.eventSource.destroy(); + this.clear(); + this.queue = []; + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + + clear() { + this.queue = []; + let promises = []; + for (let i=0; i< this.sourceBuffer.buffered.length; ++i) { + // TODO: await remove + this.cleaning = true; + promises.push(new Promise((resolve, reject)=>{ + this.cleanResolvers.push(resolve); + if (!this.sourceBuffer.updating) { + this.sourceBuffer.remove(this.sourceBuffer.buffered.start(i), this.sourceBuffer.buffered.end(i)); + resolve(); + } else { + this.sourceBuffer.onupdateend = () => { + if (this.sourceBuffer) { + this.sourceBuffer.remove(this.sourceBuffer.buffered.start(i), this.sourceBuffer.buffered.end(i)); + } + resolve(); + }; + } + })); + } + return Promise.all(promises); + } + + setLive(is_live) { + this.is_live = is_live; + } + + feedNext() { + // Log.debug("feed next ", this.sourceBuffer.updating); + if (!this.sourceBuffer.updating && !this.cleaning && this.queue.length) { + this.doAppend(this.queue.shift()); + // TODO: if is live and current position > 1hr => clean all and restart + } + } + + doCleanup() { + if (!this.cleanRanges.length) { + this.cleaning = false; + this.feedNext(); + return; + } + let range = this.cleanRanges.shift(); + Log$1.debug(`${this.codec} remove range [${range[0]} - ${range[1]}). + \nUpdating: ${this.sourceBuffer.updating} + `); + this.cleaning = true; + this.sourceBuffer.remove(range[0], range[1]); + } + + initCleanup() { + if (this.sourceBuffer.buffered.length && !this.sourceBuffer.updating && !this.cleaning) { + Log$1.debug(`${this.codec} cleanup`); + let removeBound = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length-1) - 2; + + for (let i=0; i< this.sourceBuffer.buffered.length; ++i) { + let removeStart = this.sourceBuffer.buffered.start(i); + let removeEnd = this.sourceBuffer.buffered.end(i); + if ((this.players[0].currentTime <= removeStart) || (removeBound <= removeStart)) continue; + + if ((removeBound <= removeEnd) && (removeBound >= removeStart)) { + Log$1.debug(`Clear [${removeStart}, ${removeBound}), leave [${removeBound}, ${removeEnd}]`); + removeEnd = removeBound; + if (removeEnd!=removeStart) { + this.cleanRanges.push([removeStart, removeEnd]); + } + continue; // Do not cleanup buffered range after current position + } + this.cleanRanges.push([removeStart, removeEnd]); + } + + this.doCleanup(); + + // let bufferStart = this.sourceBuffer.buffered.start(0); + // let removeEnd = this.sourceBuffer.buffered.start(0) + (this.sourceBuffer.buffered.end(0) - this.sourceBuffer.buffered.start(0))/2; + // if (this.players[0].currentTime < removeEnd) { + // this.players[0].currentTime = removeEnd; + // } + // let removeEnd = Math.max(this.players[0].currentTime - 3, this.sourceBuffer.buffered.end(0) - 3); + // + // if (removeEnd < bufferStart) { + // removeEnd = this.sourceBuffer.buffered.start(0) + (this.sourceBuffer.buffered.end(0) - this.sourceBuffer.buffered.start(0))/2; + // if (this.players[0].currentTime < removeEnd) { + // this.players[0].currentTime = removeEnd; + // } + // } + + // if (removeEnd > bufferStart && (removeEnd - bufferStart > 0.5 )) { + // // try { + // Log.debug(`${this.codec} remove range [${bufferStart} - ${removeEnd}). + // \nBuffered end: ${this.sourceBuffer.buffered.end(0)} + // \nUpdating: ${this.sourceBuffer.updating} + // `); + // this.cleaning = true; + // this.sourceBuffer.remove(bufferStart, removeEnd); + // // } catch (e) { + // // // TODO: implement + // // Log.error(e); + // // } + // } else { + // this.feedNext(); + // } + } else { + this.feedNext(); + } + } + + doAppend(data) { + // console.log(MP4Inspect.mp4toJSON(data)); + let err = this.players[0].error; + if (err) { + Log$1.error(`Error occured: ${MSE.ErrorNotes[err.code]}`); + try { + this.players.forEach((video)=>{video.stop();}); + this.mediaSource.endOfStream(); + } catch (e){ + + } + this.parent.eventSource.dispatchEvent('error'); + } else { + try { + this.sourceBuffer.appendBuffer(data); + } catch (e) { + if (e.name === 'QuotaExceededError') { + Log$1.debug(`${this.codec} quota fail`); + this.queue.unshift(data); + this.initCleanup(); + return; + } + + // reconnect on fail + Log$1.error(`Error occured while appending buffer. ${e.name}: ${e.message}`); + this.parent.eventSource.dispatchEvent('error'); + } + } + + } + + feed(data) { + this.queue = this.queue.concat(data); + // Log.debug(this.sourceBuffer.updating, this.updating, this.queue.length); + if (this.sourceBuffer && !this.sourceBuffer.updating && !this.cleaning) { + // Log.debug('enq feed'); + this.feedNext(); + } + } + } + + class MSE { + // static CODEC_AVC_BASELINE = "avc1.42E01E"; + // static CODEC_AVC_MAIN = "avc1.4D401E"; + // static CODEC_AVC_HIGH = "avc1.64001E"; + // static CODEC_VP8 = "vp8"; + // static CODEC_AAC = "mp4a.40.2"; + // static CODEC_VORBIS = "vorbis"; + // static CODEC_THEORA = "theora"; + + static get ErrorNotes() {return { + [MediaError.MEDIA_ERR_ABORTED]: 'fetching process aborted by user', + [MediaError.MEDIA_ERR_NETWORK]: 'error occurred when downloading', + [MediaError.MEDIA_ERR_DECODE]: 'error occurred when decoding', + [MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED]: 'audio/video not supported' + }}; + + static isSupported(codecs) { + return (window.MediaSource && window.MediaSource.isTypeSupported(`video/mp4; codecs="${codecs.join(',')}"`)); + } + + constructor (players) { + this.players = players; + const playing = this.players.map((video, idx) => { + video.onplaying = function () { + playing[idx] = true; + }; + video.onpause = function () { + playing[idx] = false; + }; + return !video.paused; + }); + this.playing = playing; + this.mediaSource = new MediaSource(); + this.eventSource = new EventEmitter(this.mediaSource); + this.reset(); + } + + destroy() { + this.reset(); + this.eventSource.destroy(); + this.mediaSource = null; + this.eventSource = null; + } + + play() { + this.players.forEach((video, idx)=>{ + if (video.paused && !this.playing[idx]) { + Log$1.debug(`player ${idx}: play`); + video.play(); + } + }); + } + + setLive(is_live) { + for (let idx in this.buffers) { + this.buffers[idx].setLive(is_live); + } + this.is_live = is_live; + } + + resetBuffers() { + this.players.forEach((video, idx)=>{ + if (!video.paused && this.playing[idx]) { + video.pause(); + video.currentTime = 0; + } + }); + + let promises = []; + for (let buffer of this.buffers.values()) { + promises.push(buffer.clear()); + } + return Promise.all(promises).then(()=>{ + this.mediaSource.endOfStream(); + this.mediaSource.duration = 0; + this.mediaSource.clearLiveSeekableRange(); + this.play(); + }); + } + + clear() { + this.reset(); + this.players.forEach((video)=>{video.src = URL.createObjectURL(this.mediaSource);}); + + return this.setupEvents(); + } + + setupEvents() { + this.eventSource.clear(); + this.resolved = false; + this.mediaReady = new Promise((resolve, reject)=> { + this._sourceOpen = ()=> { + Log$1.debug(`Media source opened: ${this.mediaSource.readyState}`); + if (!this.resolved) { + this.resolved = true; + resolve(); + } + }; + this._sourceEnded = ()=>{ + Log$1.debug(`Media source ended: ${this.mediaSource.readyState}`); + }; + this._sourceClose = ()=>{ + Log$1.debug(`Media source closed: ${this.mediaSource.readyState}`); + if (this.resolved) { + this.eventSource.dispatchEvent('sourceclosed'); + } + }; + this.eventSource.addEventListener('sourceopen', this._sourceOpen); + this.eventSource.addEventListener('sourceended', this._sourceEnded); + this.eventSource.addEventListener('sourceclose', this._sourceClose); + }); + return this.mediaReady; + } + + reset() { + this.ready = false; + for (let track in this.buffers) { + this.buffers[track].destroy(); + delete this.buffers[track]; + } + if (this.mediaSource.readyState == 'open') { + this.mediaSource.duration = 0; + this.mediaSource.endOfStream(); + } + this.updating = false; + this.resolved = false; + this.buffers = {}; + // this.players.forEach((video)=>{video.src = URL.createObjectURL(this.mediaSource)}); + // TODO: remove event listeners for existing media source + // this.setupEvents(); + // this.clear(); + } + + setCodec(track, mimeCodec) { + return this.mediaReady.then(()=>{ + this.buffers[track] = new MSEBuffer(this, mimeCodec); + this.buffers[track].setLive(this.is_live); + }); + } + + feed(track, data) { + if (this.buffers[track]) { + this.buffers[track].feed(data); + } + } + } + + const Log$2 = getTagged('remuxer:base'); + let track_id = 1; + class BaseRemuxer { + + static get MP4_TIMESCALE() { return 90000;} + + // TODO: move to ts parser + // static PTSNormalize(value, reference) { + // + // let offset; + // if (reference === undefined) { + // return value; + // } + // if (reference < value) { + // // - 2^33 + // offset = -8589934592; + // } else { + // // + 2^33 + // offset = 8589934592; + // } + // /* PTS is 33bit (from 0 to 2^33 -1) + // if diff between value and reference is bigger than half of the amplitude (2^32) then it means that + // PTS looping occured. fill the gap */ + // while (Math.abs(value - reference) > 4294967296) { + // value += offset; + // } + // return value; + // } + + static getTrackID() { + return track_id++; + } + + constructor(timescale, scaleFactor, params) { + this.timeOffset = 0; + this.timescale = timescale; + this.scaleFactor = scaleFactor; + this.readyToDecode = false; + this.samples = []; + this.seq = 1; + this.tsAlign = 1; + } + + scaled(timestamp) { + return timestamp / this.scaleFactor; + } + + unscaled(timestamp) { + return timestamp * this.scaleFactor; + } + + remux(unit) { + if (unit) { + this.samples.push({ + unit: unit, + pts: unit.pts, + dts: unit.dts + }); + return true; + } + return false; + } + + static toMS(timestamp) { + return timestamp/90; + } + + setConfig(config) { + + } + + insertDscontinuity() { + this.samples.push(null); + } + + init(initPTS, initDTS, shouldInitialize=true) { + this.initPTS = Math.min(initPTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); + this.initDTS = Math.min(initDTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); + Log$2.debug(`Initial pts=${this.initPTS} dts=${this.initDTS} offset=${this.unscaled(this.timeOffset)}`); + this.initialized = shouldInitialize; + } + + flush() { + this.seq++; + this.mp4track.len = 0; + this.mp4track.samples = []; + } + + static dtsSortFunc(a,b) { + return (a.dts-b.dts); + } + + static groupByDts(gop) { + const groupBy = (xs, key) => { + return xs.reduce((rv, x) => { + (rv[x[key]] = rv[x[key]] || []).push(x); + return rv; + }, {}); + }; + return groupBy(gop, 'dts'); + } + + getPayloadBase(sampleFunction, setupSample) { + if (!this.readyToDecode || !this.initialized || !this.samples.length) return null; + this.samples.sort(BaseRemuxer.dtsSortFunc); + return true; + // + // let payload = new Uint8Array(this.mp4track.len); + // let offset = 0; + // let samples=this.mp4track.samples; + // let mp4Sample, lastDTS, pts, dts; + // + // while (this.samples.length) { + // let sample = this.samples.shift(); + // if (sample === null) { + // // discontinuity + // this.nextDts = undefined; + // break; + // } + // + // let unit = sample.unit; + // + // pts = Math.round((sample.pts - this.initDTS)/this.tsAlign)*this.tsAlign; + // dts = Math.round((sample.dts - this.initDTS)/this.tsAlign)*this.tsAlign; + // // ensure DTS is not bigger than PTS + // dts = Math.min(pts, dts); + // + // // sampleFunction(pts, dts); // TODO: + // + // // mp4Sample = setupSample(unit, pts, dts); // TODO: + // + // payload.set(unit.getData(), offset); + // offset += unit.getSize(); + // + // samples.push(mp4Sample); + // lastDTS = dts; + // } + // if (!samples.length) return null; + // + // // samplesPostFunction(samples); // TODO: + // + // return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } + } + + const Log$3 = getTagged("remuxer:aac"); + // TODO: asm.js + class AACRemuxer extends BaseRemuxer { + + constructor(timescale, scaleFactor = 1, params={}) { + super(timescale, scaleFactor); + + this.codecstring=MSE.CODEC_AAC; + this.units = []; + this.initDTS = undefined; + this.nextAacPts = undefined; + this.lastPts = 0; + this.firstDTS = 0; + this.firstPTS = 0; + this.duration = params.duration || 1; + this.initialized = false; + + this.mp4track={ + id:BaseRemuxer.getTrackID(), + type: 'audio', + fragmented:true, + channelCount:0, + audiosamplerate: this.timescale, + duration: 0, + timescale: this.timescale, + volume: 1, + samples: [], + config: '', + len: 0 + }; + if (params.config) { + this.setConfig(params.config); + } + } + + setConfig(config) { + this.mp4track.channelCount = config.channels; + this.mp4track.audiosamplerate = config.samplerate; + if (!this.mp4track.duration) { + this.mp4track.duration = (this.duration?this.duration:1)*config.samplerate; + } + this.mp4track.timescale = config.samplerate; + this.mp4track.config = config.config; + this.mp4track.codec = config.codec; + this.timescale = config.samplerate; + this.scaleFactor = BaseRemuxer.MP4_TIMESCALE / config.samplerate; + this.expectedSampleDuration = 1024 * this.scaleFactor; + this.readyToDecode = true; + } + + remux(aac) { + if (super.remux.call(this, aac)) { + this.mp4track.len += aac.getSize(); + } + } + + getPayload() { + if (!this.readyToDecode || !this.samples.length) return null; + this.samples.sort(function(a, b) { + return (a.dts-b.dts); + }); + + let payload = new Uint8Array(this.mp4track.len); + let offset = 0; + let samples=this.mp4track.samples; + let mp4Sample, lastDTS, pts, dts; + + while (this.samples.length) { + let sample = this.samples.shift(); + if (sample === null) { + // discontinuity + this.nextDts = undefined; + break; + } + let unit = sample.unit; + pts = sample.pts - this.initDTS; + dts = sample.dts - this.initDTS; + + if (lastDTS === undefined) { + if (this.nextDts) { + let delta = Math.round(this.scaled(pts - this.nextAacPts)); + // if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments + if (/*contiguous || */Math.abs(delta) < 600) { + // log delta + if (delta) { + if (delta > 0) { + Log$3.log(`${delta} ms hole between AAC samples detected,filling it`); + // if we have frame overlap, overlapping for more than half a frame duraion + } else if (delta < -12) { + // drop overlapping audio frames... browser will deal with it + Log$3.log(`${(-delta)} ms overlapping between AAC samples detected, drop frame`); + this.mp4track.len -= unit.getSize(); + continue; + } + // set DTS to next DTS + pts = dts = this.nextAacPts; + } + } + } + // remember first PTS of our aacSamples, ensure value is positive + this.firstDTS = Math.max(0, dts); + } + + mp4Sample = { + size: unit.getSize(), + cts: 0, + duration:1024, + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0, + dependsOn: 1 + } + }; + + payload.set(unit.getData(), offset); + offset += unit.getSize(); + samples.push(mp4Sample); + lastDTS = dts; + } + if (!samples.length) return null; + this.nextDts =pts+this.expectedSampleDuration; + return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } + } + //test.bundle.js:42 [remuxer:h264] skip frame from the past at DTS=18397972271140676 with expected DTS=18397998040950484 + + /** + * Parser for exponential Golomb codes, a variable-bitwidth number encoding scheme used by h264. + */ + + class ExpGolomb { + + constructor(data) { + this.data = data; + // the number of bytes left to examine in this.data + this.bytesAvailable = this.data.byteLength; + // the current word being examined + this.word = 0; // :uint + // the number of bits left to examine in the current word + this.bitsAvailable = 0; // :uint + } + + // ():void + loadWord() { + var + position = this.data.byteLength - this.bytesAvailable, + workingBytes = new Uint8Array(4), + availableBytes = Math.min(4, this.bytesAvailable); + if (availableBytes === 0) { + throw new Error('no bytes available'); + } + workingBytes.set(this.data.subarray(position, position + availableBytes)); + this.word = new DataView(workingBytes.buffer, workingBytes.byteOffset, workingBytes.byteLength).getUint32(0); + // track the amount of this.data that has been processed + this.bitsAvailable = availableBytes * 8; + this.bytesAvailable -= availableBytes; + } + + // (count:int):void + skipBits(count) { + var skipBytes; // :int + if (this.bitsAvailable > count) { + this.word <<= count; + this.bitsAvailable -= count; + } else { + count -= this.bitsAvailable; + skipBytes = count >> 3; + count -= (skipBytes << 3); + this.bytesAvailable -= skipBytes; + this.loadWord(); + this.word <<= count; + this.bitsAvailable -= count; + } + } + + // (size:int):uint + readBits(size) { + var + bits = Math.min(this.bitsAvailable, size), // :uint + valu = this.word >>> (32 - bits); // :uint + if (size > 32) { + Log.error('Cannot read more than 32 bits at a time'); + } + this.bitsAvailable -= bits; + if (this.bitsAvailable > 0) { + this.word <<= bits; + } else if (this.bytesAvailable > 0) { + this.loadWord(); + } + bits = size - bits; + if (bits > 0) { + return valu << bits | this.readBits(bits); + } else { + return valu; + } + } + + // ():uint + skipLZ() { + var leadingZeroCount; // :uint + for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) { + if (0 !== (this.word & (0x80000000 >>> leadingZeroCount))) { + // the first bit of working word is 1 + this.word <<= leadingZeroCount; + this.bitsAvailable -= leadingZeroCount; + return leadingZeroCount; + } + } + // we exhausted word and still have not found a 1 + this.loadWord(); + return leadingZeroCount + this.skipLZ(); + } + + // ():void + skipUEG() { + this.skipBits(1 + this.skipLZ()); + } + + // ():void + skipEG() { + this.skipBits(1 + this.skipLZ()); + } + + // ():uint + readUEG() { + var clz = this.skipLZ(); // :uint + return this.readBits(clz + 1) - 1; + } + + // ():int + readEG() { + var valu = this.readUEG(); // :int + if (0x01 & valu) { + // the number is odd if the low order bit is set + return (1 + valu) >>> 1; // add 1 to make it even, and divide by 2 + } else { + return -1 * (valu >>> 1); // divide by two then make it negative + } + } + + // Some convenience functions + // :Boolean + readBoolean() { + return 1 === this.readBits(1); + } + + // ():int + readUByte() { + return this.readBits(8); + } + + // ():int + readUShort() { + return this.readBits(16); + } + // ():int + readUInt() { + return this.readBits(32); + } + } + + // TODO: asm.js + + function appendByteArray(buffer1, buffer2) { + let tmp = new Uint8Array((buffer1.byteLength|0) + (buffer2.byteLength|0)); + tmp.set(buffer1, 0); + tmp.set(buffer2, buffer1.byteLength|0); + return tmp; + } + function base64ToArrayBuffer(base64) { + var binary_string = window.atob(base64); + var len = binary_string.length; + var bytes = new Uint8Array( len ); + for (var i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; + } + + function hexToByteArray(hex) { + let len = hex.length >> 1; + var bufView = new Uint8Array(len); + for (var i = 0; i < len; i++) { + bufView[i] = parseInt(hex.substr(i<<1,2),16); + } + return bufView; + } + + function bitSlice(bytearray, start=0, end=bytearray.byteLength*8) { + let byteLen = Math.ceil((end-start)/8); + let res = new Uint8Array(byteLen); + let startByte = start >>> 3; // /8 + let endByte = (end>>>3) - 1; // /8 + let bitOffset = start & 0x7; // %8 + let nBitOffset = 8 - bitOffset; + let endOffset = 8 - end & 0x7; // %8 + for (let i=0; i> nBitOffset; + if (i == endByte-1 && endOffset < 8) { + tail >>= endOffset; + tail <<= endOffset; + } + } + res[i]=(bytearray[startByte+i]< 0; --i) { + + /* Shift result one left to make room for another bit, + then add the next bit on the stream. */ + result = ((result|0) << 1) | (((this.byte|0) >> (8 - (++this.bitpos))) & 0x01); + if ((this.bitpos|0)>=8) { + this.byte = this.src.getUint8(++this.bytepos); + this.bitpos &= 0x7; + } + } + + return result; + } + skipBits(length) { + this.bitpos += (length|0) & 0x7; // %8 + this.bytepos += (length|0) >>> 3; // *8 + if (this.bitpos > 7) { + this.bitpos &= 0x7; + ++this.bytepos; + } + + if (!this.finished()) { + this.byte = this.src.getUint8(this.bytepos); + return 0; + } else { + return this.bytepos-this.src.byteLength-this.src.bitpos; + } + } + + finished() { + return this.bytepos >= this.src.byteLength; + } + } + + class NALU { + + static get NDR() {return 1;} + static get SLICE_PART_A() {return 2;} + static get SLICE_PART_B() {return 3;} + static get SLICE_PART_C() {return 4;} + static get IDR() {return 5;} + static get SEI() {return 6;} + static get SPS() {return 7;} + static get PPS() {return 8;} + static get DELIMITER() {return 9;} + static get EOSEQ() {return 10;} + static get EOSTR() {return 11;} + static get FILTER() {return 12;} + static get STAP_A() {return 24;} + static get STAP_B() {return 25;} + static get FU_A() {return 28;} + static get FU_B() {return 29;} + + static get TYPES() {return { + [NALU.IDR]: 'IDR', + [NALU.SEI]: 'SEI', + [NALU.SPS]: 'SPS', + [NALU.PPS]: 'PPS', + [NALU.NDR]: 'NDR' + }}; + + static type(nalu) { + if (nalu.ntype in NALU.TYPES) { + return NALU.TYPES[nalu.ntype]; + } else { + return 'UNKNOWN'; + } + } + + constructor(ntype, nri, data, dts, pts) { + + this.data = data; + this.ntype = ntype; + this.nri = nri; + this.dts = dts; + this.pts = pts ? pts : this.dts; + this.sliceType = null; + } + + appendData(idata) { + this.data = appendByteArray(this.data, idata); + } + + toString() { + return `${NALU.type(this)}(${this.data.byteLength}): NRI: ${this.getNri()}, PTS: ${this.pts}, DTS: ${this.dts}`; + } + + getNri() { + return this.nri >> 5; + } + + type() { + return this.ntype; + } + + isKeyframe() { + return this.ntype === NALU.IDR || this.sliceType === 7; + } + + getSize() { + return 4 + 1 + this.data.byteLength; + } + + getData() { + let header = new Uint8Array(5 + this.data.byteLength); + let view = new DataView(header.buffer); + view.setUint32(0, this.data.byteLength + 1); + view.setUint8(4, (0x0 & 0x80) | (this.nri & 0x60) | (this.ntype & 0x1F)); + header.set(this.data, 5); + return header; + } + } + + class H264Parser { + + constructor(remuxer) { + this.remuxer = remuxer; + this.track = remuxer.mp4track; + this.firstFound = false; + } + + msToScaled(timestamp) { + return (timestamp - this.remuxer.timeOffset) * this.remuxer.scaleFactor; + } + + parseSPS(sps) { + var config = H264Parser.readSPS(new Uint8Array(sps)); + + this.track.width = config.width; + this.track.height = config.height; + this.track.sps = [new Uint8Array(sps)]; + // this.track.timescale = this.remuxer.timescale; + // this.track.duration = this.remuxer.timescale; // TODO: extract duration for non-live client + this.track.codec = 'avc1.'; + + let codecarray = new DataView(sps.buffer, sps.byteOffset+1, 4); + for (let i = 0; i < 3; ++i) { + var h = codecarray.getUint8(i).toString(16); + if (h.length < 2) { + h = '0' + h; + } + this.track.codec += h; + } + } + + parsePPS(pps) { + this.track.pps = [new Uint8Array(pps)]; + } + + parseNAL(unit) { + if (!unit) return false; + + let push = null; + // console.log(unit.toString()); + switch (unit.type()) { + case NALU.NDR: + case NALU.IDR: + unit.sliceType = H264Parser.parceSliceHeader(unit.data); + if (unit.isKeyframe() && !this.firstFound) { + this.firstFound = true; + } + if (this.firstFound) { + push = true; + } else { + push = false; + } + break; + case NALU.PPS: + push = false; + if (!this.track.pps) { + this.parsePPS(unit.getData().subarray(4)); + if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) { + this.remuxer.readyToDecode = true; + } + } + break; + case NALU.SPS: + push = false; + if(!this.track.sps) { + this.parseSPS(unit.getData().subarray(4)); + if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) { + this.remuxer.readyToDecode = true; + } + } + break; + case NALU.SEI: + push = false; + let data = new DataView(unit.data.buffer, unit.data.byteOffset, unit.data.byteLength); + let byte_idx = 0; + let pay_type = data.getUint8(byte_idx); + ++byte_idx; + let pay_size = 0; + let sz = data.getUint8(byte_idx); + ++byte_idx; + while (sz === 255) { + pay_size+=sz; + sz = data.getUint8(byte_idx); + ++byte_idx; + } + pay_size+=sz; + + let uuid = unit.data.subarray(byte_idx, byte_idx+16); + byte_idx+=16; + console.log(`PT: ${pay_type}, PS: ${pay_size}, UUID: ${Array.from(uuid).map(function(i) { + return ('0' + i.toString(16)).slice(-2); + }).join('')}`); + // debugger; + break; + case NALU.EOSEQ: + case NALU.EOSTR: + push = false; + default: + } + if (push === null && unit.getNri() > 0 ) { + push=true; + } + return push; + } + + static parceSliceHeader(data) { + let decoder = new ExpGolomb(data); + let first_mb = decoder.readUEG(); + let slice_type = decoder.readUEG(); + let ppsid = decoder.readUEG(); + let frame_num = decoder.readUByte(); + // console.log(`first_mb: ${first_mb}, slice_type: ${slice_type}, ppsid: ${ppsid}, frame_num: ${frame_num}`); + return slice_type; + } + + /** + * Advance the ExpGolomb decoder past a scaling list. The scaling + * list is optionally transmitted as part of a sequence parameter + * set and is not relevant to transmuxing. + * @param decoder {ExpGolomb} exp golomb decoder + * @param count {number} the number of entries in this scaling list + * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 + */ + static skipScalingList(decoder, count) { + let lastScale = 8, + nextScale = 8, + deltaScale; + for (let j = 0; j < count; j++) { + if (nextScale !== 0) { + deltaScale = decoder.readEG(); + nextScale = (lastScale + deltaScale + 256) % 256; + } + lastScale = (nextScale === 0) ? lastScale : nextScale; + } + } + + /** + * Read a sequence parameter set and return some interesting video + * properties. A sequence parameter set is the H264 metadata that + * describes the properties of upcoming video frames. + * @param data {Uint8Array} the bytes of a sequence parameter set + * @return {object} an object with configuration parsed from the + * sequence parameter set, including the dimensions of the + * associated video frames. + */ + static readSPS(data) { + let decoder = new ExpGolomb(data); + let frameCropLeftOffset = 0, + frameCropRightOffset = 0, + frameCropTopOffset = 0, + frameCropBottomOffset = 0, + sarScale = 1, + profileIdc,profileCompat,levelIdc, + numRefFramesInPicOrderCntCycle, picWidthInMbsMinus1, + picHeightInMapUnitsMinus1, + frameMbsOnlyFlag, + scalingListCount; + decoder.readUByte(); + profileIdc = decoder.readUByte(); // profile_idc + profileCompat = decoder.readBits(5); // constraint_set[0-4]_flag, u(5) + decoder.skipBits(3); // reserved_zero_3bits u(3), + levelIdc = decoder.readUByte(); //level_idc u(8) + decoder.skipUEG(); // seq_parameter_set_id + // some profiles have more optional data we don't need + if (profileIdc === 100 || + profileIdc === 110 || + profileIdc === 122 || + profileIdc === 244 || + profileIdc === 44 || + profileIdc === 83 || + profileIdc === 86 || + profileIdc === 118 || + profileIdc === 128) { + var chromaFormatIdc = decoder.readUEG(); + if (chromaFormatIdc === 3) { + decoder.skipBits(1); // separate_colour_plane_flag + } + decoder.skipUEG(); // bit_depth_luma_minus8 + decoder.skipUEG(); // bit_depth_chroma_minus8 + decoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag + if (decoder.readBoolean()) { // seq_scaling_matrix_present_flag + scalingListCount = (chromaFormatIdc !== 3) ? 8 : 12; + for (let i = 0; i < scalingListCount; ++i) { + if (decoder.readBoolean()) { // seq_scaling_list_present_flag[ i ] + if (i < 6) { + H264Parser.skipScalingList(decoder, 16); + } else { + H264Parser.skipScalingList(decoder, 64); + } + } + } + } + } + decoder.skipUEG(); // log2_max_frame_num_minus4 + var picOrderCntType = decoder.readUEG(); + if (picOrderCntType === 0) { + decoder.readUEG(); //log2_max_pic_order_cnt_lsb_minus4 + } else if (picOrderCntType === 1) { + decoder.skipBits(1); // delta_pic_order_always_zero_flag + decoder.skipEG(); // offset_for_non_ref_pic + decoder.skipEG(); // offset_for_top_to_bottom_field + numRefFramesInPicOrderCntCycle = decoder.readUEG(); + for(let i = 0; i < numRefFramesInPicOrderCntCycle; ++i) { + decoder.skipEG(); // offset_for_ref_frame[ i ] + } + } + decoder.skipUEG(); // max_num_ref_frames + decoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag + picWidthInMbsMinus1 = decoder.readUEG(); + picHeightInMapUnitsMinus1 = decoder.readUEG(); + frameMbsOnlyFlag = decoder.readBits(1); + if (frameMbsOnlyFlag === 0) { + decoder.skipBits(1); // mb_adaptive_frame_field_flag + } + decoder.skipBits(1); // direct_8x8_inference_flag + if (decoder.readBoolean()) { // frame_cropping_flag + frameCropLeftOffset = decoder.readUEG(); + frameCropRightOffset = decoder.readUEG(); + frameCropTopOffset = decoder.readUEG(); + frameCropBottomOffset = decoder.readUEG(); + } + if (decoder.readBoolean()) { + // vui_parameters_present_flag + if (decoder.readBoolean()) { + // aspect_ratio_info_present_flag + let sarRatio; + const aspectRatioIdc = decoder.readUByte(); + switch (aspectRatioIdc) { + case 1: sarRatio = [1,1]; break; + case 2: sarRatio = [12,11]; break; + case 3: sarRatio = [10,11]; break; + case 4: sarRatio = [16,11]; break; + case 5: sarRatio = [40,33]; break; + case 6: sarRatio = [24,11]; break; + case 7: sarRatio = [20,11]; break; + case 8: sarRatio = [32,11]; break; + case 9: sarRatio = [80,33]; break; + case 10: sarRatio = [18,11]; break; + case 11: sarRatio = [15,11]; break; + case 12: sarRatio = [64,33]; break; + case 13: sarRatio = [160,99]; break; + case 14: sarRatio = [4,3]; break; + case 15: sarRatio = [3,2]; break; + case 16: sarRatio = [2,1]; break; + case 255: { + sarRatio = [decoder.readUByte() << 8 | decoder.readUByte(), decoder.readUByte() << 8 | decoder.readUByte()]; + break; + } + } + if (sarRatio) { + sarScale = sarRatio[0] / sarRatio[1]; + } + } + if (decoder.readBoolean()) {decoder.skipBits(1);} + + if (decoder.readBoolean()) { + decoder.skipBits(4); + if (decoder.readBoolean()) { + decoder.skipBits(24); + } + } + if (decoder.readBoolean()) { + decoder.skipUEG(); + decoder.skipUEG(); + } + if (decoder.readBoolean()) { + let unitsInTick = decoder.readUInt(); + let timeScale = decoder.readUInt(); + let fixedFrameRate = decoder.readBoolean(); + let frameDuration = timeScale/(2*unitsInTick); + console.log(`timescale: ${timeScale}; unitsInTick: ${unitsInTick}; fixedFramerate: ${fixedFrameRate}; avgFrameDuration: ${frameDuration}`); + } + } + return { + width: Math.ceil((((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale), + height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - ((frameMbsOnlyFlag? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset)) + }; + } + + static readSliceType(decoder) { + // skip NALu type + decoder.readUByte(); + // discard first_mb_in_slice + decoder.readUEG(); + // return slice_type + return decoder.readUEG(); + } + } + + const Log$4 = getTagged("remuxer:h264"); + // TODO: asm.js + class H264Remuxer extends BaseRemuxer { + + constructor(timescale, scaleFactor=1, params={}) { + super(timescale, scaleFactor); + + this.nextDts = undefined; + this.readyToDecode = false; + this.initialized = false; + + this.firstDTS=0; + this.firstPTS=0; + this.lastDTS=undefined; + this.lastSampleDuration = 0; + this.lastDurations = []; + // this.timescale = 90000; + this.tsAlign = Math.round(this.timescale/60); + + this.mp4track={ + id:BaseRemuxer.getTrackID(), + type: 'video', + len:0, + fragmented:true, + sps:'', + pps:'', + width:0, + height:0, + timescale: timescale, + duration: timescale, + samples: [] + }; + this.samples = []; + this.lastGopDTS = -99999999999999; + this.gop=[]; + this.firstUnit = true; + + this.h264 = new H264Parser(this); + + if (params.sps) { + let arr = new Uint8Array(params.sps); + if ((arr[0] & 0x1f) === 7) { + this.setSPS(arr); + } else { + Log$4.warn("bad SPS in SDP"); + } + } + if (params.pps) { + let arr = new Uint8Array(params.pps); + if ((arr[0] & 0x1f) === 8) { + this.setPPS(arr); + } else { + Log$4.warn("bad PPS in SDP"); + } + } + + if (this.mp4track.pps && this.mp4track.sps) { + this.readyToDecode = true; + } + } + + _scaled(timestamp) { + return timestamp >>> this.scaleFactor; + } + + _unscaled(timestamp) { + return timestamp << this.scaleFactor; + } + + setSPS(sps) { + this.h264.parseSPS(sps); + } + + setPPS(pps) { + this.h264.parsePPS(pps); + } + + remux(nalu) { + if (this.lastGopDTS < nalu.dts) { + this.gop.sort(BaseRemuxer.dtsSortFunc); + + if (this.gop.length > 1) { + // Aggregate multi-slices which belong to one frame + const groupedGop = BaseRemuxer.groupByDts(this.gop); + this.gop = Object.values(groupedGop).map(group => { + return group.reduce((preUnit, curUnit) => { + const naluData = curUnit.getData(); + naluData.set(new Uint8Array([0x0, 0x0, 0x0, 0x1])); + preUnit.appendData(naluData); + return preUnit; + }); + }); + } + + for (let unit of this.gop) { + // if (this.firstUnit) { + // unit.ntype = 5;//NALU.IDR; + // this.firstUnit = false; + // } + if (super.remux.call(this, unit)) { + this.mp4track.len += unit.getSize(); + } + } + this.gop = []; + this.lastGopDTS = nalu.dts; + } + if (this.h264.parseNAL(nalu)) { + this.gop.push(nalu); + } + } + + getPayload() { + if (!this.getPayloadBase()) { + return null; + } + + let payload = new Uint8Array(this.mp4track.len); + let offset = 0; + let samples=this.mp4track.samples; + let mp4Sample, lastDTS, pts, dts; + + + // Log.debug(this.samples.map((e)=>{ + // return Math.round((e.dts - this.initDTS)); + // })); + + // let minDuration = Number.MAX_SAFE_INTEGER; + while (this.samples.length) { + let sample = this.samples.shift(); + if (sample === null) { + // discontinuity + this.nextDts = undefined; + break; + } + + let unit = sample.unit; + + pts = sample.pts- this.initDTS; // /*Math.round(*/(sample.pts - this.initDTS)/*/this.tsAlign)*this.tsAlign*/; + dts = sample.dts - this.initDTS; ///*Math.round(*/(sample.dts - this.initDTS)/*/this.tsAlign)*this.tsAlign*/; + // ensure DTS is not bigger than PTS + dts = Math.min(pts,dts); + // if not first AVC sample of video track, normalize PTS/DTS with previous sample value + // and ensure that sample duration is positive + if (lastDTS !== undefined) { + let sampleDuration = this.scaled(dts - lastDTS); + // Log.debug(`Sample duration: ${sampleDuration}`); + if (sampleDuration < 0) { + Log$4.log(`invalid AVC sample duration at PTS/DTS: ${pts}/${dts}|lastDTS: ${lastDTS}:${sampleDuration}`); + this.mp4track.len -= unit.getSize(); + continue; + } + // minDuration = Math.min(sampleDuration, minDuration); + this.lastDurations.push(sampleDuration); + if (this.lastDurations.length > 100) { + this.lastDurations.shift(); + } + mp4Sample.duration = sampleDuration; + } else { + if (this.nextDts) { + let delta = dts - this.nextDts; + // if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments + if (/*contiguous ||*/ Math.abs(Math.round(BaseRemuxer.toMS(delta))) < 600) { + + if (delta) { + // set DTS to next DTS + // Log.debug(`Video/PTS/DTS adjusted: ${pts}->${Math.max(pts - delta, this.nextDts)}/${dts}->${this.nextDts},delta:${delta}`); + dts = this.nextDts; + // offset PTS as well, ensure that PTS is smaller or equal than new DTS + pts = Math.max(pts - delta, dts); + } + } else { + if (delta < 0) { + Log$4.log(`skip frame from the past at DTS=${dts} with expected DTS=${this.nextDts}`); + this.mp4track.len -= unit.getSize(); + continue; + } + } + } + // remember first DTS of our avcSamples, ensure value is positive + this.firstDTS = Math.max(0, dts); + } + + mp4Sample = { + size: unit.getSize(), + duration: 0, + cts: this.scaled(pts - dts), + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0 + } + }; + let flags = mp4Sample.flags; + if (sample.unit.isKeyframe() === true) { + // the current sample is a key frame + flags.dependsOn = 2; + flags.isNonSync = 0; + } else { + flags.dependsOn = 1; + flags.isNonSync = 1; + } + + payload.set(unit.getData(), offset); + offset += unit.getSize(); + + samples.push(mp4Sample); + lastDTS = dts; + } + + if (!samples.length) return null; + + let avgDuration = this.lastDurations.reduce(function(a, b) { return (a|0) + (b|0); }, 0) / (this.lastDurations.length||1)|0; + if (samples.length >= 2) { + this.lastSampleDuration = avgDuration; + mp4Sample.duration = avgDuration; + } else { + mp4Sample.duration = this.lastSampleDuration; + } + + if(samples.length && (!this.nextDts || navigator.userAgent.toLowerCase().indexOf('chrome') > -1)) { + let flags = samples[0].flags; + // chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue + // https://code.google.com/p/chromium/issues/detail?id=229412 + flags.dependsOn = 2; + flags.isNonSync = 0; + } + + // next AVC sample DTS should be equal to last sample DTS + last sample duration + this.nextDts = dts + this.unscaled(this.lastSampleDuration); + // Log.debug(`next dts: ${this.nextDts}, last duration: ${this.lastSampleDuration}, last dts: ${dts}`); + + return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } + } + + class PayloadType { + static get H264() {return 1;} + static get AAC() {return 2;} + + static get map() {return { + [PayloadType.H264]: 'video', + [PayloadType.AAC]: 'audio' + }}; + + static get string_map() {return { + H264: PayloadType.H264, + AAC: PayloadType.AAC, + 'MP4A-LATM': PayloadType.AAC, + 'MPEG4-GENERIC': PayloadType.AAC + }} + } + + const LOG_TAG$1 = "remuxer"; + const Log$5 = getTagged(LOG_TAG$1); + + class Remuxer { + static get TrackConverters() {return { + [PayloadType.H264]: H264Remuxer, + [PayloadType.AAC]: AACRemuxer + }}; + + static get TrackScaleFactor() {return { + [PayloadType.H264]: 1,//4, + [PayloadType.AAC]: 0 + }}; + + static get TrackTimescale() {return { + [PayloadType.H264]: 90000,//22500, + [PayloadType.AAC]: 0 + }}; + + constructor(mediaElement) { + this.mse = new MSE([mediaElement]); + this.eventSource = new EventEmitter(); + this.mseEventSource = new EventSourceWrapper(this.mse.eventSource); + this.mse_ready = true; + + this.reset(); + + this.errorListener = this.mseClose.bind(this); + this.closeListener = this.mseClose.bind(this); + this.errorDecodeListener = this.mseErrorDecode.bind(this); + + this.eventSource.addEventListener('ready', this.init.bind(this)); + } + + initMSEHandlers() { + this.mseEventSource.on('error', this.errorListener); + this.mseEventSource.on('sourceclosed', this.closeListener); + this.mseEventSource.on('errordecode', this.errorDecodeListener); + } + + async reset() { + this.tracks = {}; + this.initialized = false; + this.initSegments = {}; + this.codecs = []; + this.streams = {}; + this.enabled = false; + await this.mse.clear(); + this.initMSEHandlers(); + } + + destroy() { + this.mseEventSource.destroy(); + this.mse.destroy(); + this.mse = null; + + this.detachClient(); + + this.eventSource.destroy(); + } + + onTracks(tracks) { + Log$5.debug(`ontracks: `, tracks.detail); + // store available track types + for (let track of tracks.detail) { + this.tracks[track.type] = new Remuxer.TrackConverters[track.type](Remuxer.TrackTimescale[track.type], Remuxer.TrackScaleFactor[track.type], track.params); + if (track.offset) { + this.tracks[track.type].timeOffset = track.offset; + } + if (track.duration) { + this.tracks[track.type].mp4track.duration = track.duration*(this.tracks[track.type].timescale || Remuxer.TrackTimescale[track.type]); + this.tracks[track.type].duration = track.duration; + } else { + this.tracks[track.type].duration = 1; + } + + // this.tracks[track.type].duration + } + this.mse.setLive(!this.client.seekable); + } + + setTimeOffset(timeOffset, track) { + if (this.tracks[track.type]) { + this.tracks[track.type].timeOffset = timeOffset;///this.tracks[track.type].scaleFactor; + } + } + + init() { + let tracks = []; + this.codecs = []; + let initmse = []; + let initPts = Infinity; + let initDts = Infinity; + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + if (!MSE.isSupported([track.mp4track.codec])) { + throw new Error(`${track.mp4track.type} codec ${track.mp4track.codec} is not supported`); + } + tracks.push(track.mp4track); + this.codecs.push(track.mp4track.codec); + track.init(initPts, initDts/*, false*/); + // initPts = Math.min(track.initPTS, initPts); + // initDts = Math.min(track.initDTS, initDts); + } + + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + //track.init(initPts, initDts); + this.initSegments[track_type] = MP4.initSegment([track.mp4track], track.duration*track.timescale, track.timescale); + initmse.push(this.initMSE(track_type, track.mp4track.codec)); + } + this.initialized = true; + return Promise.all(initmse).then(()=>{ + //this.mse.play(); + this.enabled = true; + }); + + } + + initMSE(track_type, codec) { + if (MSE.isSupported(this.codecs)) { + return this.mse.setCodec(track_type, `${PayloadType.map[track_type]}/mp4; codecs="${codec}"`).then(()=>{ + this.mse.feed(track_type, this.initSegments[track_type]); + // this.mse.play(); + // this.enabled = true; + }); + } else { + throw new Error('Codecs are not supported'); + } + } + + mseClose() { + // this.mse.clear(); + this.client.stop(); + this.eventSource.dispatchEvent('stopped'); + } + + mseErrorDecode() { + if(this.tracks[2]) { + console.warn(this.tracks[2].mp4track.type); + this.mse.buffers[2].destroy(); + delete this.tracks[2]; + } + } + + flush() { + this.onSamples(); + + if (!this.initialized) { + // Log.debug(`Initialize...`); + if (Object.keys(this.tracks).length) { + for (let track_type in this.tracks) { + if (!this.tracks[track_type].readyToDecode || !this.tracks[track_type].samples.length) return; + Log$5.debug(`Init MSE for track ${this.tracks[track_type].mp4track.type}`); + } + this.eventSource.dispatchEvent('ready'); + } + } else { + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + let pay = track.getPayload(); + if (pay && pay.byteLength) { + this.mse.feed(track_type, [MP4.moof(track.seq, track.scaled(track.firstDTS), track.mp4track), MP4.mdat(pay)]); + track.flush(); + } + } + } + } + + onSamples(ev) { + // TODO: check format + // let data = ev.detail; + // if (this.tracks[data.pay] && this.client.sampleQueues[data.pay].length) { + // console.log(`video ${data.units[0].dts}`); + for (let qidx in this.client.sampleQueues) { + let queue = this.client.sampleQueues[qidx]; + while (queue.length) { + let units = queue.shift(); + if(units){ + for (let chunk of units) { + if(this.tracks[qidx]) { + this.tracks[qidx].remux(chunk); + } + } + } else { + if (!this.initialized) { + delete this.tracks[qidx]; + } + } + } + } + // } + } + + onAudioConfig(ev) { + if (this.tracks[ev.detail.pay]) { + this.tracks[ev.detail.pay].setConfig(ev.detail.config); + } + } + + attachClient(client) { + this.detachClient(); + this.client = client; + this.clientEventSource = new EventSourceWrapper(this.client.eventSource); + this.clientEventSource.on('samples', this.samplesListener); + this.clientEventSource.on('audio_config', this.audioConfigListener); + this.clientEventSource.on('tracks', this.onTracks.bind(this)); + this.clientEventSource.on('flush', this.flush.bind(this)); + this.clientEventSource.on('clear', ()=>{ + this.reset(); + this.mse.clear().then(()=>{ + //this.mse.play(); + this.initMSEHandlers(); + }); + }); + } + + detachClient() { + if (this.client) { + this.clientEventSource.destroy(); + // this.client.eventSource.removeEventListener('samples', this.onSamples.bind(this)); + // this.client.eventSource.removeEventListener('audio_config', this.onAudioConfig.bind(this)); + // // TODO: clear other listeners + // this.client.eventSource.removeEventListener('clear', this._clearListener); + // this.client.eventSource.removeEventListener('tracks', this._tracksListener); + // this.client.eventSource.removeEventListener('flush', this._flushListener); + this.client = null; + } + } + } + + class BaseTransport { + constructor(endpoint, stream_type, config={}) { + this.stream_type = stream_type; + this.endpoint = endpoint; + this.eventSource = new EventEmitter(); + this.dataQueue = []; + } + + static canTransfer(stream_type) { + return BaseTransport.streamTypes().includes(stream_type); + } + + static streamTypes() { + return []; + } + + destroy() { + this.eventSource.destroy(); + } + + connect() { + // TO be impemented + } + + disconnect() { + // TO be impemented + } + + reconnect() { + return this.disconnect().then(()=>{ + return this.connect(); + }); + } + + setEndpoint(endpoint) { + this.endpoint = endpoint; + return this.reconnect(); + } + + send(data) { + // TO be impemented + // return this.prepare(data).send(); + } + + prepare(data) { + // TO be impemented + // return new Request(data); + } + + // onData(type, data) { + // this.eventSource.dispatchEvent(type, data); + // } + } + + const LOG_TAG$2 = "transport:ws"; + const Log$6 = getTagged(LOG_TAG$2); + + function bytesToString(data) { + let len = data.length; + let dataString = ""; + for (let i = 0; i < len; i++) { + dataString += String.fromCharCode(data[i]); + } + return dataString + } + + class WebsocketTransport extends BaseTransport { + constructor(endpoint, stream_type, options={ + socket:`${location.protocol.replace('http', 'ws')}//${location.host}/ws/`, + workers: 1 + }) { + super(endpoint, stream_type); + this.socket_url = options.socket; + + this.wssocket = undefined; + this.awaitingPromises = []; + + this.ready = this.connect(); + } + + destroy() { + return this.disconnect().then(()=>{ + return super.destroy(); + }); + + } + + static canTransfer(stream_type) { + return WebsocketTransport.streamTypes().includes(stream_type); + } + + static streamTypes() { + return ['rtsp']; + } + + _connect() { + return new Promise((resolve, reject)=>{ + this.wssocket = new WebSocket(this.socket_url, 'rtsp'); + this.wssocket.binaryType = 'arraybuffer'; + + this.wssocket.onopen = ()=>{ + Log$6.log('websocket opened'); + this.eventSource.dispatchEvent('connected'); + resolve(); + }; + + this.wssocket.onmessage = (ev)=>{ + let data = new Uint8Array(ev.data); + let prefix = data[0]; + if (prefix == 36) { // rtpData + this.dataQueue.push(data); + this.eventSource.dispatchEvent('data'); + } else if (prefix == 82){ // rtsp response + let rtspResp = bytesToString(data); + if (this.awaitingPromises.length) { + this.awaitingPromises.shift().resolve(rtspResp); + } + } else{ + Log$6.warn('无效包', prefix); + } + }; + + this.wssocket.onerror = (e)=>{ + Log$6.error(`${e.type}. code: ${e.code}, reason: ${e.reason || 'unknown reason'}`); + this.wssocket.close(); + this.eventSource.dispatchEvent('error', {code: e.code, reason: e.reason || 'unknown reason'}); + }; + + this.wssocket.onclose = (e)=>{ + Log$6.log(`${e.type}. code: ${e.code}, reason: ${e.reason || 'unknown reason'}`); + this.wssocket.onclose = null; + this.wssocket.close(); + this.eventSource.dispatchEvent('disconnected', {code: e.code, reason: e.reason || 'unknown reason'}); + }; + }); + } + + connect() { + return this.disconnect().then((resolve, reject)=>{ + return this._connect() + }); + } + + disconnect() { + return new Promise((resolve, reject)=>{ + if (this.wssocket){ + this.wssocket.onclose = ()=>{ + Log$6.log('websocket closed'); + }; + this.wssocket.close(); + resolve(); + } + else{ + resolve(); + } + }); + } + + send(_data) { + if (!this.wssocket) { + throw new Error('websocekt disconnected'); + } + + return new Promise((resolve, reject)=>{ + this.awaitingPromises.push({resolve:resolve, reject:reject}); + this.wssocket.send(_data); + }); + } + } + + class State { + constructor(name, stateMachine) { + this.stateMachine = stateMachine; + this.transitions = new Set(); + this.name = name; + } + + + activate() { + return Promise.resolve(null); + } + + finishTransition() {} + + failHandler() {} + + deactivate() { + return Promise.resolve(null); + } + } + + class StateMachine { + constructor() { + this.storage = {}; + this.currentState = null; + this.states = new Map(); + } + + addState(name, {activate, finishTransition, deactivate}) { + let state = new State(name, this); + if (activate) state.activate = activate; + if (finishTransition) state.finishTransition = finishTransition; + if (deactivate) state.deactivate = deactivate; + this.states.set(name, state); + return this; + } + + addTransition(fromName, toName){ + if (!this.states.has(fromName)) { + throw ReferenceError(`No such state: ${fromName} while connecting to ${toName}`); + } + if (!this.states.has(toName)) { + throw ReferenceError(`No such state: ${toName} while connecting from ${fromName}`); + } + this.states.get(fromName).transitions.add(toName); + return this; + } + + _promisify(res) { + let promise; + try { + promise = res; + if (!promise.then) { + promise = Promise.resolve(promise); + } + } catch (e) { + promise = Promise.reject(e); + } + return promise; + } + + transitionTo(stateName) { + if (this.currentState == null) { + let state = this.states.get(stateName); + return this._promisify(state.activate.call(this)) + .then((data)=> { + this.currentState = state; + return data; + }).then(state.finishTransition.bind(this)).catch((e)=>{ + state.failHandler(); + throw e; + }); + } + if (this.currentState.name == stateName) return Promise.resolve(); + if (this.currentState.transitions.has(stateName)) { + let state = this.states.get(stateName); + return this._promisify(state.deactivate.call(this)) + .then(state.activate.bind(this)).then((data)=> { + this.currentState = state; + return data; + }).then(state.finishTransition.bind(this)).catch((e)=>{ + state.failHandler(); + throw e; + }); + } else { + return Promise.reject(`No such transition: ${this.currentState.name} to ${stateName}`); + } + } + + } + + const Log$7 = getTagged("parser:sdp"); + + class SDPParser { + constructor() { + this.version = -1; + this.origin = null; + this.sessionName = null; + this.timing = null; + this.sessionBlock = {}; + this.media = {}; + this.tracks = {}; + this.mediaMap = {}; + } + + parse(content) { + // Log.debug(content); + return new Promise((resolve, reject) => { + var dataString = content; + var success = true; + var currentMediaBlock = this.sessionBlock; + + // TODO: multiple audio/video tracks + + for (let line of dataString.split("\n")) { + line = line.replace(/\r/, ''); + if (0 === line.length) { + /* Empty row (last row perhaps?), skip to next */ + continue; + } + + switch (line.charAt(0)) { + case 'v': + if (-1 !== this.version) { + Log$7.log('Version present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseVersion(line); + break; + + case 'o': + if (null !== this.origin) { + Log$7.log('Origin present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseOrigin(line); + break; + + case 's': + if (null !== this.sessionName) { + Log$7.log('Session Name present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseSessionName(line); + break; + + case 't': + if (null !== this.timing) { + Log$7.log('Timing present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseTiming(line); + break; + + case 'm': + if (null !== currentMediaBlock && this.sessionBlock !== currentMediaBlock) { + /* Complete previous block and store it */ + this.media[currentMediaBlock.type] = currentMediaBlock; + } + + /* A wild media block appears */ + currentMediaBlock = {}; + currentMediaBlock.rtpmap = {}; + this._parseMediaDescription(line, currentMediaBlock); + break; + + case 'a': + SDPParser._parseAttribute(line, currentMediaBlock); + break; + + default: + Log$7.log('Ignored unknown SDP directive: ' + line); + break; + } + + if (!success) { + reject(); + return; + } + } + + this.media[currentMediaBlock.type] = currentMediaBlock; + + success ? resolve() : reject(); + }); + } + + _parseVersion(line) { + let matches = line.match(/^v=([0-9]+)$/); + if (!matches || !matches.length) { + Log$7.log('\'v=\' (Version) formatted incorrectly: ' + line); + return false; + } + + this.version = matches[1]; + if (0 != this.version) { + Log$7.log('Unsupported SDP version:' + this.version); + return false; + } + + return true; + } + + _parseOrigin(line) { + let matches = line.match(/^o=([^ ]+) (-?[0-9]+) (-?[0-9]+) (IN) (IP4|IP6) ([^ ]+)$/); + if (!matches || !matches.length) { + Log$7.log('\'o=\' (Origin) formatted incorrectly: ' + line); + return false; + } + + this.origin = {}; + this.origin.username = matches[1]; + this.origin.sessionid = matches[2]; + this.origin.sessionversion = matches[3]; + this.origin.nettype = matches[4]; + this.origin.addresstype = matches[5]; + this.origin.unicastaddress = matches[6]; + + return true; + } + + _parseSessionName(line) { + let matches = line.match(/^s=([^\r\n]+)$/); + if (!matches || !matches.length) { + Log$7.log('\'s=\' (Session Name) formatted incorrectly: ' + line); + return false; + } + + this.sessionName = matches[1]; + + return true; + } + + _parseTiming(line) { + let matches = line.match(/^t=([0-9]+) ([0-9]+)$/); + if (!matches || !matches.length) { + Log$7.log('\'t=\' (Timing) formatted incorrectly: ' + line); + return false; + } + + this.timing = {}; + this.timing.start = matches[1]; + this.timing.stop = matches[2]; + + return true; + } + + _parseMediaDescription(line, media) { + let matches = line.match(/^m=([^ ]+) ([^ ]+) ([^ ]+)[ ]/); + if (!matches || !matches.length) { + Log$7.log('\'m=\' (Media) formatted incorrectly: ' + line); + return false; + } + + media.type = matches[1]; + media.port = matches[2]; + media.proto = matches[3]; + media.fmt = line.substr(matches[0].length).split(' ').map(function (fmt, index, array) { + return parseInt(fmt); + }); + + for (let fmt of media.fmt) { + this.mediaMap[fmt] = media; + } + + return true; + } + + static _parseAttribute(line, media) { + if (null === media) { + /* Not in a media block, can't be bothered parsing attributes for session */ + return true; + } + + var matches; + /* Used for some cases of below switch-case */ + var separator = line.indexOf(':'); + var attribute = line.substr(0, (-1 === separator) ? 0x7FFFFFFF : separator); + /* 0x7FF.. is default */ + + switch (attribute) { + case 'a=recvonly': + case 'a=sendrecv': + case 'a=sendonly': + case 'a=inactive': + media.mode = line.substr('a='.length); + break; + case 'a=range': + matches = line.match(/^a=range:\s*([a-zA-Z-]+)=([0-9.]+|now)\s*-\s*([0-9.]*)$/); + media.range = [Number(matches[2] == "now" ? -1 : matches[2]), Number(matches[3]), matches[1]]; + break; + case 'a=control': + media.control = line.substr('a=control:'.length); + break; + + case 'a=rtpmap': + matches = line.match(/^a=rtpmap:(\d+) (.*)$/); + if (null === matches) { + Log$7.log('Could not parse \'rtpmap\' of \'a=\''); + return false; + } + + var payload = parseInt(matches[1]); + media.rtpmap[payload] = {}; + + var attrs = matches[2].split('/'); + media.rtpmap[payload].name = attrs[0].toUpperCase(); + media.rtpmap[payload].clock = attrs[1]; + if (undefined !== attrs[2]) { + media.rtpmap[payload].encparams = attrs[2]; + } + media.ptype = PayloadType.string_map[attrs[0].toUpperCase()]; + + break; + + case 'a=fmtp': + matches = line.match(/^a=fmtp:(\d+) (.*)$/); + if (0 === matches.length) { + Log$7.log('Could not parse \'fmtp\' of \'a=\''); + return false; + } + + media.fmtp = {}; + for (var param of matches[2].split(';')) { + var idx = param.indexOf('='); + media.fmtp[param.substr(0, idx).toLowerCase().trim()] = param.substr(idx + 1).trim(); + } + break; + } + + return true; + } + + getSessionBlock() { + return this.sessionBlock; + } + + hasMedia(mediaType) { + return this.media[mediaType] != undefined; + } + + getMediaBlock(mediaType) { + return this.media[mediaType]; + } + + getMediaBlockByPayloadType(pt) { + // for (var m in this.media) { + // if (-1 !== this.media[m].fmt.indexOf(pt)) { + // return this.media[m]; + // } + // } + return this.mediaMap[pt] || null; + + //ErrorManager.dispatchError(826, [pt], true); + // Log.error(`failed to find media with payload type ${pt}`); + // + // return null; + } + + getMediaBlockList() { + var res = []; + for (var m in this.media) { + res.push(m); + } + + return res; + } + } + + const LOG_TAG$3 = "rtsp:stream"; + const Log$8 = getTagged(LOG_TAG$3); + + class RTSPStream { + + constructor(client, track) { + this.state = null; + this.client = client; + this.track = track; + this.rtpChannel = 1; + + this.stopKeepAlive(); + this.keepaliveInterval = null; + this.keepaliveTime = 30000; + } + + reset() { + this.stopKeepAlive(); + this.client.forgetRTPChannel(this.rtpChannel); + this.client = null; + this.track = null; + } + + start(lastSetupPromise = null) { + if (lastSetupPromise != null) { + // if a setup was already made, use the same session + return lastSetupPromise.then((obj) => this.sendSetup(obj.session)) + } else { + return this.sendSetup(); + } + } + + stop() { + return this.sendTeardown(); + } + + getSetupURL(track) { + let sessionBlock = this.client.sdp.getSessionBlock(); + if (Url.isAbsolute(track.control)) { + return track.control; + } else if (Url.isAbsolute(`${sessionBlock.control}${track.control}`)) { + return `${sessionBlock.control}${track.control}`; + } else if (Url.isAbsolute(`${this.client.contentBase}${track.control}`)) { + /* Should probably check session level control before this */ + return `${this.client.contentBase}${track.control}`; + } + else {//need return default + return track.control; + } + Log$8.error('Can\'t determine track URL from ' + + 'block.control:' + track.control + ', ' + + 'session.control:' + sessionBlock.control + ', and ' + + 'content-base:' + this.client.contentBase); + } + + getControlURL() { + let ctrl = this.client.sdp.getSessionBlock().control; + if (Url.isAbsolute(ctrl)) { + return ctrl; + } else if (!ctrl || '*' === ctrl) { + return this.client.contentBase; + } else { + return `${this.client.contentBase}${ctrl}`; + } + } + + sendKeepalive() { + if (this.client.methods.includes('GET_PARAMETER')) { + return this.client.sendRequest('GET_PARAMETER', this.getSetupURL(this.track), { + 'Session': this.session + }); + } else { + return this.client.sendRequest('OPTIONS', '*'); + } + } + + stopKeepAlive() { + clearInterval(this.keepaliveInterval); + } + + startKeepAlive() { + this.keepaliveInterval = setInterval(() => { + this.sendKeepalive().catch((e) => { + // Log.error(e); + if (e instanceof RTSPError) { + if (Number(e.data.parsed.code) == 501) { + return; + } + } + this.client.reconnect(); + }); + }, this.keepaliveTime); + } + + sendRequest(_cmd, _params = {}) { + let params = {}; + if (this.session) { + params['Session'] = this.session; + } + Object.assign(params, _params); + return this.client.sendRequest(_cmd, this.getControlURL(), params); + } + + sendSetup(session = null) { + this.state = RTSPClientSM.STATE_SETUP; + this.rtpChannel = this.client.interleaveChannelIndex; + let interleavedChannels = this.client.interleaveChannelIndex++ + "-" + this.client.interleaveChannelIndex++; + let params = { + 'Transport': `RTP/AVP/TCP;unicast;interleaved=${interleavedChannels}`, + 'Date': new Date().toUTCString() + }; + if(session){ + params.Session = session; + } + return this.client.sendRequest('SETUP', this.getSetupURL(this.track), params).then((_data) => { + this.session = _data.headers['session']; + let transport = _data.headers['transport']; + if (transport) { + let interleaved = transport.match(/interleaved=([0-9]+)-([0-9]+)/)[1]; + if (interleaved) { + this.rtpChannel = Number(interleaved); + } + } + let sessionParamsChunks = this.session.split(';').slice(1); + let sessionParams = {}; + for (let chunk of sessionParamsChunks) { + let kv = chunk.split('='); + sessionParams[kv[0]] = kv[1]; + } + if (sessionParams['timeout']) { + this.keepaliveInterval = Number(sessionParams['timeout']) * 500; // * 1000 / 2 + } + /*if (!/RTP\/AVP\/TCP;unicast;interleaved=/.test(_data.headers["transport"])) { + // TODO: disconnect stream and notify client + throw new Error("Connection broken"); + }*/ + this.client.useRTPChannel(this.rtpChannel); + this.startKeepAlive(); + return {track: this.track, data: _data, session: this.session}; + }); + } + } + + /* + * JavaScript MD5 + * https://github.com/blueimp/JavaScript-MD5 + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + * + * Based on + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + + + /* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ + function safeAdd(x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF) + } + + /* + * Bitwise rotate a 32-bit number to the left. + */ + function bitRotateLeft(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)) + } + + /* + * These functions implement the four basic operations the algorithm uses. + */ + function md5cmn(q, a, b, x, s, t) { + return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b) + } + function md5ff(a, b, c, d, x, s, t) { + return md5cmn((b & c) | ((~b) & d), a, b, x, s, t) + } + function md5gg(a, b, c, d, x, s, t) { + return md5cmn((b & d) | (c & (~d)), a, b, x, s, t) + } + function md5hh(a, b, c, d, x, s, t) { + return md5cmn(b ^ c ^ d, a, b, x, s, t) + } + function md5ii(a, b, c, d, x, s, t) { + return md5cmn(c ^ (b | (~d)), a, b, x, s, t) + } + + /* + * Calculate the MD5 of an array of little-endian words, and a bit length. + */ + function binlMD5(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << (len % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var i; + var olda; + var oldb; + var oldc; + var oldd; + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for (i = 0; i < x.length; i += 16) { + olda = a; + oldb = b; + oldc = c; + oldd = d; + + a = md5ff(a, b, c, d, x[i], 7, -680876936); + d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); + c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); + b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330); + a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); + d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); + c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); + b = md5ff(b, c, d, a, x[i + 7], 22, -45705983); + a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); + d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); + c = md5ff(c, d, a, b, x[i + 10], 17, -42063); + b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162); + a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); + d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); + c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); + b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329); + + a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); + d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); + c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); + b = md5gg(b, c, d, a, x[i], 20, -373897302); + a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); + d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); + c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); + b = md5gg(b, c, d, a, x[i + 4], 20, -405537848); + a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); + d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); + c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); + b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501); + a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); + d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); + c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); + b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734); + + a = md5hh(a, b, c, d, x[i + 5], 4, -378558); + d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); + c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); + b = md5hh(b, c, d, a, x[i + 14], 23, -35309556); + a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); + d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); + c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); + b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640); + a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); + d = md5hh(d, a, b, c, x[i], 11, -358537222); + c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); + b = md5hh(b, c, d, a, x[i + 6], 23, 76029189); + a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); + d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); + c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); + b = md5hh(b, c, d, a, x[i + 2], 23, -995338651); + + a = md5ii(a, b, c, d, x[i], 6, -198630844); + d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); + c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); + b = md5ii(b, c, d, a, x[i + 5], 21, -57434055); + a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); + d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); + c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); + b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799); + a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); + d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); + c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); + b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649); + a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); + d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); + c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); + b = md5ii(b, c, d, a, x[i + 9], 21, -343485551); + + a = safeAdd(a, olda); + b = safeAdd(b, oldb); + c = safeAdd(c, oldc); + d = safeAdd(d, oldd); + } + return [a, b, c, d] + } + + /* + * Convert an array of little-endian words to a string + */ + function binl2rstr(input) { + var i; + var output = ''; + var length32 = input.length * 32; + for (i = 0; i < length32; i += 8) { + output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF); + } + return output + } + + /* + * Convert a raw string to an array of little-endian words + * Characters >255 have their high-byte silently ignored. + */ + function rstr2binl(input) { + var i; + var output = []; + output[(input.length >> 2) - 1] = undefined; + for (i = 0; i < output.length; i += 1) { + output[i] = 0; + } + var length8 = input.length * 8; + for (i = 0; i < length8; i += 8) { + output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32); + } + return output + } + + /* + * Calculate the MD5 of a raw string + */ + function rstrMD5(s) { + return binl2rstr(binlMD5(rstr2binl(s), s.length * 8)) + } + + /* + * Calculate the HMAC-MD5, of a key and some data (raw strings) + */ + function rstrHMACMD5(key, data) { + var i; + var bkey = rstr2binl(key); + var ipad = []; + var opad = []; + var hash; + ipad[15] = opad[15] = undefined; + if (bkey.length > 16) { + bkey = binlMD5(bkey, key.length * 8); + } + for (i = 0; i < 16; i += 1) { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); + return binl2rstr(binlMD5(opad.concat(hash), 512 + 128)) + } + + /* + * Convert a raw string to a hex string + */ + function rstr2hex(input) { + var hexTab = '0123456789abcdef'; + var output = ''; + var x; + var i; + for (i = 0; i < input.length; i += 1) { + x = input.charCodeAt(i); + output += hexTab.charAt((x >>> 4) & 0x0F) + + hexTab.charAt(x & 0x0F); + } + return output + } + + /* + * Encode a string as utf-8 + */ + function str2rstrUTF8(input) { + return unescape(encodeURIComponent(input)) + } + + /* + * Take string arguments and return either raw or hex encoded strings + */ + function rawMD5(s) { + return rstrMD5(str2rstrUTF8(s)) + } + function hexMD5(s) { + return rstr2hex(rawMD5(s)) + } + function rawHMACMD5(k, d) { + return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d)) + } + function hexHMACMD5(k, d) { + return rstr2hex(rawHMACMD5(k, d)) + } + + function md5(string, key, raw) { + if (!key) { + if (!raw) { + return hexMD5(string) + } + return rawMD5(string) + } + if (!raw) { + return hexHMACMD5(key, string) + } + return rawHMACMD5(key, string) + } + + // TODO: asm.js + class RTP { + constructor(pkt/*uint8array*/, sdp) { + let bytes = new DataView(pkt.buffer, pkt.byteOffset, pkt.byteLength); + + this.version = bytes.getUint8(0) >>> 6; + this.padding = bytes.getUint8(0) & 0x20 >>> 5; + this.has_extension = bytes.getUint8(0) & 0x10 >>> 4; + this.csrc = bytes.getUint8(0) & 0x0F; + this.marker = bytes.getUint8(1) >>> 7; + this.pt = bytes.getUint8(1) & 0x7F; + this.sequence = bytes.getUint16(2) ; + this.timestamp = bytes.getUint32(4); + this.ssrc = bytes.getUint32(8); + this.csrcs = []; + + let pktIndex=12; + if (this.csrc>0) { + this.csrcs.push(bytes.getUint32(pktIndex)); + pktIndex+=4; + } + if (this.has_extension==1) { + this.extension = bytes.getUint16(pktIndex); + this.ehl = bytes.getUint16(pktIndex+2); + pktIndex+=4; + this.header_data = pkt.slice(pktIndex, this.ehl); + pktIndex += this.ehl; + } + + this.headerLength = pktIndex; + let padLength = 0; + if (this.padding) { + padLength = bytes.getUint8(pkt.byteLength-1); + } + + // this.bodyLength = pkt.byteLength-this.headerLength-padLength; + + this.media = sdp.getMediaBlockByPayloadType(this.pt); + if (null === this.media) { + Log.log(`Media description for payload type: ${this.pt} not provided.`); + } else { + this.type = this.media.ptype;//PayloadType.string_map[this.media.rtpmap[this.media.fmt[0]].name]; + } + + this.data = pkt.subarray(pktIndex); + // this.timestamp = 1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); + // console.log(this); + } + getPayload() { + return this.data; + } + + getTimestampMS() { + return this.timestamp; //1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); + } + + toString() { + return "RTP(" + + "version:" + this.version + ", " + + "padding:" + this.padding + ", " + + "has_extension:" + this.has_extension + ", " + + "csrc:" + this.csrc + ", " + + "marker:" + this.marker + ", " + + "pt:" + this.pt + ", " + + "sequence:" + this.sequence + ", " + + "timestamp:" + this.timestamp + ", " + + "ssrc:" + this.ssrc + ")"; + } + + isVideo(){return this.media.type == 'video';} + isAudio(){return this.media.type == 'audio';} + + + } + + class RTPFactory { + constructor(sdp) { + this.tsOffsets={}; + for (let pay in sdp.media) { + for (let pt of sdp.media[pay].fmt) { + this.tsOffsets[pt] = {last: 0, overflow: 0}; + } + } + } + + build(pkt/*uint8array*/, sdp) { + let rtp = new RTP(pkt, sdp); + + let tsOffset = this.tsOffsets[rtp.pt]; + if (tsOffset) { + rtp.timestamp += tsOffset.overflow; + if (tsOffset.last && Math.abs(rtp.timestamp - tsOffset.last) > 0x7fffffff) { + console.log(`\nlast ts: ${tsOffset.last}\n + new ts: ${rtp.timestamp}\n + new ts adjusted: ${rtp.timestamp+0xffffffff}\n + last overflow: ${tsOffset.overflow}\n + new overflow: ${tsOffset.overflow+0xffffffff}\n + `); + tsOffset.overflow += 0xffffffff; + rtp.timestamp += 0xffffffff; + } + /*if (rtp.timestamp>0xffffffff) { + console.log(`ts: ${rtp.timestamp}, seq: ${rtp.sequence}`); + }*/ + tsOffset.last = rtp.timestamp; + } + + return rtp; + } + } + + class RTSPMessage { + static get RTSP_1_0() {return "RTSP/1.0";} + + constructor(_rtsp_version) { + this.version = _rtsp_version; + } + + build(_cmd, _host, _params={}, _payload=null) { + let requestString = `${_cmd} ${_host} ${this.version}\r\n`; + for (let param in _params) { + requestString+=`${param}: ${_params[param]}\r\n`; + } + // TODO: binary payload + if (_payload) { + requestString+=`Content-Length: ${_payload.length}\r\n`; + } + requestString+='\r\n'; + if (_payload) { + requestString+=_payload; + } + return requestString; + } + + parse(_data) { + let lines = _data.split('\r\n'); + let parsed = { + headers:{}, + body:null, + code: 0, + statusLine: '' + }; + + let match; + [match, parsed.code, parsed.statusLine] = lines[0].match(new RegExp(`${this.version}[ ]+([0-9]{3})[ ]+(.*)`)); + parsed.code = Number(parsed.code); + let lineIdx = 1; + + while (lines[lineIdx]) { + let [k,v] = lines[lineIdx].split(/:(.+)/); + parsed.headers[k.toLowerCase()] = v.trim(); + lineIdx++; + } + + parsed.body = lines.slice(lineIdx).join('\n\r'); + + return parsed; + } + + } + + const MessageBuilder = new RTSPMessage(RTSPMessage.RTSP_1_0); + + // TODO: asm.js + class NALUAsm { + + constructor() { + this.fragmented_nalu = null; + } + + + static parseNALHeader(hdr) { + return { + nri: hdr & 0x60, + type: hdr & 0x1F + } + } + + parseSingleNALUPacket(rawData, header, dts, pts) { + return new NALU(header.type, header.nri, rawData.subarray(0), dts, pts); + } + + parseAggregationPacket(rawData, header, dts, pts) { + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + let nal_start_idx = 0; + let don = null; + if (NALU.STAP_B === header.type) { + don = data.getUint16(nal_start_idx); + nal_start_idx += 2; + } + let ret = []; + while (nal_start_idx < data.byteLength) { + let size = data.getUint16(nal_start_idx); + nal_start_idx += 2; + let header = NALUAsm.parseNALHeader(data.getInt8(nal_start_idx)); + nal_start_idx++; + let nalu = this.parseSingleNALUPacket(rawData.subarray(nal_start_idx, nal_start_idx+size), header, dts, pts); + if (nalu !== null) { + ret.push(nalu); + } + nal_start_idx+=size; + } + return ret; + } + + parseFragmentationUnit(rawData, header, dts, pts) { + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + let nal_start_idx = 0; + let fu_header = data.getUint8(nal_start_idx); + let is_start = (fu_header & 0x80) >>> 7; + let is_end = (fu_header & 0x40) >>> 6; + let payload_type = fu_header & 0x1F; + let ret = null; + + nal_start_idx++; + let don = 0; + if (NALU.FU_B === header.type) { + don = data.getUint16(nal_start_idx); + nal_start_idx += 2; + } + + if (is_start) { + this.fragmented_nalu = new NALU(payload_type, header.nri, rawData.subarray(nal_start_idx), dts, pts); + } + if (this.fragmented_nalu && this.fragmented_nalu.ntype === payload_type) { + if (!is_start) { + this.fragmented_nalu.appendData(rawData.subarray(nal_start_idx)); + } + if (is_end) { + ret = this.fragmented_nalu; + this.fragmented_nalu = null; + return ret; + } + } + return null; + } + + onNALUFragment(rawData, dts, pts) { + + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + + let header = NALUAsm.parseNALHeader(data.getUint8(0)); + + let nal_start_idx = 1; + + let unit = null; + if (header.type > 0 && header.type < 24) { + unit = this.parseSingleNALUPacket(rawData.subarray(nal_start_idx), header, dts, pts); + } else if (NALU.FU_A === header.type || NALU.FU_B === header.type) { + unit = this.parseFragmentationUnit(rawData.subarray(nal_start_idx), header, dts, pts); + } else if (NALU.STAP_A === header.type || NALU.STAP_B === header.type) { + return this.parseAggregationPacket(rawData.subarray(nal_start_idx), header, dts, pts); + } else { + /* 30 - 31 is undefined, ignore those (RFC3984). */ + Log.log('Undefined NAL unit, type: ' + header.type); + return null; + } + if (unit) { + return [unit]; + } + return null; + } + } + + class AACFrame { + + constructor(data, dts, pts) { + this.dts = dts; + this.pts = pts ? pts : this.dts; + + this.data=data;//.subarray(offset); + } + + getData() { + return this.data; + } + + getSize() { + return this.data.byteLength; + } + } + + // import {AACParser} from "../parsers/aac.js"; + // TODO: asm.js + class AACAsm { + constructor() { + this.config = null; + } + + onAACFragment(pkt) { + let rawData = pkt.getPayload(); + if (!pkt.media) { + return null; + } + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + + let sizeLength = Number(pkt.media.fmtp['sizelength'] || 0); + let indexLength = Number(pkt.media.fmtp['indexlength'] || 0); + let indexDeltaLength = Number(pkt.media.fmtp['indexdeltalength'] || 0); + let CTSDeltaLength = Number(pkt.media.fmtp['ctsdeltalength'] || 0); + let DTSDeltaLength = Number(pkt.media.fmtp['dtsdeltalength'] || 0); + let RandomAccessIndication = Number(pkt.media.fmtp['randomaccessindication'] || 0); + let StreamStateIndication = Number(pkt.media.fmtp['streamstateindication'] || 0); + let AuxiliaryDataSizeLength = Number(pkt.media.fmtp['auxiliarydatasizelength'] || 0); + + let configHeaderLength = + sizeLength + Math.max(indexLength, indexDeltaLength) + CTSDeltaLength + DTSDeltaLength + + RandomAccessIndication + StreamStateIndication + AuxiliaryDataSizeLength; + + + let auHeadersLengthPadded = 0; + let offset = 0; + let ts = (Math.round(pkt.getTimestampMS()/1024) << 10) * 90000 / this.config.samplerate; + if (0 !== configHeaderLength) { + /* The AU header section is not empty, read it from payload */ + let auHeadersLengthInBits = data.getUint16(0); // Always 2 octets, without padding + auHeadersLengthPadded = 2 + (auHeadersLengthInBits>>>3) + ((auHeadersLengthInBits & 0x7)?1:0); // Add padding + + //this.config = AACParser.parseAudioSpecificConfig(new Uint8Array(rawData, 0 , auHeadersLengthPadded)); + // TODO: parse config + let frames = []; + let frameOffset=0; + let bits = new BitArray(rawData.subarray(2 + offset)); + let cts = 0; + let dts = 0; + for (let offset=0; offset{ + if (this.connected) { + while (this.transport.dataQueue.length) { + this.onData(this.transport.dataQueue.pop()); + } + } + }; + this._onConnect = this.onConnected.bind(this); + this._onDisconnect = this.onDisconnected.bind(this); + } + + static streamType() { + return null; + } + + destroy() { + this.detachTransport(); + } + + attachTransport(transport) { + if (this.transport) { + this.detachTransport(); + } + this.transport = transport; + this.transport.eventSource.addEventListener('data', this._onData); + this.transport.eventSource.addEventListener('connected', this._onConnect); + this.transport.eventSource.addEventListener('disconnected', this._onDisconnect); + } + + detachTransport() { + if (this.transport) { + this.transport.eventSource.removeEventListener('data', this._onData); + this.transport.eventSource.removeEventListener('connected', this._onConnect); + this.transport.eventSource.removeEventListener('disconnected', this._onDisconnect); + this.transport = null; + } + } + reset() { + + } + + start() { + Log.log('Client started'); + this.paused = false; + // this.startStreamFlush(); + } + + stop() { + Log.log('Client paused'); + this.paused = true; + // this.stopStreamFlush(); + } + + seek(timeOffset) { + + } + + setSource(source) { + this.stop(); + this.endpoint = source; + this.sourceUrl = source.urlpath; + } + + startStreamFlush() { + this.flushInterval = setInterval(()=>{ + if (!this.paused) { + this.eventSource.dispatchEvent('flush'); + } + }, this.options.flush); + } + + stopStreamFlush() { + clearInterval(this.flushInterval); + } + + onData(data) { + + } + + onConnected() { + if (!this.seekable) { + this.transport.dataQueue = []; + this.eventSource.dispatchEvent('clear'); + } + this.connected = true; + } + + onDisconnected() { + this.connected = false; + } + + queryCredentials() { + return Promise.resolve(); + } + + setCredentials(user, password) { + this.endpoint.user = user; + this.endpoint.pass = password; + this.endpoint.auth = `${user}:${password}`; + } + } + + class AACParser { + static get SampleRates() {return [ + 96000, 88200, + 64000, 48000, + 44100, 32000, + 24000, 22050, + 16000, 12000, + 11025, 8000, + 7350];} + + // static Profile = [ + // 0: Null + // 1: AAC Main + // 2: AAC LC (Low Complexity) + // 3: AAC SSR (Scalable Sample Rate) + // 4: AAC LTP (Long Term Prediction) + // 5: SBR (Spectral Band Replication) + // 6: AAC Scalable + // ] + + static parseAudioSpecificConfig(bytesOrBits) { + let config; + if (bytesOrBits.byteLength) { // is byteArray + config = new BitArray(bytesOrBits); + } else { + config = bytesOrBits; + } + + let bitpos = config.bitpos+(config.src.byteOffset+config.bytepos)*8; + let prof = config.readBits(5); + this.codec = `mp4a.40.${prof}`; + let sfi = config.readBits(4); + if (sfi == 0xf) config.skipBits(24); + let channels = config.readBits(4); + + return { + config: bitSlice(new Uint8Array(config.src.buffer), bitpos, bitpos+16), + codec: `mp4a.40.${prof}`, + samplerate: AACParser.SampleRates[sfi], + channels: channels + } + } + + static parseStreamMuxConfig(bytes) { + // ISO_IEC_14496-3 Part 3 Audio. StreamMuxConfig + let config = new BitArray(bytes); + + if (!config.readBits(1)) { + config.skipBits(14); + return AACParser.parseAudioSpecificConfig(config); + } + } + } + + const LOG_TAG$4 = "rtsp:session"; + const Log$9 = getTagged(LOG_TAG$4); + + class RTSPSession { + + constructor(client, sessionId) { + this.state = null; + this.client = client; + this.sessionId = sessionId; + this.url = this.getControlURL(); + } + + reset() { + this.client = null; + } + + start() { + return this.sendPlay(); + } + + stop() { + return this.sendTeardown(); + } + + getControlURL() { + let ctrl = this.client.sdp.getSessionBlock().control; + if (Url.isAbsolute(ctrl)) { + return ctrl; + } else if (!ctrl || '*' === ctrl) { + return this.client.contentBase; + } else { + return `${this.client.contentBase}${ctrl}`; + } + } + + sendRequest(_cmd, _params = {}) { + let params = {}; + if (this.sessionId) { + params['Session'] = this.sessionId; + } + Object.assign(params, _params); + return this.client.sendRequest(_cmd, this.getControlURL(), params); + } + + async sendPlay(pos = 0) { + this.state = RTSPClientSM.STATE_PLAY; + let params = {}; + let range = this.client.sdp.sessionBlock.range; + if (range) { + // TODO: seekable + if (range[0] == -1) { + range[0] = 0;// Do not handle now at the moment + } + // params['Range'] = `${range[2]}=${range[0]}-`; + } + let data = await this.sendRequest('PLAY', params); + this.state = RTSPClientSM.STATE_PLAYING; + return {data: data}; + } + + async sendPause() { + if (!this.client.supports("PAUSE")) { + return; + } + this.state = RTSPClientSM.STATE_PAUSE; + await this.sendRequest("PAUSE"); + this.state = RTSPClientSM.STATE_PAUSED; + } + + async sendTeardown() { + if (this.state != RTSPClientSM.STATE_TEARDOWN) { + this.state = RTSPClientSM.STATE_TEARDOWN; + await this.sendRequest("TEARDOWN"); + Log$9.log('RTSPClient: STATE_TEARDOWN'); + ///this.client.connection.disconnect(); + // TODO: Notify client + } + } + } + + const LOG_TAG$5 = "client:rtsp"; + const Log$a = getTagged(LOG_TAG$5); + + class RTSPClient extends BaseClient { + constructor(options={flush: 200}) { + super(options); + this.clientSM = new RTSPClientSM(this); + this.clientSM.ontracks = (tracks) => { + this.eventSource.dispatchEvent('tracks', tracks); + this.startStreamFlush(); + }; + this.sampleQueues={}; + } + + static streamType() { + return 'rtsp'; + } + + setSource(url) { + super.setSource(url); + this.clientSM.setSource(url); + } + attachTransport(transport) { + super.attachTransport(transport); + this.clientSM.transport = transport; + } + + detachTransport() { + super.detachTransport(); + this.clientSM.transport = null; + } + + reset() { + super.reset(); + this.sampleQueues={}; + } + + destroy() { + this.clientSM.destroy(); + return super.destroy(); + } + + start() { + super.start(); + if (this.transport) { + return this.transport.ready.then(() => { + return this.clientSM.start(); + }); + } else { + return Promise.reject("no transport attached"); + } + } + + stop() { + super.stop(); + return this.clientSM.stop(); + } + + onData(data) { + this.clientSM.onData(data); + } + + onConnected() { + this.clientSM.onConnected(); + super.onConnected(); + } + + onDisconnected() { + super.onDisconnected(); + this.clientSM.onDisconnected(); + } + } + + class AuthError extends Error { + constructor(msg) { + super(msg); + } + } + + class RTSPError extends Error { + constructor(data) { + super(data.msg); + this.data = data; + } + } + + class RTSPClientSM extends StateMachine { + static get USER_AGENT() {return 'SFRtsp 0.3';} + static get STATE_INITIAL() {return 1 << 0;} + static get STATE_OPTIONS() {return 1 << 1;} + static get STATE_DESCRIBE () {return 1 << 2;} + static get STATE_SETUP() {return 1 << 3;} + static get STATE_STREAMS() {return 1 << 4;} + static get STATE_TEARDOWN() {return 1 << 5;} + static get STATE_PLAY() {return 1 << 6;} + static get STATE_PLAYING() {return 1 << 7;} + static get STATE_PAUSE() {return 1 << 8;} + static get STATE_PAUSED() {return 1 << 9;} + // static STATE_PAUSED = 1 << 6; + + constructor(parent) { + super(); + + this.parent = parent; + this.transport = null; + this.payParser = new RTPPayloadParser(); + this.rtp_channels = new Set(); + this.sessions = {}; + this.ontracks = null; + + this.addState(RTSPClientSM.STATE_INITIAL,{ + }).addState(RTSPClientSM.STATE_OPTIONS, { + activate: this.sendOptions, + finishTransition: this.onOptions + }).addState(RTSPClientSM.STATE_DESCRIBE, { + activate: this.sendDescribe, + finishTransition: this.onDescribe + }).addState(RTSPClientSM.STATE_SETUP, { + activate: this.sendSetup, + finishTransition: this.onSetup + }).addState(RTSPClientSM.STATE_STREAMS, { + + }).addState(RTSPClientSM.STATE_TEARDOWN, { + activate: ()=>{ + this.started = false; + }, + finishTransition: ()=>{ + return this.transitionTo(RTSPClientSM.STATE_INITIAL) + } + }).addTransition(RTSPClientSM.STATE_INITIAL, RTSPClientSM.STATE_OPTIONS) + .addTransition(RTSPClientSM.STATE_INITIAL, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_OPTIONS, RTSPClientSM.STATE_DESCRIBE) + .addTransition(RTSPClientSM.STATE_DESCRIBE, RTSPClientSM.STATE_SETUP) + .addTransition(RTSPClientSM.STATE_SETUP, RTSPClientSM.STATE_STREAMS) + .addTransition(RTSPClientSM.STATE_TEARDOWN, RTSPClientSM.STATE_INITIAL) + // .addTransition(RTSPClientSM.STATE_STREAMS, RTSPClientSM.STATE_PAUSED) + // .addTransition(RTSPClientSM.STATE_PAUSED, RTSPClientSM.STATE_STREAMS) + .addTransition(RTSPClientSM.STATE_STREAMS, RTSPClientSM.STATE_TEARDOWN) + // .addTransition(RTSPClientSM.STATE_PAUSED, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_SETUP, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_DESCRIBE, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_OPTIONS, RTSPClientSM.STATE_TEARDOWN); + + this.reset(); + + this.shouldReconnect = false; + + // TODO: remove listeners + // this.connection.eventSource.addEventListener('connected', ()=>{ + // if (this.shouldReconnect) { + // this.reconnect(); + // } + // }); + // this.connection.eventSource.addEventListener('disconnected', ()=>{ + // if (this.started) { + // this.shouldReconnect = true; + // } + // }); + // this.connection.eventSource.addEventListener('data', (data)=>{ + // let channel = new DataView(data).getUint8(1); + // if (this.rtp_channels.has(channel)) { + // this.onRTP({packet: new Uint8Array(data, 4), type: channel}); + // } + // + // }); + } + + destroy() { + this.parent = null; + } + + setSource(url) { + this.reset(); + this.endpoint = url; + this.url = `${url.protocol}://${url.location}${url.urlpath}`; + } + + onConnected() { + if (this.rtpFactory) { + this.rtpFactory = null; + } + if (this.shouldReconnect) { + this.start(); + } + } + + async onDisconnected() { + this.reset(); + this.shouldReconnect = true; + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + + start() { + if (this.currentState.name !== RTSPClientSM.STATE_STREAMS) { + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } else { + // TODO: seekable + let promises = []; + for (let session in this.sessions) { + promises.push(this.sessions[session].sendPlay()); + } + return Promise.all(promises); + } + } + + onData(data) { + let channel = data[1]; + if (this.rtp_channels.has(channel)) { + this.onRTP({packet: data.subarray(4), type: channel}); + } + } + + useRTPChannel(channel) { + this.rtp_channels.add(channel); + } + + forgetRTPChannel(channel) { + this.rtp_channels.delete(channel); + } + + stop() { + this.shouldReconnect = false; + let promises = []; + for (let session in this.sessions) { + promises.push(this.sessions[session].sendPause()); + } + return Promise.all(promises); + // this.mse = null; + } + + async reset() { + this.authenticator = ''; + this.methods = []; + this.tracks = []; + this.rtpBuffer={}; + for (let stream in this.streams) { + this.streams[stream].reset(); + } + for (let session in this.sessions) { + this.sessions[session].reset(); + } + this.streams={}; + this.sessions={}; + this.contentBase = ""; + if (this.currentState) { + if (this.currentState.name != RTSPClientSM.STATE_INITIAL) { + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + } else { + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + this.sdp = null; + this.interleaveChannelIndex = 0; + this.session = null; + this.timeOffset = {}; + this.lastTimestamp = {}; + } + + async reconnect() { + //this.parent.eventSource.dispatchEvent('clear'); + await this.reset(); + if (this.currentState.name != RTSPClientSM.STATE_INITIAL) { + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } else { + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } + } + + supports(method) { + return this.methods.includes(method) + } + + parse(_data) { + Log$a.debug(_data); + let d=_data.split('\r\n\r\n'); + let parsed = MessageBuilder.parse(d[0]); + let len = Number(parsed.headers['content-length']); + if (len) { + parsed.body = d[1]; + } else { + parsed.body=""; + } + return parsed + } + + sendRequest(_cmd, _host, _params={}, _payload=null) { + this.cSeq++; + Object.assign(_params, { + CSeq: this.cSeq, + 'User-Agent': RTSPClientSM.USER_AGENT + }); + if (this.authenticator) { + _params['Authorization'] = this.authenticator(_cmd); + } + return this.send(MessageBuilder.build(_cmd, _host, _params, _payload), _cmd).catch((e)=>{ + if ((e instanceof AuthError) && !_params['Authorization'] ) { + return this.sendRequest(_cmd, _host, _params, _payload); + } else { + throw e; + } + }); + } + + async send(_data, _method) { + if (this.transport) { + try { + await this.transport.ready; + } catch(e) { + this.onDisconnected(); + throw e; + } + Log$a.debug(_data); + let response = await this.transport.send(_data); + let parsed = this.parse(response); + // TODO: parse status codes + if (parsed.code == 401 /*&& !this.authenticator */) { + Log$a.debug(parsed.headers['www-authenticate']); + let auth = parsed.headers['www-authenticate']; + let method = auth.substring(0, auth.indexOf(' ')); + auth = auth.substr(method.length+1); + let chunks = auth.split(','); + + let ep = this.parent.endpoint; + if (!ep.user || !ep.pass) { + try { + await this.parent.queryCredentials.call(this.parent); + } catch (e) { + throw new AuthError(); + } + } + + if (method.toLowerCase() == 'digest') { + let parsedChunks = {}; + for (let chunk of chunks) { + let c = chunk.trim(); + let [k,v] = c.split('='); + parsedChunks[k] = v.substr(1, v.length-2); + } + this.authenticator = (_method)=>{ + let ep = this.parent.endpoint; + let ha1 = md5(`${ep.user}:${parsedChunks.realm}:${ep.pass}`); + let ha2 = md5(`${_method}:${this.url}`); + let response = md5(`${ha1}:${parsedChunks.nonce}:${ha2}`); + let tail=''; // TODO: handle other params + return `Digest username="${ep.user}", realm="${parsedChunks.realm}", nonce="${parsedChunks.nonce}", uri="${this.url}", response="${response}"${tail}`; + }; + } else { + this.authenticator = ()=>{return `Basic ${btoa(this.parent.endpoint.auth)}`;}; + } + + throw new AuthError(parsed); + } + if (parsed.code >= 300) { + Log$a.error(parsed.statusLine); + throw new RTSPError({msg: `RTSP error: ${parsed.code} ${parsed.statusLine}`, parsed: parsed}); + } + return parsed; + } else { + return Promise.reject("No transport attached"); + } + } + + sendOptions() { + this.reset(); + this.started = true; + this.cSeq = 0; + return this.sendRequest('OPTIONS', '*', {}); + } + + onOptions(data) { + this.methods = data.headers['public'].split(',').map((e)=>e.trim()); + this.transitionTo(RTSPClientSM.STATE_DESCRIBE); + } + + sendDescribe() { + return this.sendRequest('DESCRIBE', this.url, { + 'Accept': 'application/sdp' + }).then((data)=>{ + this.sdp = new SDPParser(); + return this.sdp.parse(data.body).catch(()=>{ + throw new Error("Failed to parse SDP"); + }).then(()=>{return data;}); + }); + } + + onDescribe(data) { + this.contentBase = data.headers['content-base'] || this.url;// `${this.endpoint.protocol}://${this.endpoint.location}${this.endpoint.urlpath}/`; + this.tracks = this.sdp.getMediaBlockList(); + this.rtpFactory = new RTPFactory(this.sdp); + + Log$a.log('SDP contained ' + this.tracks.length + ' track(s). Calling SETUP for each.'); + + if (data.headers['session']) { + this.session = data.headers['session']; + } + + if (!this.tracks.length) { + throw new Error("No tracks in SDP"); + } + + this.transitionTo(RTSPClientSM.STATE_SETUP); + } + + sendSetup() { + let streams=[]; + let lastPromise = null; + + // TODO: select first video and first audio tracks + for (let track_type of this.tracks) { + Log$a.log("setup track: "+track_type); + // if (track_type=='audio') continue; + // if (track_type=='video') continue; + let track = this.sdp.getMediaBlock(track_type); + if (!PayloadType.string_map[track.rtpmap[track.fmt[0]].name]) continue; + + this.streams[track_type] = new RTSPStream(this, track); + let setupPromise = this.streams[track_type].start(lastPromise); + lastPromise = setupPromise; + this.parent.sampleQueues[PayloadType.string_map[track.rtpmap[track.fmt[0]].name]]=[]; + this.rtpBuffer[track.fmt[0]]=[]; + streams.push(setupPromise.then(({track, data})=>{ + this.timeOffset[track.fmt[0]] = 0; + try { + let rtp_info = data.headers["rtp-info"].split(';'); + for (let chunk of rtp_info) { + let [key, val] = chunk.split("="); + if (key === "rtptime") { + this.timeOffset[track.fmt[0]] = 0;//Number(val); + } + } + } catch (e) { + // new Date().getTime(); + } + let params = { + timescale: 0, + scaleFactor: 0 + }; + if (track.fmtp['sprop-parameter-sets']) { + let sps_pps = track.fmtp['sprop-parameter-sets'].split(','); + params = { + sps:base64ToArrayBuffer(sps_pps[0]), + pps:base64ToArrayBuffer(sps_pps[1]) + }; + } else if (track.fmtp['config']) { + let config = track.fmtp['config']; + this.has_config = track.fmtp['cpresent']!='0'; + let generic = track.rtpmap[track.fmt[0]].name == 'MPEG4-GENERIC'; + if (generic) { + params={config: + AACParser.parseAudioSpecificConfig(hexToByteArray(config)) + }; + this.payParser.aacparser.setConfig(params.config); + } else if (config) { + // todo: parse audio specific config for mpeg4-generic + params={config: + AACParser.parseStreamMuxConfig(hexToByteArray(config)) + }; + this.payParser.aacparser.setConfig(params.config); + } + } + params.duration = this.sdp.sessionBlock.range?this.sdp.sessionBlock.range[1]-this.sdp.sessionBlock.range[0]:1; + this.parent.seekable = (params.duration > 1); + let res = { + track: track, + offset: this.timeOffset[track.fmt[0]], + type: PayloadType.string_map[track.rtpmap[track.fmt[0]].name], + params: params, + duration: params.duration + }; + console.log(res, this.timeOffset); + let session = data.headers.session.split(';')[0]; + if (!this.sessions[session]) { + this.sessions[session] = new RTSPSession(this, session); + } + return res; + })); + } + return Promise.all(streams).then((tracks)=>{ + let sessionPromises = []; + for (let session in this.sessions) { + sessionPromises.push(this.sessions[session].start()); + } + return Promise.all(sessionPromises).then(()=>{ + if (this.ontracks) { + this.ontracks(tracks); + } + }) + }).catch((e)=>{ + console.error(e); + this.stop(); + this.reset(); + }); + } + + onSetup() { + this.transitionTo(RTSPClientSM.STATE_STREAMS); + } + + onRTP(_data) { + if (!this.rtpFactory) return; + + let rtp = this.rtpFactory.build(_data.packet, this.sdp); + if (!rtp.type) { + return; + } + + if (this.timeOffset[rtp.pt] === undefined) { + //console.log(rtp.pt, this.timeOffset[rtp.pt]); + this.rtpBuffer[rtp.pt].push(rtp); + return; + } + + if (this.lastTimestamp[rtp.pt] === undefined) { + this.lastTimestamp[rtp.pt] = rtp.timestamp-this.timeOffset[rtp.pt]; + } + + let queue = this.rtpBuffer[rtp.pt]; + queue.push(rtp); + + while (queue.length) { + let rtp = queue.shift(); + + rtp.timestamp = rtp.timestamp-this.timeOffset[rtp.pt]-this.lastTimestamp[rtp.pt]; + // TODO: overflow + // if (rtp.timestamp < 0) { + // rtp.timestamp = (rtp.timestamp + Number.MAX_SAFE_INTEGER) % 0x7fffffff; + // } + if (rtp.media) { + let pay = this.payParser.parse(rtp); + if (pay) { + // if (typeof pay == typeof []) { + this.parent.sampleQueues[rtp.type].push(pay); + // } else { + // this.parent.sampleQueues[rtp.type].push([pay]); + // } + } + } + } + // this.remuxer.feedRTP(); + } + } + + const Log$b = getTagged('wsplayer'); + + class WSPlayer { + + constructor(node, opts) { + if (typeof node == typeof '') { + this.videoElement = document.getElementById(node); + } else { + this.videoElement = node; + } + + this.eventSource = new EventEmitter(); + this.type = "rtsp"; + + // TODO: opts 处理 + // this.videoElement.addEventListener('play', ()=>{ + // if (!this.isPlaying()) { + // this.client.start(); + // } + // }, false); + + this.videoElement.addEventListener('abort', () => { + this.unload(); + }, false); + } + + isPlaying() { + return !(this.player.paused || this.client.paused); + } + + // 加载并启动 + async load(wsurl) { + await this.unload(); + + this.url = wsurl; + try { + this.endpoint = Url.parse(wsurl); + this.endpoint.protocol="rtsp"; + } catch (e) { + return; + } + this.transport = new WebsocketTransport(this.endpoint, this.type, {socket:wsurl}); + this.client = new RTSPClient(); + this.remuxer = new Remuxer(this.videoElement); + + this.remuxer.attachClient(this.client); + + this.client.attachTransport(this.transport); + this.client.setSource(this.endpoint); + + // 确保video元素可用 + this.videoElement.src = "rtsp://placehold"; + this.client.start().catch((e)=>{ + this.errorHandler(e); + }); + } + + // 卸载 + async unload() { + if (this.remuxer) { + this.remuxer.destroy(); + this.remuxer = null; + } + if (this.client) { + this.client.stop(); + await this.client.destroy(); + this.client = null; + } + if (this.transport) { + await this.transport.destroy(); + this.transport = null; + } + // 重置video元素 + this.videoElement.src = "rtsp://placehold"; + } + + errorHandler(ev) { + this.eventSource.dispatchEvent('error', ev); + } + } + + setDefaultLogLevel(LogLevel.Debug); + getTagged("transport:ws").setLevel(LogLevel.Debug); + getTagged("client:rtsp").setLevel(LogLevel.Debug); + getTagged("mse").setLevel(LogLevel.Debug); + + // factory method + // node = video element + function createPlayer(node, optionalConfig) { + return new WSPlayer(node, optionalConfig) + } + + // interfaces + let rtspjs = {}; + rtspjs.createPlayer = createPlayer; + rtspjs.getLogger = getTagged; + window.rtspjs = rtspjs; + +}()); diff --git a/demos/rtsp/style.css b/demos/rtsp/style.css new file mode 100755 index 0000000..dac210c --- /dev/null +++ b/demos/rtsp/style.css @@ -0,0 +1,28 @@ +body { + max-width: 720px; + margin: 50px auto; +} + +#test_video { + width: 720px; +} + +.controls { + display: flex; + justify-content: space-around; + align-items: center; +} +input.input, .form-inline .input-group>.form-control { + width: 300px; +} +.logs { + overflow: auto; + width: 720px; + height: 150px; + padding: 5px; + border-top: solid 1px gray; + border-bottom: solid 1px gray; +} +button { + margin: 5px +} \ No newline at end of file diff --git a/demos/wsp/free.player.1.8.js b/demos/wsp/free.player.1.8.js new file mode 100644 index 0000000..bce5dd4 --- /dev/null +++ b/demos/wsp/free.player.1.8.js @@ -0,0 +1,9466 @@ +(function () { +'use strict'; + +// ERROR=0, WARN=1, LOG=2, DEBUG=3 +const LogLevel = { + Error: 0, + Warn: 1, + Log: 2, + Debug: 3 +}; + +let DEFAULT_LOG_LEVEL = LogLevel.Debug; + +function setDefaultLogLevel(level) { + DEFAULT_LOG_LEVEL = level; +} +class Logger { + constructor(level = DEFAULT_LOG_LEVEL, tag) { + this.tag = tag; + this.setLevel(level); + } + + setLevel(level) { + this.level = level; + } + + static get level_map() { return { + [LogLevel.Debug]:'log', + [LogLevel.Log]:'log', + [LogLevel.Warn]:'warn', + [LogLevel.Error]:'error' + }}; + + _log(lvl, args) { + args = Array.prototype.slice.call(args); + if (this.tag) { + args.unshift(`[${this.tag}]`); + } + if (this.level>=lvl) console[Logger.level_map[lvl]].apply(console, args); + } + log(){ + this._log(LogLevel.Log, arguments); + } + debug(){ + this._log(LogLevel.Debug, arguments); + } + error(){ + this._log(LogLevel.Error, arguments); + } + warn(){ + this._log(LogLevel.Warn, arguments); + } +} + +const taggedLoggers = new Map(); +function getTagged(tag) { + if (!taggedLoggers.has(tag)) { + taggedLoggers.set(tag, new Logger(DEFAULT_LOG_LEVEL, tag)); + } + return taggedLoggers.get(tag); +} +const Log = new Logger(); + +// export * from 'bp_logger'; + +class Url { + static parse(url) { + let ret = {}; + + let regex = /^([^:]+):\/\/([^\/]+)(.*)$/; //protocol, login, urlpath + let result = regex.exec(url); + + if (!result) { + throw new Error("bad url"); + } + + ret.full = url; + ret.protocol = result[1]; + ret.urlpath = result[3]; + + let parts = ret.urlpath.split('/'); + ret.basename = parts.pop().split(/\?|#/)[0]; + ret.basepath = parts.join('/'); + + let loginSplit = result[2].split('@'); + let hostport = loginSplit[0].split(':'); + let userpass = [ null, null ]; + if (loginSplit.length === 2) { + userpass = loginSplit[0].split(':'); + hostport = loginSplit[1].split(':'); + } + + ret.user = userpass[0]; + ret.pass = userpass[1]; + ret.host = hostport[0]; + ret.auth = (ret.user && ret.pass) ? `${ret.user}:${ret.pass}` : ''; + + ret.port = (null == hostport[1]) ? Url.protocolDefaultPort(ret.protocol) : hostport[1]; + ret.portDefined = (null != hostport[1]); + ret.location = `${ret.host}:${ret.port}`; + + if (ret.protocol == 'unix') { + ret.socket = ret.port; + ret.port = undefined; + } + + return ret; + } + + static full(parsed) { + return `${parsed.protocol}://${parsed.location}/${parsed.urlpath}`; + } + + static isAbsolute(url) { + return /^[^:]+:\/\//.test(url); + } + + static protocolDefaultPort(protocol) { + switch (protocol) { + case 'rtsp': return 554; + case 'http': return 80; + case 'https': return 443; + } + + return 0; + } +} + +const listener = Symbol("event_listener"); +const listeners = Symbol("event_listeners"); + +class DestructibleEventListener { + constructor(eventListener) { + this[listener] = eventListener; + this[listeners] = new Map(); + } + + clear() { + if (this[listeners]) { + for (let entry of this[listeners]) { + for (let fn of entry[1]) { + this[listener].removeEventListener(entry[0], fn); + } + } + } + this[listeners].clear(); + } + + destroy() { + this.clear(); + this[listeners] = null; + } + + on(event, selector, fn) { + if (fn == undefined) { + fn = selector; + selector = null; + } + if (selector) { + return this.addEventListener(event, (e) => { + if (e.target.matches(selector)) { + fn(e); + } + }); + } else { + return this.addEventListener(event, fn); + } + } + + addEventListener(event, fn) { + if (!this[listeners].has(event)) { + this[listeners].set(event, new Set()); + } + this[listeners].get(event).add(fn); + this[listener].addEventListener(event, fn, false); + return fn; + } + + removeEventListener(event, fn) { + this[listener].removeEventListener(event, fn, false); + if (this[listeners].has(event)) { + //this[listeners].set(event, new Set()); + let ev = this[listeners].get(event); + ev.delete(fn); + if (!ev.size) { + this[listeners].delete(event); + } + } + } + + dispatchEvent(event) { + if (this[listener]) { + this[listener].dispatchEvent(event); + } + } +} + +class EventEmitter { + constructor(element=null) { + this[listener] = new DestructibleEventListener(element || document.createElement('div')); + } + + clear() { + if (this[listener]) { + this[listener].clear(); + } + } + + destroy() { + if (this[listener]) { + this[listener].destroy(); + this[listener] = null; + } + } + + on(event, selector, fn) { + if (this[listener]) { + return this[listener].on(event, selector, fn); + } + return null; + } + + addEventListener(event, fn) { + if (this[listener]) { + return this[listener].addEventListener(event, fn, false); + } + return null; + } + + removeEventListener(event, fn) { + if (this[listener]) { + this[listener].removeEventListener(event, fn, false); + } + } + + dispatchEvent(event, data) { + if (this[listener]) { + this[listener].dispatchEvent(new CustomEvent(event, {detail: data})); + } + } +} + +class EventSourceWrapper { + constructor(eventSource) { + this.eventSource = eventSource; + this[listeners] = new Map(); + } + + on(event, selector, fn) { + if (!this[listeners].has(event)) { + this[listeners].set(event, new Set()); + } + let listener = this.eventSource.on(event, selector, fn); + if (listener) { + this[listeners].get(event).add(listener); + } + } + + off(event, fn){ + this.eventSource.removeEventListener(event, fn); + } + + clear() { + this.eventSource.clear(); + this[listeners].clear(); + } + + destroy() { + this.eventSource.clear(); + this[listeners] = null; + this.eventSource = null; + } +} + +// export * from 'bp_event'; + +/** + * Generate MP4 Box + * got from: https://github.com/dailymotion/hls.js + */ + +class MP4 { + static init() { + MP4.types = { + avc1: [], // codingname + avcC: [], + btrt: [], + dinf: [], + dref: [], + esds: [], + ftyp: [], + hdlr: [], + mdat: [], + mdhd: [], + mdia: [], + mfhd: [], + minf: [], + moof: [], + moov: [], + mp4a: [], + mvex: [], + mvhd: [], + sdtp: [], + stbl: [], + stco: [], + stsc: [], + stsd: [], + stsz: [], + stts: [], + tfdt: [], + tfhd: [], + traf: [], + trak: [], + trun: [], + trex: [], + tkhd: [], + vmhd: [], + smhd: [] + }; + + var i; + for (i in MP4.types) { + if (MP4.types.hasOwnProperty(i)) { + MP4.types[i] = [ + i.charCodeAt(0), + i.charCodeAt(1), + i.charCodeAt(2), + i.charCodeAt(3) + ]; + } + } + + var videoHdlr = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x56, 0x69, 0x64, 0x65, + 0x6f, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' + ]); + + var audioHdlr = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x53, 0x6f, 0x75, 0x6e, + 0x64, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' + ]); + + MP4.HDLR_TYPES = { + 'video': videoHdlr, + 'audio': audioHdlr + }; + + var dref = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01, // entry_count + 0x00, 0x00, 0x00, 0x0c, // entry_size + 0x75, 0x72, 0x6c, 0x20, // 'url' type + 0x00, // version 0 + 0x00, 0x00, 0x01 // entry_flags + ]); + + var stco = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00 // entry_count + ]); + + MP4.STTS = MP4.STSC = MP4.STCO = stco; + + MP4.STSZ = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // sample_size + 0x00, 0x00, 0x00, 0x00, // sample_count + ]); + MP4.VMHD = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x01, // flags + 0x00, 0x00, // graphicsmode + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00 // opcolor + ]); + MP4.SMHD = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, // balance + 0x00, 0x00 // reserved + ]); + + MP4.STSD = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01]);// entry_count + + var majorBrand = new Uint8Array([105,115,111,109]); // isom + var avc1Brand = new Uint8Array([97,118,99,49]); // avc1 + var minorVersion = new Uint8Array([0, 0, 0, 1]); + + MP4.FTYP = MP4.box(MP4.types.ftyp, majorBrand, minorVersion, majorBrand, avc1Brand); + MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, dref)); + } + + static box(type, ...payload) { + var size = 8, + i = payload.length, + len = i, + result; + // calculate the total size we need to allocate + while (i--) { + size += payload[i].byteLength; + } + result = new Uint8Array(size); + result[0] = (size >> 24) & 0xff; + result[1] = (size >> 16) & 0xff; + result[2] = (size >> 8) & 0xff; + result[3] = size & 0xff; + result.set(type, 4); + // copy the payload into the result + for (i = 0, size = 8; i < len; ++i) { + // copy payload[i] array @ offset size + result.set(payload[i], size); + size += payload[i].byteLength; + } + return result; + } + + static hdlr(type) { + return MP4.box(MP4.types.hdlr, MP4.HDLR_TYPES[type]); + } + + static mdat(data) { + return MP4.box(MP4.types.mdat, data); + } + + static mdhd(timescale, duration) { + return MP4.box(MP4.types.mdhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x02, // creation_time + 0x00, 0x00, 0x00, 0x03, // modification_time + (timescale >> 24) & 0xFF, + (timescale >> 16) & 0xFF, + (timescale >> 8) & 0xFF, + timescale & 0xFF, // timescale + (duration >> 24), + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x55, 0xc4, // 'und' language (undetermined) + 0x00, 0x00 + ])); + } + + static mdia(track) { + return MP4.box(MP4.types.mdia, MP4.mdhd(track.timescale, track.duration), MP4.hdlr(track.type), MP4.minf(track)); + } + + static mfhd(sequenceNumber) { + return MP4.box(MP4.types.mfhd, new Uint8Array([ + 0x00, + 0x00, 0x00, 0x00, // flags + (sequenceNumber >> 24), + (sequenceNumber >> 16) & 0xFF, + (sequenceNumber >> 8) & 0xFF, + sequenceNumber & 0xFF, // sequence_number + ])); + } + + static minf(track) { + if (track.type === 'audio') { + return MP4.box(MP4.types.minf, MP4.box(MP4.types.smhd, MP4.SMHD), MP4.DINF, MP4.stbl(track)); + } else { + return MP4.box(MP4.types.minf, MP4.box(MP4.types.vmhd, MP4.VMHD), MP4.DINF, MP4.stbl(track)); + } + } + + static moof(sn, baseMediaDecodeTime, track) { + return MP4.box(MP4.types.moof, MP4.mfhd(sn), MP4.traf(track,baseMediaDecodeTime)); + } + /** + * @param tracks... (optional) {array} the tracks associated with this movie + */ + static moov(tracks, duration, timescale) { + var + i = tracks.length, + boxes = []; + + while (i--) { + boxes[i] = MP4.trak(tracks[i]); + } + + return MP4.box.apply(null, [MP4.types.moov, MP4.mvhd(timescale, duration)].concat(boxes).concat(MP4.mvex(tracks))); + } + + static mvex(tracks) { + var + i = tracks.length, + boxes = []; + + while (i--) { + boxes[i] = MP4.trex(tracks[i]); + } + return MP4.box.apply(null, [MP4.types.mvex].concat(boxes)); + } + + static mvhd(timescale,duration) { + var + bytes = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01, // creation_time + 0x00, 0x00, 0x00, 0x02, // modification_time + (timescale >> 24) & 0xFF, + (timescale >> 16) & 0xFF, + (timescale >> 8) & 0xFF, + timescale & 0xFF, // timescale + (duration >> 24) & 0xFF, + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x00, 0x01, 0x00, 0x00, // 1.0 rate + 0x01, 0x00, // 1.0 volume + 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0xff, 0xff, 0xff, 0xff // next_track_ID + ]); + return MP4.box(MP4.types.mvhd, bytes); + } + + static sdtp(track) { + var + samples = track.samples || [], + bytes = new Uint8Array(4 + samples.length), + flags, + i; + // leave the full box header (4 bytes) all zero + // write the sample table + for (i = 0; i < samples.length; i++) { + flags = samples[i].flags; + bytes[i + 4] = (flags.dependsOn << 4) | + (flags.isDependedOn << 2) | + (flags.hasRedundancy); + } + + return MP4.box(MP4.types.sdtp, bytes); + } + + static stbl(track) { + return MP4.box(MP4.types.stbl, MP4.stsd(track), MP4.box(MP4.types.stts, MP4.STTS), MP4.box(MP4.types.stsc, MP4.STSC), MP4.box(MP4.types.stsz, MP4.STSZ), MP4.box(MP4.types.stco, MP4.STCO)); + } + + static avc1(track) { + var sps = [], pps = [], i, data, len; + // assemble the SPSs + + for (i = 0; i < track.sps.length; i++) { + data = track.sps[i]; + len = data.byteLength; + sps.push((len >>> 8) & 0xFF); + sps.push((len & 0xFF)); + sps = sps.concat(Array.prototype.slice.call(data)); // SPS + } + + // assemble the PPSs + for (i = 0; i < track.pps.length; i++) { + data = track.pps[i]; + len = data.byteLength; + pps.push((len >>> 8) & 0xFF); + pps.push((len & 0xFF)); + pps = pps.concat(Array.prototype.slice.call(data)); + } + + var avcc = MP4.box(MP4.types.avcC, new Uint8Array([ + 0x01, // version + sps[3], // profile + sps[4], // profile compat + sps[5], // level + 0xfc | 3, // lengthSizeMinusOne, hard-coded to 4 bytes + 0xE0 | track.sps.length // 3bit reserved (111) + numOfSequenceParameterSets + ].concat(sps).concat([ + track.pps.length // numOfPictureParameterSets + ]).concat(pps))), // "PPS" + width = track.width, + height = track.height; + //console.log('avcc:' + Hex.hexDump(avcc)); + return MP4.box(MP4.types.avc1, new Uint8Array([ + 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, // pre_defined + 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // pre_defined + (width >> 8) & 0xFF, + width & 0xff, // width + (height >> 8) & 0xFF, + height & 0xff, // height + 0x00, 0x48, 0x00, 0x00, // horizresolution + 0x00, 0x48, 0x00, 0x00, // vertresolution + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // frame_count + 0x12, + 0x62, 0x69, 0x6E, 0x65, //binelpro.ru + 0x6C, 0x70, 0x72, 0x6F, + 0x2E, 0x72, 0x75, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, // compressorname + 0x00, 0x18, // depth = 24 + 0x11, 0x11]), // pre_defined = -1 + avcc, + MP4.box(MP4.types.btrt, new Uint8Array([ + 0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB + 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate + 0x00, 0x2d, 0xc6, 0xc0])) // avgBitrate + ); + } + + static esds(track) { + var configlen = track.config.byteLength; + let data = new Uint8Array(26+configlen+3); + data.set([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + + 0x03, // descriptor_type + 0x17+configlen, // length + 0x00, 0x01, //es_id + 0x00, // stream_priority + + 0x04, // descriptor_type + 0x0f+configlen, // length + 0x40, //codec : mpeg4_audio + 0x15, // stream_type + 0x00, 0x00, 0x00, // buffer_size + 0x00, 0x00, 0x00, 0x00, // maxBitrate + 0x00, 0x00, 0x00, 0x00, // avgBitrate + + 0x05, // descriptor_type + configlen + ]); + data.set(track.config, 26); + data.set([0x06, 0x01, 0x02], 26+configlen); + // return new Uint8Array([ + // 0x00, // version 0 + // 0x00, 0x00, 0x00, // flags + // + // 0x03, // descriptor_type + // 0x17+configlen, // length + // 0x00, 0x01, //es_id + // 0x00, // stream_priority + // + // 0x04, // descriptor_type + // 0x0f+configlen, // length + // 0x40, //codec : mpeg4_audio + // 0x15, // stream_type + // 0x00, 0x00, 0x00, // buffer_size + // 0x00, 0x00, 0x00, 0x00, // maxBitrate + // 0x00, 0x00, 0x00, 0x00, // avgBitrate + // + // 0x05 // descriptor_type + // ].concat([configlen]).concat(track.config).concat([0x06, 0x01, 0x02])); // GASpecificConfig)); // length + audio config descriptor + return data; + } + + static mp4a(track) { + var audiosamplerate = track.audiosamplerate; + return MP4.box(MP4.types.mp4a, new Uint8Array([ + 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, track.channelCount, // channelcount + 0x00, 0x10, // sampleSize:16bits + 0x00, 0x00, // pre_defined + 0x00, 0x00, // reserved2 + (audiosamplerate >> 8) & 0xFF, + audiosamplerate & 0xff, // + 0x00, 0x00]), + MP4.box(MP4.types.esds, MP4.esds(track))); + } + + static stsd(track) { + if (track.type === 'audio') { + return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track)); + } else { + return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track)); + } + } + + static tkhd(track) { + var id = track.id, + duration = track.duration, + width = track.width, + height = track.height, + volume = track.volume; + return MP4.box(MP4.types.tkhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x07, // flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + (id >> 24) & 0xFF, + (id >> 16) & 0xFF, + (id >> 8) & 0xFF, + id & 0xFF, // track_ID + 0x00, 0x00, 0x00, 0x00, // reserved + (duration >> 24), + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, // layer + 0x00, 0x00, // alternate_group + (volume>>0)&0xff, (((volume%1)*10)>>0)&0xff, // track volume // FIXME + 0x00, 0x00, // reserved + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix + (width >> 8) & 0xFF, + width & 0xFF, + 0x00, 0x00, // width + (height >> 8) & 0xFF, + height & 0xFF, + 0x00, 0x00 // height + ])); + } + + static traf(track,baseMediaDecodeTime) { + var sampleDependencyTable = MP4.sdtp(track), + id = track.id; + return MP4.box(MP4.types.traf, + MP4.box(MP4.types.tfhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (id >> 24), + (id >> 16) & 0XFF, + (id >> 8) & 0XFF, + (id & 0xFF) // track_ID + ])), + MP4.box(MP4.types.tfdt, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (baseMediaDecodeTime >>24), + (baseMediaDecodeTime >> 16) & 0XFF, + (baseMediaDecodeTime >> 8) & 0XFF, + (baseMediaDecodeTime & 0xFF) // baseMediaDecodeTime + ])), + MP4.trun(track, + sampleDependencyTable.length + + 16 + // tfhd + 16 + // tfdt + 8 + // traf header + 16 + // mfhd + 8 + // moof header + 8), // mdat header + sampleDependencyTable); + } + + /** + * Generate a track box. + * @param track {object} a track definition + * @return {Uint8Array} the track box + */ + static trak(track) { + track.duration = track.duration || 0xffffffff; + return MP4.box(MP4.types.trak, MP4.tkhd(track), MP4.mdia(track)); + } + + static trex(track) { + var id = track.id; + return MP4.box(MP4.types.trex, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (id >> 24), + (id >> 16) & 0XFF, + (id >> 8) & 0XFF, + (id & 0xFF), // track_ID + 0x00, 0x00, 0x00, 0x01, // default_sample_description_index + 0x00, 0x00, 0x00, 0x00, // default_sample_duration + 0x00, 0x00, 0x00, 0x00, // default_sample_size + 0x00, 0x01, 0x00, 0x01 // default_sample_flags + ])); + } + + static trun(track, offset) { + var samples= track.samples || [], + len = samples.length, + arraylen = 12 + (16 * len), + array = new Uint8Array(arraylen), + i,sample,duration,size,flags,cts; + offset += 8 + arraylen; + array.set([ + 0x00, // version 0 + 0x00, 0x0f, 0x01, // flags + (len >>> 24) & 0xFF, + (len >>> 16) & 0xFF, + (len >>> 8) & 0xFF, + len & 0xFF, // sample_count + (offset >>> 24) & 0xFF, + (offset >>> 16) & 0xFF, + (offset >>> 8) & 0xFF, + offset & 0xFF // data_offset + ],0); + for (i = 0; i < len; i++) { + sample = samples[i]; + duration = sample.duration; + size = sample.size; + flags = sample.flags; + cts = sample.cts; + array.set([ + (duration >>> 24) & 0xFF, + (duration >>> 16) & 0xFF, + (duration >>> 8) & 0xFF, + duration & 0xFF, // sample_duration + (size >>> 24) & 0xFF, + (size >>> 16) & 0xFF, + (size >>> 8) & 0xFF, + size & 0xFF, // sample_size + (flags.isLeading << 2) | flags.dependsOn, + (flags.isDependedOn << 6) | + (flags.hasRedundancy << 4) | + (flags.paddingValue << 1) | + flags.isNonSync, + flags.degradPrio & 0xF0 << 8, + flags.degradPrio & 0x0F, // sample_flags + (cts >>> 24) & 0xFF, + (cts >>> 16) & 0xFF, + (cts >>> 8) & 0xFF, + cts & 0xFF // sample_composition_time_offset + ],12+16*i); + } + return MP4.box(MP4.types.trun, array); + } + + static initSegment(tracks, duration, timescale) { + if (!MP4.types) { + MP4.init(); + } + var movie = MP4.moov(tracks, duration, timescale), result; + result = new Uint8Array(MP4.FTYP.byteLength + movie.byteLength); + result.set(MP4.FTYP); + result.set(movie, MP4.FTYP.byteLength); + return result; + } +} + +//import {MP4Inspect} from '../iso-bmff/mp4-inspector.js'; + +const LOG_TAG = "mse"; +const Log$1 = getTagged(LOG_TAG); + +class MSEBuffer { + constructor(parent, codec) { + this.mediaSource = parent.mediaSource; + this.players = parent.players; + this.cleaning = false; + this.parent = parent; + this.queue = []; + this.cleanResolvers = []; + this.codec = codec; + this.cleanRanges = []; + + Log$1.debug(`Use codec: ${codec}`); + + this.sourceBuffer = this.mediaSource.addSourceBuffer(codec); + this.eventSource = new EventEmitter(this.sourceBuffer); + + this.eventSource.addEventListener('updatestart', (e)=> { + // this.updating = true; + // Log.debug('update start'); + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning start`); + } + }); + + this.eventSource.addEventListener('update', (e)=> { + // this.updating = true; + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning update`); + } + }); + + this.eventSource.addEventListener('updateend', (e)=> { + // Log.debug('update end'); + // this.updating = false; + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning end`); + + try { + if (this.sourceBuffer.buffered.length && this.players[0].currentTime < this.sourceBuffer.buffered.start(0)) { + this.players[0].currentTime = this.sourceBuffer.buffered.start(0); + } + } catch (e) { + // TODO: do something? + } + while (this.cleanResolvers.length) { + let resolver = this.cleanResolvers.shift(); + resolver(); + } + this.cleaning = false; + + if (this.cleanRanges.length) { + this.doCleanup(); + return; + } + } else { + // Log.debug(`buffered: ${this.sourceBuffer.buffered.end(0)}, current ${this.players[0].currentTime}`); + } + this.feedNext(); + }); + + this.eventSource.addEventListener('error', (e)=> { + Log$1.debug(`Source buffer error: ${this.mediaSource.readyState}`); + if (this.mediaSource.sourceBuffers.length) { + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + this.parent.eventSource.dispatchEvent('error'); + }); + + this.eventSource.addEventListener('abort', (e)=> { + Log$1.debug(`Source buffer aborted: ${this.mediaSource.readyState}`); + if (this.mediaSource.sourceBuffers.length) { + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + this.parent.eventSource.dispatchEvent('error'); + }); + + if (!this.sourceBuffer.updating) { + this.feedNext(); + } + // TODO: cleanup every hour for live streams + } + + destroy() { + this.eventSource.destroy(); + this.clear(); + this.queue = []; + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + + clear() { + this.queue = []; + let promises = []; + for (let i=0; i< this.sourceBuffer.buffered.length; ++i) { + // TODO: await remove + this.cleaning = true; + promises.push(new Promise((resolve, reject)=>{ + this.cleanResolvers.push(resolve); + if (!this.sourceBuffer.updating) { + this.sourceBuffer.remove(this.sourceBuffer.buffered.start(i), this.sourceBuffer.buffered.end(i)); + resolve(); + } else { + this.sourceBuffer.onupdateend = () => { + if (this.sourceBuffer) { + this.sourceBuffer.remove(this.sourceBuffer.buffered.start(i), this.sourceBuffer.buffered.end(i)); + } + resolve(); + }; + } + })); + } + return Promise.all(promises); + } + + setLive(is_live) { + this.is_live = is_live; + } + + feedNext() { + // Log.debug("feed next ", this.sourceBuffer.updating); + if (!this.sourceBuffer.updating && !this.cleaning && this.queue.length) { + this.doAppend(this.queue.shift()); + // TODO: if is live and current position > 1hr => clean all and restart + } + } + + doCleanup() { + if (!this.cleanRanges.length) { + this.cleaning = false; + this.feedNext(); + return; + } + let range = this.cleanRanges.shift(); + Log$1.debug(`${this.codec} remove range [${range[0]} - ${range[1]}). + \nUpdating: ${this.sourceBuffer.updating} + `); + this.cleaning = true; + this.sourceBuffer.remove(range[0], range[1]); + } + + initCleanup() { + if (this.sourceBuffer.buffered.length && !this.sourceBuffer.updating && !this.cleaning) { + Log$1.debug(`${this.codec} cleanup`); + let removeBound = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length-1) - 2; + + for (let i=0; i< this.sourceBuffer.buffered.length; ++i) { + let removeStart = this.sourceBuffer.buffered.start(i); + let removeEnd = this.sourceBuffer.buffered.end(i); + if ((this.players[0].currentTime <= removeStart) || (removeBound <= removeStart)) continue; + + if ((removeBound <= removeEnd) && (removeBound >= removeStart)) { + Log$1.debug(`Clear [${removeStart}, ${removeBound}), leave [${removeBound}, ${removeEnd}]`); + removeEnd = removeBound; + if (removeEnd!=removeStart) { + this.cleanRanges.push([removeStart, removeEnd]); + } + continue; // Do not cleanup buffered range after current position + } + this.cleanRanges.push([removeStart, removeEnd]); + } + + this.doCleanup(); + + // let bufferStart = this.sourceBuffer.buffered.start(0); + // let removeEnd = this.sourceBuffer.buffered.start(0) + (this.sourceBuffer.buffered.end(0) - this.sourceBuffer.buffered.start(0))/2; + // if (this.players[0].currentTime < removeEnd) { + // this.players[0].currentTime = removeEnd; + // } + // let removeEnd = Math.max(this.players[0].currentTime - 3, this.sourceBuffer.buffered.end(0) - 3); + // + // if (removeEnd < bufferStart) { + // removeEnd = this.sourceBuffer.buffered.start(0) + (this.sourceBuffer.buffered.end(0) - this.sourceBuffer.buffered.start(0))/2; + // if (this.players[0].currentTime < removeEnd) { + // this.players[0].currentTime = removeEnd; + // } + // } + + // if (removeEnd > bufferStart && (removeEnd - bufferStart > 0.5 )) { + // // try { + // Log.debug(`${this.codec} remove range [${bufferStart} - ${removeEnd}). + // \nBuffered end: ${this.sourceBuffer.buffered.end(0)} + // \nUpdating: ${this.sourceBuffer.updating} + // `); + // this.cleaning = true; + // this.sourceBuffer.remove(bufferStart, removeEnd); + // // } catch (e) { + // // // TODO: implement + // // Log.error(e); + // // } + // } else { + // this.feedNext(); + // } + } else { + this.feedNext(); + } + } + + doAppend(data) { + // console.log(MP4Inspect.mp4toJSON(data)); + let err = this.players[0].error; + if (err) { + Log$1.error(`Error occured: ${MSE.ErrorNotes[err.code]}`); + try { + this.players.forEach((video)=>{video.stop();}); + this.mediaSource.endOfStream(); + } catch (e){ + + } + this.parent.eventSource.dispatchEvent('error'); + } else { + try { + this.sourceBuffer.appendBuffer(data); + } catch (e) { + if (e.name === 'QuotaExceededError') { + Log$1.debug(`${this.codec} quota fail`); + this.queue.unshift(data); + this.initCleanup(); + return; + } + + // reconnect on fail + Log$1.error(`Error occured while appending buffer. ${e.name}: ${e.message}`); + this.parent.eventSource.dispatchEvent('error'); + } + } + + } + + feed(data) { + this.queue = this.queue.concat(data); + // Log.debug(this.sourceBuffer.updating, this.updating, this.queue.length); + if (this.sourceBuffer && !this.sourceBuffer.updating && !this.cleaning) { + // Log.debug('enq feed'); + this.feedNext(); + } + } +} + +class MSE { + // static CODEC_AVC_BASELINE = "avc1.42E01E"; + // static CODEC_AVC_MAIN = "avc1.4D401E"; + // static CODEC_AVC_HIGH = "avc1.64001E"; + // static CODEC_VP8 = "vp8"; + // static CODEC_AAC = "mp4a.40.2"; + // static CODEC_VORBIS = "vorbis"; + // static CODEC_THEORA = "theora"; + + static get ErrorNotes() {return { + [MediaError.MEDIA_ERR_ABORTED]: 'fetching process aborted by user', + [MediaError.MEDIA_ERR_NETWORK]: 'error occurred when downloading', + [MediaError.MEDIA_ERR_DECODE]: 'error occurred when decoding', + [MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED]: 'audio/video not supported' + }}; + + static isSupported(codecs) { + return (window.MediaSource && window.MediaSource.isTypeSupported(`video/mp4; codecs="${codecs.join(',')}"`)); + } + + constructor (players) { + this.players = players; + const playing = this.players.map((video, idx) => { + video.onplaying = function () { + playing[idx] = true; + }; + video.onpause = function () { + playing[idx] = false; + }; + return !video.paused; + }); + this.playing = playing; + this.mediaSource = new MediaSource(); + this.eventSource = new EventEmitter(this.mediaSource); + this.reset(); + } + + destroy() { + this.reset(); + this.eventSource.destroy(); + this.mediaSource = null; + this.eventSource = null; + } + + play() { + this.players.forEach((video, idx)=>{ + if (video.paused && !this.playing[idx]) { + Log$1.debug(`player ${idx}: play`); + video.play(); + } + }); + } + + setLive(is_live) { + for (let idx in this.buffers) { + this.buffers[idx].setLive(is_live); + } + this.is_live = is_live; + } + + resetBuffers() { + this.players.forEach((video, idx)=>{ + if (!video.paused && this.playing[idx]) { + video.pause(); + video.currentTime = 0; + } + }); + + let promises = []; + for (let buffer of this.buffers.values()) { + promises.push(buffer.clear()); + } + return Promise.all(promises).then(()=>{ + this.mediaSource.endOfStream(); + this.mediaSource.duration = 0; + this.mediaSource.clearLiveSeekableRange(); + this.play(); + }); + } + + clear() { + this.reset(); + this.players.forEach((video)=>{video.src = URL.createObjectURL(this.mediaSource);}); + + return this.setupEvents(); + } + + setupEvents() { + this.eventSource.clear(); + this.resolved = false; + this.mediaReady = new Promise((resolve, reject)=> { + this._sourceOpen = ()=> { + Log$1.debug(`Media source opened: ${this.mediaSource.readyState}`); + if (!this.resolved) { + this.resolved = true; + resolve(); + } + }; + this._sourceEnded = ()=>{ + Log$1.debug(`Media source ended: ${this.mediaSource.readyState}`); + }; + this._sourceClose = ()=>{ + Log$1.debug(`Media source closed: ${this.mediaSource.readyState}`); + if (this.resolved) { + this.eventSource.dispatchEvent('sourceclosed'); + } + }; + this.eventSource.addEventListener('sourceopen', this._sourceOpen); + this.eventSource.addEventListener('sourceended', this._sourceEnded); + this.eventSource.addEventListener('sourceclose', this._sourceClose); + }); + return this.mediaReady; + } + + reset() { + this.ready = false; + for (let track in this.buffers) { + this.buffers[track].destroy(); + delete this.buffers[track]; + } + if (this.mediaSource.readyState == 'open') { + this.mediaSource.duration = 0; + this.mediaSource.endOfStream(); + } + this.updating = false; + this.resolved = false; + this.buffers = {}; + // this.players.forEach((video)=>{video.src = URL.createObjectURL(this.mediaSource)}); + // TODO: remove event listeners for existing media source + // this.setupEvents(); + // this.clear(); + } + + setCodec(track, mimeCodec) { + return this.mediaReady.then(()=>{ + this.buffers[track] = new MSEBuffer(this, mimeCodec); + this.buffers[track].setLive(this.is_live); + }); + } + + feed(track, data) { + if (this.buffers[track]) { + this.buffers[track].feed(data); + } + } +} + +const Log$2 = getTagged('remuxer:base'); +let track_id = 1; +class BaseRemuxer { + + static get MP4_TIMESCALE() { return 90000;} + + // TODO: move to ts parser + // static PTSNormalize(value, reference) { + // + // let offset; + // if (reference === undefined) { + // return value; + // } + // if (reference < value) { + // // - 2^33 + // offset = -8589934592; + // } else { + // // + 2^33 + // offset = 8589934592; + // } + // /* PTS is 33bit (from 0 to 2^33 -1) + // if diff between value and reference is bigger than half of the amplitude (2^32) then it means that + // PTS looping occured. fill the gap */ + // while (Math.abs(value - reference) > 4294967296) { + // value += offset; + // } + // return value; + // } + + static getTrackID() { + return track_id++; + } + + constructor(timescale, scaleFactor, params) { + this.timeOffset = 0; + this.timescale = timescale; + this.scaleFactor = scaleFactor; + this.readyToDecode = false; + this.samples = []; + this.seq = 1; + this.tsAlign = 1; + } + + scaled(timestamp) { + return timestamp / this.scaleFactor; + } + + unscaled(timestamp) { + return timestamp * this.scaleFactor; + } + + remux(unit) { + if (unit) { + this.samples.push({ + unit: unit, + pts: unit.pts, + dts: unit.dts + }); + return true; + } + return false; + } + + static toMS(timestamp) { + return timestamp/90; + } + + setConfig(config) { + + } + + insertDscontinuity() { + this.samples.push(null); + } + + init(initPTS, initDTS, shouldInitialize=true) { + this.initPTS = Math.min(initPTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); + this.initDTS = Math.min(initDTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); + Log$2.debug(`Initial pts=${this.initPTS} dts=${this.initDTS} offset=${this.unscaled(this.timeOffset)}`); + this.initialized = shouldInitialize; + } + + flush() { + this.seq++; + this.mp4track.len = 0; + this.mp4track.samples = []; + } + + static dtsSortFunc(a,b) { + return (a.dts-b.dts); + } + + getPayloadBase(sampleFunction, setupSample) { + if (!this.readyToDecode || !this.initialized || !this.samples.length) return null; + this.samples.sort(BaseRemuxer.dtsSortFunc); + return true; + // + // let payload = new Uint8Array(this.mp4track.len); + // let offset = 0; + // let samples=this.mp4track.samples; + // let mp4Sample, lastDTS, pts, dts; + // + // while (this.samples.length) { + // let sample = this.samples.shift(); + // if (sample === null) { + // // discontinuity + // this.nextDts = undefined; + // break; + // } + // + // let unit = sample.unit; + // + // pts = Math.round((sample.pts - this.initDTS)/this.tsAlign)*this.tsAlign; + // dts = Math.round((sample.dts - this.initDTS)/this.tsAlign)*this.tsAlign; + // // ensure DTS is not bigger than PTS + // dts = Math.min(pts, dts); + // + // // sampleFunction(pts, dts); // TODO: + // + // // mp4Sample = setupSample(unit, pts, dts); // TODO: + // + // payload.set(unit.getData(), offset); + // offset += unit.getSize(); + // + // samples.push(mp4Sample); + // lastDTS = dts; + // } + // if (!samples.length) return null; + // + // // samplesPostFunction(samples); // TODO: + // + // return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } +} + +const Log$3 = getTagged("remuxer:aac"); +// TODO: asm.js +class AACRemuxer extends BaseRemuxer { + + constructor(timescale, scaleFactor = 1, params={}) { + super(timescale, scaleFactor); + + this.codecstring=MSE.CODEC_AAC; + this.units = []; + this.initDTS = undefined; + this.nextAacPts = undefined; + this.lastPts = 0; + this.firstDTS = 0; + this.firstPTS = 0; + this.duration = params.duration || 1; + this.initialized = false; + + this.mp4track={ + id:BaseRemuxer.getTrackID(), + type: 'audio', + fragmented:true, + channelCount:0, + audiosamplerate: this.timescale, + duration: 0, + timescale: this.timescale, + volume: 1, + samples: [], + config: '', + len: 0 + }; + if (params.config) { + this.setConfig(params.config); + } + } + + setConfig(config) { + this.mp4track.channelCount = config.channels; + this.mp4track.audiosamplerate = config.samplerate; + if (!this.mp4track.duration) { + this.mp4track.duration = (this.duration?this.duration:1)*config.samplerate; + } + this.mp4track.timescale = config.samplerate; + this.mp4track.config = config.config; + this.mp4track.codec = config.codec; + this.timescale = config.samplerate; + this.scaleFactor = BaseRemuxer.MP4_TIMESCALE / config.samplerate; + this.expectedSampleDuration = 1024 * this.scaleFactor; + this.readyToDecode = true; + } + + remux(aac) { + if (super.remux.call(this, aac)) { + this.mp4track.len += aac.getSize(); + } + } + + getPayload() { + if (!this.readyToDecode || !this.samples.length) return null; + this.samples.sort(function(a, b) { + return (a.dts-b.dts); + }); + + let payload = new Uint8Array(this.mp4track.len); + let offset = 0; + let samples=this.mp4track.samples; + let mp4Sample, lastDTS, pts, dts; + + while (this.samples.length) { + let sample = this.samples.shift(); + if (sample === null) { + // discontinuity + this.nextDts = undefined; + break; + } + let unit = sample.unit; + pts = sample.pts - this.initDTS; + dts = sample.dts - this.initDTS; + + if (lastDTS === undefined) { + if (this.nextDts) { + let delta = Math.round(this.scaled(pts - this.nextAacPts)); + // if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments + if (/*contiguous || */Math.abs(delta) < 600) { + // log delta + if (delta) { + if (delta > 0) { + Log$3.log(`${delta} ms hole between AAC samples detected,filling it`); + // if we have frame overlap, overlapping for more than half a frame duraion + } else if (delta < -12) { + // drop overlapping audio frames... browser will deal with it + Log$3.log(`${(-delta)} ms overlapping between AAC samples detected, drop frame`); + this.mp4track.len -= unit.getSize(); + continue; + } + // set DTS to next DTS + pts = dts = this.nextAacPts; + } + } + } + // remember first PTS of our aacSamples, ensure value is positive + this.firstDTS = Math.max(0, dts); + } + + mp4Sample = { + size: unit.getSize(), + cts: 0, + duration:1024, + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0, + dependsOn: 1 + } + }; + + payload.set(unit.getData(), offset); + offset += unit.getSize(); + samples.push(mp4Sample); + lastDTS = dts; + } + if (!samples.length) return null; + this.nextDts =pts+this.expectedSampleDuration; + return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } +} +//test.bundle.js:42 [remuxer:h264] skip frame from the past at DTS=18397972271140676 with expected DTS=18397998040950484 + +/** + * Parser for exponential Golomb codes, a variable-bitwidth number encoding scheme used by h264. +*/ +// TODO: asm.js +class ExpGolomb { + + constructor(data) { + this.data = data; + // the number of bytes left to examine in this.data + this.bytesAvailable = this.data.byteLength; + // the current word being examined + this.word = 0; // :uint + // the number of bits left to examine in the current word + this.bitsAvailable = 0; // :uint + } + + // ():void + loadWord() { + var + position = this.data.byteLength - this.bytesAvailable, + workingBytes = new Uint8Array(4), + availableBytes = Math.min(4, this.bytesAvailable); + if (availableBytes === 0) { + throw new Error('no bytes available'); + } + workingBytes.set(this.data.subarray(position, position + availableBytes)); + this.word = new DataView(workingBytes.buffer, workingBytes.byteOffset, workingBytes.byteLength).getUint32(0); + // track the amount of this.data that has been processed + this.bitsAvailable = availableBytes * 8; + this.bytesAvailable -= availableBytes; + } + + // (count:int):void + skipBits(count) { + var skipBytes; // :int + if (this.bitsAvailable > count) { + this.word <<= count; + this.bitsAvailable -= count; + } else { + count -= this.bitsAvailable; + skipBytes = count >> 3; + count -= (skipBytes << 3); + this.bytesAvailable -= skipBytes; + this.loadWord(); + this.word <<= count; + this.bitsAvailable -= count; + } + } + + // (size:int):uint + readBits(size) { + var + bits = Math.min(this.bitsAvailable, size), // :uint + valu = this.word >>> (32 - bits); // :uint + if (size > 32) { + Log.error('Cannot read more than 32 bits at a time'); + } + this.bitsAvailable -= bits; + if (this.bitsAvailable > 0) { + this.word <<= bits; + } else if (this.bytesAvailable > 0) { + this.loadWord(); + } + bits = size - bits; + if (bits > 0) { + return valu << bits | this.readBits(bits); + } else { + return valu; + } + } + + // ():uint + skipLZ() { + var leadingZeroCount; // :uint + for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) { + if (0 !== (this.word & (0x80000000 >>> leadingZeroCount))) { + // the first bit of working word is 1 + this.word <<= leadingZeroCount; + this.bitsAvailable -= leadingZeroCount; + return leadingZeroCount; + } + } + // we exhausted word and still have not found a 1 + this.loadWord(); + return leadingZeroCount + this.skipLZ(); + } + + // ():void + skipUEG() { + this.skipBits(1 + this.skipLZ()); + } + + // ():void + skipEG() { + this.skipBits(1 + this.skipLZ()); + } + + // ():uint + readUEG() { + var clz = this.skipLZ(); // :uint + return this.readBits(clz + 1) - 1; + } + + // ():int + readEG() { + var valu = this.readUEG(); // :int + if (0x01 & valu) { + // the number is odd if the low order bit is set + return (1 + valu) >>> 1; // add 1 to make it even, and divide by 2 + } else { + return -1 * (valu >>> 1); // divide by two then make it negative + } + } + + // Some convenience functions + // :Boolean + readBoolean() { + return 1 === this.readBits(1); + } + + // ():int + readUByte() { + return this.readBits(8); + } + + // ():int + readUShort() { + return this.readBits(16); + } + // ():int + readUInt() { + return this.readBits(32); + } +} + +// TODO: asm.js + +function appendByteArray(buffer1, buffer2) { + let tmp = new Uint8Array((buffer1.byteLength|0) + (buffer2.byteLength|0)); + tmp.set(buffer1, 0); + tmp.set(buffer2, buffer1.byteLength|0); + return tmp; +} + + +function base64ToArrayBuffer(base64) { + var binary_string = window.atob(base64); + var len = binary_string.length; + var bytes = new Uint8Array( len ); + for (var i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; +} + +function hexToByteArray(hex) { + let len = hex.length >> 1; + var bufView = new Uint8Array(len); + for (var i = 0; i < len; i++) { + bufView[i] = parseInt(hex.substr(i<<1,2),16); + } + return bufView; +} + + + +function bitSlice(bytearray, start=0, end=bytearray.byteLength*8) { + let byteLen = Math.ceil((end-start)/8); + let res = new Uint8Array(byteLen); + let startByte = start >>> 3; // /8 + let endByte = (end>>>3) - 1; // /8 + let bitOffset = start & 0x7; // %8 + let nBitOffset = 8 - bitOffset; + let endOffset = 8 - end & 0x7; // %8 + for (let i=0; i> nBitOffset; + if (i == endByte-1 && endOffset < 8) { + tail >>= endOffset; + tail <<= endOffset; + } + } + res[i]=(bytearray[startByte+i]< 0; --i) { + + /* Shift result one left to make room for another bit, + then add the next bit on the stream. */ + result = ((result|0) << 1) | (((this.byte|0) >> (8 - (++this.bitpos))) & 0x01); + if ((this.bitpos|0)>=8) { + this.byte = this.src.getUint8(++this.bytepos); + this.bitpos &= 0x7; + } + } + + return result; + } + skipBits(length) { + this.bitpos += (length|0) & 0x7; // %8 + this.bytepos += (length|0) >>> 3; // *8 + if (this.bitpos > 7) { + this.bitpos &= 0x7; + ++this.bytepos; + } + + if (!this.finished()) { + this.byte = this.src.getUint8(this.bytepos); + return 0; + } else { + return this.bytepos-this.src.byteLength-this.src.bitpos; + } + } + + finished() { + return this.bytepos >= this.src.byteLength; + } +} + +class NALU { + + static get NDR() {return 1;} + static get SLICE_PART_A() {return 2;} + static get SLICE_PART_B() {return 3;} + static get SLICE_PART_C() {return 4;} + static get IDR() {return 5;} + static get SEI() {return 6;} + static get SPS() {return 7;} + static get PPS() {return 8;} + static get DELIMITER() {return 9;} + static get EOSEQ() {return 10;} + static get EOSTR() {return 11;} + static get FILTER() {return 12;} + static get STAP_A() {return 24;} + static get STAP_B() {return 25;} + static get FU_A() {return 28;} + static get FU_B() {return 29;} + + static get TYPES() {return { + [NALU.IDR]: 'IDR', + [NALU.SEI]: 'SEI', + [NALU.SPS]: 'SPS', + [NALU.PPS]: 'PPS', + [NALU.NDR]: 'NDR' + }}; + + static type(nalu) { + if (nalu.ntype in NALU.TYPES) { + return NALU.TYPES[nalu.ntype]; + } else { + return 'UNKNOWN'; + } + } + + constructor(ntype, nri, data, dts, pts) { + + this.data = data; + this.ntype = ntype; + this.nri = nri; + this.dts = dts; + this.pts = pts ? pts : this.dts; + this.sliceType = null; + } + + appendData(idata) { + this.data = appendByteArray(this.data, idata); + } + + toString() { + return `${NALU.type(this)}(${this.data.byteLength}): NRI: ${this.getNri()}, PTS: ${this.pts}, DTS: ${this.dts}`; + } + + getNri() { + return this.nri >> 5; + } + + type() { + return this.ntype; + } + + isKeyframe() { + return this.ntype === NALU.IDR || this.sliceType === 7; + } + + getSize() { + return 4 + 1 + this.data.byteLength; + } + + getData() { + let header = new Uint8Array(5 + this.data.byteLength); + let view = new DataView(header.buffer); + view.setUint32(0, this.data.byteLength + 1); + view.setUint8(4, (0x0 & 0x80) | (this.nri & 0x60) | (this.ntype & 0x1F)); + header.set(this.data, 5); + return header; + } +} + +class H264Parser { + + constructor(remuxer) { + this.remuxer = remuxer; + this.track = remuxer.mp4track; + this.firstFound = false; + } + + msToScaled(timestamp) { + return (timestamp - this.remuxer.timeOffset) * this.remuxer.scaleFactor; + } + + parseSPS(sps) { + var config = H264Parser.readSPS(new Uint8Array(sps)); + + this.track.width = config.width; + this.track.height = config.height; + this.track.sps = [new Uint8Array(sps)]; + // this.track.timescale = this.remuxer.timescale; + // this.track.duration = this.remuxer.timescale; // TODO: extract duration for non-live client + this.track.codec = 'avc1.'; + + let codecarray = new DataView(sps.buffer, sps.byteOffset+1, 4); + for (let i = 0; i < 3; ++i) { + var h = codecarray.getUint8(i).toString(16); + if (h.length < 2) { + h = '0' + h; + } + this.track.codec += h; + } + } + + parsePPS(pps) { + this.track.pps = [new Uint8Array(pps)]; + } + + parseNAL(unit) { + if (!unit) return false; + + let push = null; + // console.log(unit.toString()); + switch (unit.type()) { + case NALU.NDR: + case NALU.IDR: + unit.sliceType = H264Parser.parceSliceHeader(unit.data); + if (unit.isKeyframe() && !this.firstFound) { + this.firstFound = true; + } + if (this.firstFound) { + push = true; + } else { + push = false; + } + break; + case NALU.PPS: + push = false; + if (!this.track.pps) { + this.parsePPS(unit.getData().subarray(4)); + if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) { + this.remuxer.readyToDecode = true; + } + } + break; + case NALU.SPS: + push = false; + if(!this.track.sps) { + this.parseSPS(unit.getData().subarray(4)); + if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) { + this.remuxer.readyToDecode = true; + } + } + break; + case NALU.SEI: + push = false; + let data = new DataView(unit.data.buffer, unit.data.byteOffset, unit.data.byteLength); + let byte_idx = 0; + let pay_type = data.getUint8(byte_idx); + ++byte_idx; + let pay_size = 0; + let sz = data.getUint8(byte_idx); + ++byte_idx; + while (sz === 255) { + pay_size+=sz; + sz = data.getUint8(byte_idx); + ++byte_idx; + } + pay_size+=sz; + + let uuid = unit.data.subarray(byte_idx, byte_idx+16); + byte_idx+=16; + console.log(`PT: ${pay_type}, PS: ${pay_size}, UUID: ${Array.from(uuid).map(function(i) { + return ('0' + i.toString(16)).slice(-2); + }).join('')}`); + // debugger; + break; + case NALU.EOSEQ: + case NALU.EOSTR: + push = false; + default: + } + if (push === null && unit.getNri() > 0 ) { + push=true; + } + return push; + } + + static parceSliceHeader(data) { + let decoder = new ExpGolomb(data); + let first_mb = decoder.readUEG(); + let slice_type = decoder.readUEG(); + let ppsid = decoder.readUEG(); + let frame_num = decoder.readUByte(); + // console.log(`first_mb: ${first_mb}, slice_type: ${slice_type}, ppsid: ${ppsid}, frame_num: ${frame_num}`); + return slice_type; + } + + /** + * Advance the ExpGolomb decoder past a scaling list. The scaling + * list is optionally transmitted as part of a sequence parameter + * set and is not relevant to transmuxing. + * @param decoder {ExpGolomb} exp golomb decoder + * @param count {number} the number of entries in this scaling list + * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 + */ + static skipScalingList(decoder, count) { + let lastScale = 8, + nextScale = 8, + deltaScale; + for (let j = 0; j < count; j++) { + if (nextScale !== 0) { + deltaScale = decoder.readEG(); + nextScale = (lastScale + deltaScale + 256) % 256; + } + lastScale = (nextScale === 0) ? lastScale : nextScale; + } + } + + /** + * Read a sequence parameter set and return some interesting video + * properties. A sequence parameter set is the H264 metadata that + * describes the properties of upcoming video frames. + * @param data {Uint8Array} the bytes of a sequence parameter set + * @return {object} an object with configuration parsed from the + * sequence parameter set, including the dimensions of the + * associated video frames. + */ + static readSPS(data) { + let decoder = new ExpGolomb(data); + let frameCropLeftOffset = 0, + frameCropRightOffset = 0, + frameCropTopOffset = 0, + frameCropBottomOffset = 0, + sarScale = 1, + profileIdc,profileCompat,levelIdc, + numRefFramesInPicOrderCntCycle, picWidthInMbsMinus1, + picHeightInMapUnitsMinus1, + frameMbsOnlyFlag, + scalingListCount; + decoder.readUByte(); + profileIdc = decoder.readUByte(); // profile_idc + profileCompat = decoder.readBits(5); // constraint_set[0-4]_flag, u(5) + decoder.skipBits(3); // reserved_zero_3bits u(3), + levelIdc = decoder.readUByte(); //level_idc u(8) + decoder.skipUEG(); // seq_parameter_set_id + // some profiles have more optional data we don't need + if (profileIdc === 100 || + profileIdc === 110 || + profileIdc === 122 || + profileIdc === 244 || + profileIdc === 44 || + profileIdc === 83 || + profileIdc === 86 || + profileIdc === 118 || + profileIdc === 128) { + var chromaFormatIdc = decoder.readUEG(); + if (chromaFormatIdc === 3) { + decoder.skipBits(1); // separate_colour_plane_flag + } + decoder.skipUEG(); // bit_depth_luma_minus8 + decoder.skipUEG(); // bit_depth_chroma_minus8 + decoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag + if (decoder.readBoolean()) { // seq_scaling_matrix_present_flag + scalingListCount = (chromaFormatIdc !== 3) ? 8 : 12; + for (let i = 0; i < scalingListCount; ++i) { + if (decoder.readBoolean()) { // seq_scaling_list_present_flag[ i ] + if (i < 6) { + H264Parser.skipScalingList(decoder, 16); + } else { + H264Parser.skipScalingList(decoder, 64); + } + } + } + } + } + decoder.skipUEG(); // log2_max_frame_num_minus4 + var picOrderCntType = decoder.readUEG(); + if (picOrderCntType === 0) { + decoder.readUEG(); //log2_max_pic_order_cnt_lsb_minus4 + } else if (picOrderCntType === 1) { + decoder.skipBits(1); // delta_pic_order_always_zero_flag + decoder.skipEG(); // offset_for_non_ref_pic + decoder.skipEG(); // offset_for_top_to_bottom_field + numRefFramesInPicOrderCntCycle = decoder.readUEG(); + for(let i = 0; i < numRefFramesInPicOrderCntCycle; ++i) { + decoder.skipEG(); // offset_for_ref_frame[ i ] + } + } + decoder.skipUEG(); // max_num_ref_frames + decoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag + picWidthInMbsMinus1 = decoder.readUEG(); + picHeightInMapUnitsMinus1 = decoder.readUEG(); + frameMbsOnlyFlag = decoder.readBits(1); + if (frameMbsOnlyFlag === 0) { + decoder.skipBits(1); // mb_adaptive_frame_field_flag + } + decoder.skipBits(1); // direct_8x8_inference_flag + if (decoder.readBoolean()) { // frame_cropping_flag + frameCropLeftOffset = decoder.readUEG(); + frameCropRightOffset = decoder.readUEG(); + frameCropTopOffset = decoder.readUEG(); + frameCropBottomOffset = decoder.readUEG(); + } + if (decoder.readBoolean()) { + // vui_parameters_present_flag + if (decoder.readBoolean()) { + // aspect_ratio_info_present_flag + let sarRatio; + const aspectRatioIdc = decoder.readUByte(); + switch (aspectRatioIdc) { + case 1: sarRatio = [1,1]; break; + case 2: sarRatio = [12,11]; break; + case 3: sarRatio = [10,11]; break; + case 4: sarRatio = [16,11]; break; + case 5: sarRatio = [40,33]; break; + case 6: sarRatio = [24,11]; break; + case 7: sarRatio = [20,11]; break; + case 8: sarRatio = [32,11]; break; + case 9: sarRatio = [80,33]; break; + case 10: sarRatio = [18,11]; break; + case 11: sarRatio = [15,11]; break; + case 12: sarRatio = [64,33]; break; + case 13: sarRatio = [160,99]; break; + case 14: sarRatio = [4,3]; break; + case 15: sarRatio = [3,2]; break; + case 16: sarRatio = [2,1]; break; + case 255: { + sarRatio = [decoder.readUByte() << 8 | decoder.readUByte(), decoder.readUByte() << 8 | decoder.readUByte()]; + break; + } + } + if (sarRatio) { + sarScale = sarRatio[0] / sarRatio[1]; + } + } + if (decoder.readBoolean()) {decoder.skipBits(1);} + + if (decoder.readBoolean()) { + decoder.skipBits(4); + if (decoder.readBoolean()) { + decoder.skipBits(24); + } + } + if (decoder.readBoolean()) { + decoder.skipUEG(); + decoder.skipUEG(); + } + if (decoder.readBoolean()) { + let unitsInTick = decoder.readUInt(); + let timeScale = decoder.readUInt(); + let fixedFrameRate = decoder.readBoolean(); + let frameDuration = timeScale/(2*unitsInTick); + console.log(`timescale: ${timeScale}; unitsInTick: ${unitsInTick}; fixedFramerate: ${fixedFrameRate}; avgFrameDuration: ${frameDuration}`); + } + } + return { + width: Math.ceil((((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale), + height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - ((frameMbsOnlyFlag? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset)) + }; + } + + static readSliceType(decoder) { + // skip NALu type + decoder.readUByte(); + // discard first_mb_in_slice + decoder.readUEG(); + // return slice_type + return decoder.readUEG(); + } +} + +const Log$4 = getTagged("remuxer:h264"); +// TODO: asm.js +class H264Remuxer extends BaseRemuxer { + + constructor(timescale, scaleFactor=1, params={}) { + super(timescale, scaleFactor); + + this.nextDts = undefined; + this.readyToDecode = false; + this.initialized = false; + + this.firstDTS=0; + this.firstPTS=0; + this.lastDTS=undefined; + this.lastSampleDuration = 0; + this.lastDurations = []; + // this.timescale = 90000; + this.tsAlign = Math.round(this.timescale/60); + + this.mp4track={ + id:BaseRemuxer.getTrackID(), + type: 'video', + len:0, + fragmented:true, + sps:'', + pps:'', + width:0, + height:0, + timescale: timescale, + duration: timescale, + samples: [] + }; + this.samples = []; + this.lastGopDTS = -99999999999999; + this.gop=[]; + this.firstUnit = true; + + this.h264 = new H264Parser(this); + + if (params.sps) { + let arr = new Uint8Array(params.sps); + if ((arr[0] & 0x1f) === 7) { + this.setSPS(arr); + } else { + Log$4.warn("bad SPS in SDP"); + } + } + if (params.pps) { + let arr = new Uint8Array(params.pps); + if ((arr[0] & 0x1f) === 8) { + this.setPPS(arr); + } else { + Log$4.warn("bad PPS in SDP"); + } + } + + if (this.mp4track.pps && this.mp4track.sps) { + this.readyToDecode = true; + } + } + + _scaled(timestamp) { + return timestamp >>> this.scaleFactor; + } + + _unscaled(timestamp) { + return timestamp << this.scaleFactor; + } + + setSPS(sps) { + this.h264.parseSPS(sps); + } + + setPPS(pps) { + this.h264.parsePPS(pps); + } + + remux(nalu) { + if (this.lastGopDTS < nalu.dts) { + this.gop.sort(BaseRemuxer.dtsSortFunc); + for (let unit of this.gop) { + // if (this.firstUnit) { + // unit.ntype = 5;//NALU.IDR; + // this.firstUnit = false; + // } + if (super.remux.call(this, unit)) { + this.mp4track.len += unit.getSize(); + } + } + this.gop = []; + this.lastGopDTS = nalu.dts; + } + if (this.h264.parseNAL(nalu)) { + this.gop.push(nalu); + } + } + + getPayload() { + if (!this.getPayloadBase()) { + return null; + } + + let payload = new Uint8Array(this.mp4track.len); + let offset = 0; + let samples=this.mp4track.samples; + let mp4Sample, lastDTS, pts, dts; + + + // Log.debug(this.samples.map((e)=>{ + // return Math.round((e.dts - this.initDTS)); + // })); + + // let minDuration = Number.MAX_SAFE_INTEGER; + while (this.samples.length) { + let sample = this.samples.shift(); + if (sample === null) { + // discontinuity + this.nextDts = undefined; + break; + } + + let unit = sample.unit; + + pts = sample.pts- this.initDTS; // /*Math.round(*/(sample.pts - this.initDTS)/*/this.tsAlign)*this.tsAlign*/; + dts = sample.dts - this.initDTS; ///*Math.round(*/(sample.dts - this.initDTS)/*/this.tsAlign)*this.tsAlign*/; + // ensure DTS is not bigger than PTS + dts = Math.min(pts,dts); + // if not first AVC sample of video track, normalize PTS/DTS with previous sample value + // and ensure that sample duration is positive + if (lastDTS !== undefined) { + let sampleDuration = this.scaled(dts - lastDTS); + // Log.debug(`Sample duration: ${sampleDuration}`); + if (sampleDuration < 0) { + Log$4.log(`invalid AVC sample duration at PTS/DTS: ${pts}/${dts}|lastDTS: ${lastDTS}:${sampleDuration}`); + this.mp4track.len -= unit.getSize(); + continue; + } + // minDuration = Math.min(sampleDuration, minDuration); + this.lastDurations.push(sampleDuration); + if (this.lastDurations.length > 100) { + this.lastDurations.shift(); + } + mp4Sample.duration = sampleDuration; + } else { + if (this.nextDts) { + let delta = dts - this.nextDts; + // if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments + if (/*contiguous ||*/ Math.abs(Math.round(BaseRemuxer.toMS(delta))) < 600) { + + if (delta) { + // set DTS to next DTS + // Log.debug(`Video/PTS/DTS adjusted: ${pts}->${Math.max(pts - delta, this.nextDts)}/${dts}->${this.nextDts},delta:${delta}`); + dts = this.nextDts; + // offset PTS as well, ensure that PTS is smaller or equal than new DTS + pts = Math.max(pts - delta, dts); + } + } else { + if (delta < 0) { + Log$4.log(`skip frame from the past at DTS=${dts} with expected DTS=${this.nextDts}`); + this.mp4track.len -= unit.getSize(); + continue; + } + } + } + // remember first DTS of our avcSamples, ensure value is positive + this.firstDTS = Math.max(0, dts); + } + + mp4Sample = { + size: unit.getSize(), + duration: 0, + cts: this.scaled(pts - dts), + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0 + } + }; + let flags = mp4Sample.flags; + if (sample.unit.isKeyframe() === true) { + // the current sample is a key frame + flags.dependsOn = 2; + flags.isNonSync = 0; + } else { + flags.dependsOn = 1; + flags.isNonSync = 1; + } + + payload.set(unit.getData(), offset); + offset += unit.getSize(); + + samples.push(mp4Sample); + lastDTS = dts; + } + + if (!samples.length) return null; + + let avgDuration = this.lastDurations.reduce(function(a, b) { return (a|0) + (b|0); }, 0) / (this.lastDurations.length||1)|0; + if (samples.length >= 2) { + this.lastSampleDuration = avgDuration; + mp4Sample.duration = avgDuration; + } else { + mp4Sample.duration = this.lastSampleDuration; + } + + if(samples.length && (!this.nextDts || navigator.userAgent.toLowerCase().indexOf('chrome') > -1)) { + let flags = samples[0].flags; + // chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue + // https://code.google.com/p/chromium/issues/detail?id=229412 + flags.dependsOn = 2; + flags.isNonSync = 0; + } + + // next AVC sample DTS should be equal to last sample DTS + last sample duration + this.nextDts = dts + this.unscaled(this.lastSampleDuration); + // Log.debug(`next dts: ${this.nextDts}, last duration: ${this.lastSampleDuration}, last dts: ${dts}`); + + return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } +} + +class PayloadType { + static get H264() {return 1;} + static get AAC() {return 2;} + + static get map() {return { + [PayloadType.H264]: 'video', + [PayloadType.AAC]: 'audio' + }}; + + static get string_map() {return { + H264: PayloadType.H264, + AAC: PayloadType.AAC, + 'MP4A-LATM': PayloadType.AAC, + 'MPEG4-GENERIC': PayloadType.AAC + }} +} + +const LOG_TAG$1 = "remuxer"; +const Log$5 = getTagged(LOG_TAG$1); + +class Remuxer { + static get TrackConverters() {return { + [PayloadType.H264]: H264Remuxer, + [PayloadType.AAC]: AACRemuxer + }}; + + static get TrackScaleFactor() {return { + [PayloadType.H264]: 1,//4, + [PayloadType.AAC]: 0 + }}; + + static get TrackTimescale() {return { + [PayloadType.H264]: 90000,//22500, + [PayloadType.AAC]: 0 + }}; + + constructor(mediaElement) { + this.mse = new MSE([mediaElement]); + this.eventSource = new EventEmitter(); + this.mseEventSource = new EventSourceWrapper(this.mse.eventSource); + this.mse_ready = true; + + this.reset(); + + this.errorListener = this.mseClose.bind(this); + this.closeListener = this.mseClose.bind(this); + + this.eventSource.addEventListener('ready', this.init.bind(this)); + } + + initMSEHandlers() { + this.mseEventSource.on('error', this.errorListener); + this.mseEventSource.on('sourceclosed', this.closeListener); + } + + async reset() { + this.tracks = {}; + this.initialized = false; + this.initSegments = {}; + this.codecs = []; + this.streams = {}; + this.enabled = false; + await this.mse.clear(); + this.initMSEHandlers(); + } + + destroy() { + this.mseEventSource.destroy(); + this.mse.destroy(); + this.mse = null; + + this.detachClient(); + + this.eventSource.destroy(); + } + + onTracks(tracks) { + Log$5.debug(`ontracks: `, tracks.detail); + // store available track types + for (let track of tracks.detail) { + this.tracks[track.type] = new Remuxer.TrackConverters[track.type](Remuxer.TrackTimescale[track.type], Remuxer.TrackScaleFactor[track.type], track.params); + if (track.offset) { + this.tracks[track.type].timeOffset = track.offset; + } + if (track.duration) { + this.tracks[track.type].mp4track.duration = track.duration*(this.tracks[track.type].timescale || Remuxer.TrackTimescale[track.type]); + this.tracks[track.type].duration = track.duration; + } else { + this.tracks[track.type].duration = 1; + } + + // this.tracks[track.type].duration + } + this.mse.setLive(!this.client.seekable); + } + + setTimeOffset(timeOffset, track) { + if (this.tracks[track.type]) { + this.tracks[track.type].timeOffset = timeOffset;///this.tracks[track.type].scaleFactor; + } + } + + init() { + let tracks = []; + this.codecs = []; + let initmse = []; + let initPts = Infinity; + let initDts = Infinity; + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + if (!MSE.isSupported([track.mp4track.codec])) { + throw new Error(`${track.mp4track.type} codec ${track.mp4track.codec} is not supported`); + } + tracks.push(track.mp4track); + this.codecs.push(track.mp4track.codec); + track.init(initPts, initDts/*, false*/); + // initPts = Math.min(track.initPTS, initPts); + // initDts = Math.min(track.initDTS, initDts); + } + + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + //track.init(initPts, initDts); + this.initSegments[track_type] = MP4.initSegment([track.mp4track], track.duration*track.timescale, track.timescale); + initmse.push(this.initMSE(track_type, track.mp4track.codec)); + } + this.initialized = true; + return Promise.all(initmse).then(()=>{ + //this.mse.play(); + this.enabled = true; + }); + + } + + initMSE(track_type, codec) { + if (MSE.isSupported(this.codecs)) { + return this.mse.setCodec(track_type, `${PayloadType.map[track_type]}/mp4; codecs="${codec}"`).then(()=>{ + this.mse.feed(track_type, this.initSegments[track_type]); + // this.mse.play(); + // this.enabled = true; + }); + } else { + throw new Error('Codecs are not supported'); + } + } + + mseClose() { + // this.mse.clear(); + this.client.stop(); + this.eventSource.dispatchEvent('stopped'); + } + + flush() { + this.onSamples(); + + if (!this.initialized) { + // Log.debug(`Initialize...`); + if (Object.keys(this.tracks).length) { + for (let track_type in this.tracks) { + if (!this.tracks[track_type].readyToDecode || !this.tracks[track_type].samples.length) return; + Log$5.debug(`Init MSE for track ${this.tracks[track_type].mp4track.type}`); + } + this.eventSource.dispatchEvent('ready'); + } + } else { + + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + let pay = track.getPayload(); + if (pay && pay.byteLength) { + this.mse.feed(track_type, [MP4.moof(track.seq, track.scaled(track.firstDTS), track.mp4track), MP4.mdat(pay)]); + track.flush(); + } + } + } + } + + onSamples(ev) { + // TODO: check format + // let data = ev.detail; + // if (this.tracks[data.pay] && this.client.sampleQueues[data.pay].length) { + // console.log(`video ${data.units[0].dts}`); + for (let qidx in this.client.sampleQueues) { + let queue = this.client.sampleQueues[qidx]; + while (queue.length) { + let units = queue.shift(); + for (let chunk of units) { + this.tracks[qidx].remux(chunk); + } + } + } + // } + } + + onAudioConfig(ev) { + if (this.tracks[ev.detail.pay]) { + this.tracks[ev.detail.pay].setConfig(ev.detail.config); + } + } + + attachClient(client) { + this.detachClient(); + this.client = client; + this.clientEventSource = new EventSourceWrapper(this.client.eventSource); + this.clientEventSource.on('samples', this.samplesListener); + this.clientEventSource.on('audio_config', this.audioConfigListener); + this.clientEventSource.on('tracks', this.onTracks.bind(this)); + this.clientEventSource.on('flush', this.flush.bind(this)); + this.clientEventSource.on('clear', ()=>{ + this.reset(); + this.mse.clear().then(()=>{ + //this.mse.play(); + this.initMSEHandlers(); + }); + }); + } + + detachClient() { + if (this.client) { + this.clientEventSource.destroy(); + // this.client.eventSource.removeEventListener('samples', this.onSamples.bind(this)); + // this.client.eventSource.removeEventListener('audio_config', this.onAudioConfig.bind(this)); + // // TODO: clear other listeners + // this.client.eventSource.removeEventListener('clear', this._clearListener); + // this.client.eventSource.removeEventListener('tracks', this._tracksListener); + // this.client.eventSource.removeEventListener('flush', this._flushListener); + this.client = null; + } + } +} + +class State { + constructor(name, stateMachine) { + this.stateMachine = stateMachine; + this.transitions = new Set(); + this.name = name; + } + + + activate() { + return Promise.resolve(null); + } + + finishTransition() {} + + failHandler() {} + + deactivate() { + return Promise.resolve(null); + } +} + +class StateMachine { + constructor() { + this.storage = {}; + this.currentState = null; + this.states = new Map(); + } + + addState(name, {activate, finishTransition, deactivate}) { + let state = new State(name, this); + if (activate) state.activate = activate; + if (finishTransition) state.finishTransition = finishTransition; + if (deactivate) state.deactivate = deactivate; + this.states.set(name, state); + return this; + } + + addTransition(fromName, toName){ + if (!this.states.has(fromName)) { + throw ReferenceError(`No such state: ${fromName} while connecting to ${toName}`); + } + if (!this.states.has(toName)) { + throw ReferenceError(`No such state: ${toName} while connecting from ${fromName}`); + } + this.states.get(fromName).transitions.add(toName); + return this; + } + + _promisify(res) { + let promise; + try { + promise = res; + if (!promise.then) { + promise = Promise.resolve(promise); + } + } catch (e) { + promise = Promise.reject(e); + } + return promise; + } + + transitionTo(stateName) { + if (this.currentState == null) { + let state = this.states.get(stateName); + return this._promisify(state.activate.call(this)) + .then((data)=> { + this.currentState = state; + return data; + }).then(state.finishTransition.bind(this)).catch((e)=>{ + state.failHandler(); + throw e; + }); + } + if (this.currentState.name == stateName) return Promise.resolve(); + if (this.currentState.transitions.has(stateName)) { + let state = this.states.get(stateName); + return this._promisify(state.deactivate.call(this)) + .then(state.activate.bind(this)).then((data)=> { + this.currentState = state; + return data; + }).then(state.finishTransition.bind(this)).catch((e)=>{ + state.failHandler(); + throw e; + }); + } else { + return Promise.reject(`No such transition: ${this.currentState.name} to ${stateName}`); + } + } + +} + +// export * from 'bp_statemachine'; + +const Log$6 = getTagged("parser:sdp"); + +class SDPParser { + constructor() { + this.version = -1; + this.origin = null; + this.sessionName = null; + this.timing = null; + this.sessionBlock = {}; + this.media = {}; + this.tracks = {}; + this.mediaMap = {}; + } + + parse(content) { + // Log.debug(content); + return new Promise((resolve, reject) => { + var dataString = content; + var success = true; + var currentMediaBlock = this.sessionBlock; + + // TODO: multiple audio/video tracks + + for (let line of dataString.split("\n")) { + line = line.replace(/\r/, ''); + if (0 === line.length) { + /* Empty row (last row perhaps?), skip to next */ + continue; + } + + switch (line.charAt(0)) { + case 'v': + if (-1 !== this.version) { + Log$6.log('Version present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseVersion(line); + break; + + case 'o': + if (null !== this.origin) { + Log$6.log('Origin present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseOrigin(line); + break; + + case 's': + if (null !== this.sessionName) { + Log$6.log('Session Name present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseSessionName(line); + break; + + case 't': + if (null !== this.timing) { + Log$6.log('Timing present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseTiming(line); + break; + + case 'm': + if (null !== currentMediaBlock && this.sessionBlock !== currentMediaBlock) { + /* Complete previous block and store it */ + this.media[currentMediaBlock.type] = currentMediaBlock; + } + + /* A wild media block appears */ + currentMediaBlock = {}; + currentMediaBlock.rtpmap = {}; + this._parseMediaDescription(line, currentMediaBlock); + break; + + case 'a': + SDPParser._parseAttribute(line, currentMediaBlock); + break; + + default: + Log$6.log('Ignored unknown SDP directive: ' + line); + break; + } + + if (!success) { + reject(); + return; + } + } + + this.media[currentMediaBlock.type] = currentMediaBlock; + + success ? resolve() : reject(); + }); + } + + _parseVersion(line) { + let matches = line.match(/^v=([0-9]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'v=\' (Version) formatted incorrectly: ' + line); + return false; + } + + this.version = matches[1]; + if (0 != this.version) { + Log$6.log('Unsupported SDP version:' + this.version); + return false; + } + + return true; + } + + _parseOrigin(line) { + let matches = line.match(/^o=([^ ]+) (-?[0-9]+) (-?[0-9]+) (IN) (IP4|IP6) ([^ ]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'o=\' (Origin) formatted incorrectly: ' + line); + return false; + } + + this.origin = {}; + this.origin.username = matches[1]; + this.origin.sessionid = matches[2]; + this.origin.sessionversion = matches[3]; + this.origin.nettype = matches[4]; + this.origin.addresstype = matches[5]; + this.origin.unicastaddress = matches[6]; + + return true; + } + + _parseSessionName(line) { + let matches = line.match(/^s=([^\r\n]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'s=\' (Session Name) formatted incorrectly: ' + line); + return false; + } + + this.sessionName = matches[1]; + + return true; + } + + _parseTiming(line) { + let matches = line.match(/^t=([0-9]+) ([0-9]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'t=\' (Timing) formatted incorrectly: ' + line); + return false; + } + + this.timing = {}; + this.timing.start = matches[1]; + this.timing.stop = matches[2]; + + return true; + } + + _parseMediaDescription(line, media) { + let matches = line.match(/^m=([^ ]+) ([^ ]+) ([^ ]+)[ ]/); + if (!matches || !matches.length) { + Log$6.log('\'m=\' (Media) formatted incorrectly: ' + line); + return false; + } + + media.type = matches[1]; + media.port = matches[2]; + media.proto = matches[3]; + media.fmt = line.substr(matches[0].length).split(' ').map(function (fmt, index, array) { + return parseInt(fmt); + }); + + for (let fmt of media.fmt) { + this.mediaMap[fmt] = media; + } + + return true; + } + + static _parseAttribute(line, media) { + if (null === media) { + /* Not in a media block, can't be bothered parsing attributes for session */ + return true; + } + + var matches; + /* Used for some cases of below switch-case */ + var separator = line.indexOf(':'); + var attribute = line.substr(0, (-1 === separator) ? 0x7FFFFFFF : separator); + /* 0x7FF.. is default */ + + switch (attribute) { + case 'a=recvonly': + case 'a=sendrecv': + case 'a=sendonly': + case 'a=inactive': + media.mode = line.substr('a='.length); + break; + case 'a=range': + matches = line.match(/^a=range:\s*([a-zA-Z-]+)=([0-9.]+|now)\s*-\s*([0-9.]*)$/); + media.range = [Number(matches[2] == "now" ? -1 : matches[2]), Number(matches[3]), matches[1]]; + break; + case 'a=control': + media.control = line.substr('a=control:'.length); + break; + + case 'a=rtpmap': + matches = line.match(/^a=rtpmap:(\d+) (.*)$/); + if (null === matches) { + Log$6.log('Could not parse \'rtpmap\' of \'a=\''); + return false; + } + + var payload = parseInt(matches[1]); + media.rtpmap[payload] = {}; + + var attrs = matches[2].split('/'); + media.rtpmap[payload].name = attrs[0].toUpperCase(); + media.rtpmap[payload].clock = attrs[1]; + if (undefined !== attrs[2]) { + media.rtpmap[payload].encparams = attrs[2]; + } + media.ptype = PayloadType.string_map[attrs[0].toUpperCase()]; + + break; + + case 'a=fmtp': + matches = line.match(/^a=fmtp:(\d+) (.*)$/); + if (0 === matches.length) { + Log$6.log('Could not parse \'fmtp\' of \'a=\''); + return false; + } + + media.fmtp = {}; + for (var param of matches[2].split(';')) { + var idx = param.indexOf('='); + media.fmtp[param.substr(0, idx).toLowerCase().trim()] = param.substr(idx + 1).trim(); + } + break; + } + + return true; + } + + getSessionBlock() { + return this.sessionBlock; + } + + hasMedia(mediaType) { + return this.media[mediaType] != undefined; + } + + getMediaBlock(mediaType) { + return this.media[mediaType]; + } + + getMediaBlockByPayloadType(pt) { + // for (var m in this.media) { + // if (-1 !== this.media[m].fmt.indexOf(pt)) { + // return this.media[m]; + // } + // } + return this.mediaMap[pt] || null; + + //ErrorManager.dispatchError(826, [pt], true); + // Log.error(`failed to find media with payload type ${pt}`); + // + // return null; + } + + getMediaBlockList() { + var res = []; + for (var m in this.media) { + res.push(m); + } + + return res; + } +} + +const LOG_TAG$2 = "rtsp:stream"; +const Log$7 = getTagged(LOG_TAG$2); + +class RTSPStream { + + constructor(client, track) { + this.state = null; + this.client = client; + this.track = track; + this.rtpChannel = 1; + + this.stopKeepAlive(); + this.keepaliveInterval = null; + this.keepaliveTime = 30000; + } + + reset() { + this.stopKeepAlive(); + this.client.forgetRTPChannel(this.rtpChannel); + this.client = null; + this.track = null; + } + + start() { + return this.sendSetup();//.then(this.sendPlay.bind(this)); + } + + stop() { + return this.sendTeardown(); + } + + getSetupURL(track) { + let sessionBlock = this.client.sdp.getSessionBlock(); + if (Url.isAbsolute(track.control)) { + return track.control; + } else if (Url.isAbsolute(`${sessionBlock.control}${track.control}`)) { + return `${sessionBlock.control}${track.control}`; + } else if (Url.isAbsolute(`${this.client.contentBase}${track.control}`)) { + /* Should probably check session level control before this */ + return `${this.client.contentBase}${track.control}`; + } + else {//need return default + return track.control; + } + Log$7.error('Can\'t determine track URL from ' + + 'block.control:' + track.control + ', ' + + 'session.control:' + sessionBlock.control + ', and ' + + 'content-base:' + this.client.contentBase); + } + + getControlURL() { + let ctrl = this.client.sdp.getSessionBlock().control; + if (Url.isAbsolute(ctrl)) { + return ctrl; + } else if (!ctrl || '*' === ctrl) { + return this.client.contentBase; + } else { + return `${this.client.contentBase}${ctrl}`; + } + } + + sendKeepalive() { + if (this.client.methods.includes('GET_PARAMETER')) { + return this.client.sendRequest('GET_PARAMETER', this.getSetupURL(this.track), { + 'Session': this.session + }); + } else { + return this.client.sendRequest('OPTIONS', '*'); + } + } + + stopKeepAlive() { + clearInterval(this.keepaliveInterval); + } + + startKeepAlive() { + this.keepaliveInterval = setInterval(() => { + this.sendKeepalive().catch((e) => { + Log$7.error(e); + if (e instanceof RTSPError) { + if (Number(e.data.parsed.code) == 501) { + return; + } + } + this.client.reconnect(); + }); + }, this.keepaliveTime); + } + + sendRequest(_cmd, _params = {}) { + let params = {}; + if (this.session) { + params['Session'] = this.session; + } + Object.assign(params, _params); + return this.client.sendRequest(_cmd, this.getControlURL(), params); + } + + sendSetup() { + this.state = RTSPClientSM.STATE_SETUP; + this.rtpChannel = this.client.interleaveChannelIndex; + let interleavedChannels = this.client.interleaveChannelIndex++ + "-" + this.client.interleaveChannelIndex++; + return this.client.sendRequest('SETUP', this.getSetupURL(this.track), { + 'Transport': `RTP/AVP/TCP;unicast;interleaved=${interleavedChannels}`, + 'Date': new Date().toUTCString() + }).then((_data) => { + this.session = _data.headers['session']; + let transport = _data.headers['transport']; + if (transport) { + let interleaved = transport.match(/interleaved=([0-9]+)-([0-9]+)/)[1]; + if (interleaved) { + this.rtpChannel = Number(interleaved); + } + } + let sessionParamsChunks = this.session.split(';').slice(1); + let sessionParams = {}; + for (let chunk of sessionParamsChunks) { + let kv = chunk.split('='); + sessionParams[kv[0]]=kv[1]; + } + if (sessionParams['timeout']) { + this.keepaliveInterval = Number(sessionParams['timeout']) * 500; // * 1000 / 2 + } + /*if (!/RTP\/AVP\/TCP;unicast;interleaved=/.test(_data.headers["transport"])) { + // TODO: disconnect stream and notify client + throw new Error("Connection broken"); + }*/ + this.client.useRTPChannel(this.rtpChannel); + this.startKeepAlive(); + return {track: this.track, data: _data}; + }); + } +} + +/* + * JavaScript MD5 + * https://github.com/blueimp/JavaScript-MD5 + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + * + * Based on + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + + +/* +* Add integers, wrapping at 2^32. This uses 16-bit operations internally +* to work around bugs in some JS interpreters. +*/ +function safeAdd(x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF) +} + +/* +* Bitwise rotate a 32-bit number to the left. +*/ +function bitRotateLeft(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)) +} + +/* +* These functions implement the four basic operations the algorithm uses. +*/ +function md5cmn(q, a, b, x, s, t) { + return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b) +} +function md5ff(a, b, c, d, x, s, t) { + return md5cmn((b & c) | ((~b) & d), a, b, x, s, t) +} +function md5gg(a, b, c, d, x, s, t) { + return md5cmn((b & d) | (c & (~d)), a, b, x, s, t) +} +function md5hh(a, b, c, d, x, s, t) { + return md5cmn(b ^ c ^ d, a, b, x, s, t) +} +function md5ii(a, b, c, d, x, s, t) { + return md5cmn(c ^ (b | (~d)), a, b, x, s, t) +} + +/* +* Calculate the MD5 of an array of little-endian words, and a bit length. +*/ +function binlMD5(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << (len % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var i; + var olda; + var oldb; + var oldc; + var oldd; + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for (i = 0; i < x.length; i += 16) { + olda = a; + oldb = b; + oldc = c; + oldd = d; + + a = md5ff(a, b, c, d, x[i], 7, -680876936); + d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); + c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); + b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330); + a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); + d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); + c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); + b = md5ff(b, c, d, a, x[i + 7], 22, -45705983); + a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); + d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); + c = md5ff(c, d, a, b, x[i + 10], 17, -42063); + b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162); + a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); + d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); + c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); + b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329); + + a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); + d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); + c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); + b = md5gg(b, c, d, a, x[i], 20, -373897302); + a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); + d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); + c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); + b = md5gg(b, c, d, a, x[i + 4], 20, -405537848); + a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); + d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); + c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); + b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501); + a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); + d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); + c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); + b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734); + + a = md5hh(a, b, c, d, x[i + 5], 4, -378558); + d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); + c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); + b = md5hh(b, c, d, a, x[i + 14], 23, -35309556); + a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); + d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); + c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); + b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640); + a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); + d = md5hh(d, a, b, c, x[i], 11, -358537222); + c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); + b = md5hh(b, c, d, a, x[i + 6], 23, 76029189); + a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); + d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); + c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); + b = md5hh(b, c, d, a, x[i + 2], 23, -995338651); + + a = md5ii(a, b, c, d, x[i], 6, -198630844); + d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); + c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); + b = md5ii(b, c, d, a, x[i + 5], 21, -57434055); + a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); + d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); + c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); + b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799); + a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); + d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); + c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); + b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649); + a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); + d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); + c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); + b = md5ii(b, c, d, a, x[i + 9], 21, -343485551); + + a = safeAdd(a, olda); + b = safeAdd(b, oldb); + c = safeAdd(c, oldc); + d = safeAdd(d, oldd); + } + return [a, b, c, d] +} + +/* +* Convert an array of little-endian words to a string +*/ +function binl2rstr(input) { + var i; + var output = ''; + var length32 = input.length * 32; + for (i = 0; i < length32; i += 8) { + output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF); + } + return output +} + +/* +* Convert a raw string to an array of little-endian words +* Characters >255 have their high-byte silently ignored. +*/ +function rstr2binl(input) { + var i; + var output = []; + output[(input.length >> 2) - 1] = undefined; + for (i = 0; i < output.length; i += 1) { + output[i] = 0; + } + var length8 = input.length * 8; + for (i = 0; i < length8; i += 8) { + output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32); + } + return output +} + +/* +* Calculate the MD5 of a raw string +*/ +function rstrMD5(s) { + return binl2rstr(binlMD5(rstr2binl(s), s.length * 8)) +} + +/* +* Calculate the HMAC-MD5, of a key and some data (raw strings) +*/ +function rstrHMACMD5(key, data) { + var i; + var bkey = rstr2binl(key); + var ipad = []; + var opad = []; + var hash; + ipad[15] = opad[15] = undefined; + if (bkey.length > 16) { + bkey = binlMD5(bkey, key.length * 8); + } + for (i = 0; i < 16; i += 1) { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); + return binl2rstr(binlMD5(opad.concat(hash), 512 + 128)) +} + +/* +* Convert a raw string to a hex string +*/ +function rstr2hex(input) { + var hexTab = '0123456789abcdef'; + var output = ''; + var x; + var i; + for (i = 0; i < input.length; i += 1) { + x = input.charCodeAt(i); + output += hexTab.charAt((x >>> 4) & 0x0F) + + hexTab.charAt(x & 0x0F); + } + return output +} + +/* +* Encode a string as utf-8 +*/ +function str2rstrUTF8(input) { + return unescape(encodeURIComponent(input)) +} + +/* +* Take string arguments and return either raw or hex encoded strings +*/ +function rawMD5(s) { + return rstrMD5(str2rstrUTF8(s)) +} +function hexMD5(s) { + return rstr2hex(rawMD5(s)) +} +function rawHMACMD5(k, d) { + return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d)) +} +function hexHMACMD5(k, d) { + return rstr2hex(rawHMACMD5(k, d)) +} + +function md5(string, key, raw) { + if (!key) { + if (!raw) { + return hexMD5(string) + } + return rawMD5(string) + } + if (!raw) { + return hexHMACMD5(key, string) + } + return rawHMACMD5(key, string) +} + +// TODO: asm.js +class RTP { + constructor(pkt/*uint8array*/, sdp) { + let bytes = new DataView(pkt.buffer, pkt.byteOffset, pkt.byteLength); + + this.version = bytes.getUint8(0) >>> 6; + this.padding = bytes.getUint8(0) & 0x20 >>> 5; + this.has_extension = bytes.getUint8(0) & 0x10 >>> 4; + this.csrc = bytes.getUint8(0) & 0x0F; + this.marker = bytes.getUint8(1) >>> 7; + this.pt = bytes.getUint8(1) & 0x7F; + this.sequence = bytes.getUint16(2) ; + this.timestamp = bytes.getUint32(4); + this.ssrc = bytes.getUint32(8); + this.csrcs = []; + + let pktIndex=12; + if (this.csrc>0) { + this.csrcs.push(bytes.getUint32(pktIndex)); + pktIndex+=4; + } + if (this.has_extension==1) { + this.extension = bytes.getUint16(pktIndex); + this.ehl = bytes.getUint16(pktIndex+2); + pktIndex+=4; + this.header_data = pkt.slice(pktIndex, this.ehl); + pktIndex += this.ehl; + } + + this.headerLength = pktIndex; + let padLength = 0; + if (this.padding) { + padLength = bytes.getUint8(pkt.byteLength-1); + } + + // this.bodyLength = pkt.byteLength-this.headerLength-padLength; + + this.media = sdp.getMediaBlockByPayloadType(this.pt); + if (null === this.media) { + Log.log(`Media description for payload type: ${this.pt} not provided.`); + } else { + this.type = this.media.ptype;//PayloadType.string_map[this.media.rtpmap[this.media.fmt[0]].name]; + } + + this.data = pkt.subarray(pktIndex); + // this.timestamp = 1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); + // console.log(this); + } + getPayload() { + return this.data; + } + + getTimestampMS() { + return this.timestamp; //1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); + } + + toString() { + return "RTP(" + + "version:" + this.version + ", " + + "padding:" + this.padding + ", " + + "has_extension:" + this.has_extension + ", " + + "csrc:" + this.csrc + ", " + + "marker:" + this.marker + ", " + + "pt:" + this.pt + ", " + + "sequence:" + this.sequence + ", " + + "timestamp:" + this.timestamp + ", " + + "ssrc:" + this.ssrc + ")"; + } + + isVideo(){return this.media.type == 'video';} + isAudio(){return this.media.type == 'audio';} + + +} + +class RTPFactory { + constructor(sdp) { + this.tsOffsets={}; + for (let pay in sdp.media) { + for (let pt of sdp.media[pay].fmt) { + this.tsOffsets[pt] = {last: 0, overflow: 0}; + } + } + } + + build(pkt/*uint8array*/, sdp) { + let rtp = new RTP(pkt, sdp); + + let tsOffset = this.tsOffsets[rtp.pt]; + if (tsOffset) { + rtp.timestamp += tsOffset.overflow; + if (tsOffset.last && Math.abs(rtp.timestamp - tsOffset.last) > 0x7fffffff) { + console.log(`\nlast ts: ${tsOffset.last}\n + new ts: ${rtp.timestamp}\n + new ts adjusted: ${rtp.timestamp+0xffffffff}\n + last overflow: ${tsOffset.overflow}\n + new overflow: ${tsOffset.overflow+0xffffffff}\n + `); + tsOffset.overflow += 0xffffffff; + rtp.timestamp += 0xffffffff; + } + /*if (rtp.timestamp>0xffffffff) { + console.log(`ts: ${rtp.timestamp}, seq: ${rtp.sequence}`); + }*/ + tsOffset.last = rtp.timestamp; + } + + return rtp; + } +} + +class RTSPMessage { + static get RTSP_1_0() {return "RTSP/1.0";} + + constructor(_rtsp_version) { + this.version = _rtsp_version; + } + + build(_cmd, _host, _params={}, _payload=null) { + let requestString = `${_cmd} ${_host} ${this.version}\r\n`; + for (let param in _params) { + requestString+=`${param}: ${_params[param]}\r\n`; + } + // TODO: binary payload + if (_payload) { + requestString+=`Content-Length: ${_payload.length}\r\n`; + } + requestString+='\r\n'; + if (_payload) { + requestString+=_payload; + } + return requestString; + } + + parse(_data) { + let lines = _data.split('\r\n'); + let parsed = { + headers:{}, + body:null, + code: 0, + statusLine: '' + }; + + let match; + [match, parsed.code, parsed.statusLine] = lines[0].match(new RegExp(`${this.version}[ ]+([0-9]{3})[ ]+(.*)`)); + parsed.code = Number(parsed.code); + let lineIdx = 1; + + while (lines[lineIdx]) { + let [k,v] = lines[lineIdx].split(/:(.+)/); + parsed.headers[k.toLowerCase()] = v.trim(); + lineIdx++; + } + + parsed.body = lines.slice(lineIdx).join('\n\r'); + + return parsed; + } + +} + +const MessageBuilder = new RTSPMessage(RTSPMessage.RTSP_1_0); + +// TODO: asm.js +class NALUAsm { + + constructor() { + this.fragmented_nalu = null; + } + + + static parseNALHeader(hdr) { + return { + nri: hdr & 0x60, + type: hdr & 0x1F + } + } + + parseSingleNALUPacket(rawData, header, dts, pts) { + return new NALU(header.type, header.nri, rawData.subarray(0), dts, pts); + } + + parseAggregationPacket(rawData, header, dts, pts) { + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + let nal_start_idx = 0; + let don = null; + if (NALU.STAP_B === header.type) { + don = data.getUint16(nal_start_idx); + nal_start_idx += 2; + } + let ret = []; + while (nal_start_idx < data.byteLength) { + let size = data.getUint16(nal_start_idx); + nal_start_idx += 2; + let header = NALUAsm.parseNALHeader(data.getInt8(nal_start_idx)); + nal_start_idx++; + let nalu = this.parseSingleNALUPacket(rawData.subarray(nal_start_idx, nal_start_idx+size), header, dts, pts); + if (nalu !== null) { + ret.push(nalu); + } + nal_start_idx+=size; + } + return ret; + } + + parseFragmentationUnit(rawData, header, dts, pts) { + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + let nal_start_idx = 0; + let fu_header = data.getUint8(nal_start_idx); + let is_start = (fu_header & 0x80) >>> 7; + let is_end = (fu_header & 0x40) >>> 6; + let payload_type = fu_header & 0x1F; + let ret = null; + + nal_start_idx++; + let don = 0; + if (NALU.FU_B === header.type) { + don = data.getUint16(nal_start_idx); + nal_start_idx += 2; + } + + if (is_start) { + this.fragmented_nalu = new NALU(payload_type, header.nri, rawData.subarray(nal_start_idx), dts, pts); + } + if (this.fragmented_nalu && this.fragmented_nalu.ntype === payload_type) { + if (!is_start) { + this.fragmented_nalu.appendData(rawData.subarray(nal_start_idx)); + } + if (is_end) { + ret = this.fragmented_nalu; + this.fragmented_nalu = null; + return ret; + } + } + return null; + } + + onNALUFragment(rawData, dts, pts) { + + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + + let header = NALUAsm.parseNALHeader(data.getUint8(0)); + + let nal_start_idx = 1; + + let unit = null; + if (header.type > 0 && header.type < 24) { + unit = this.parseSingleNALUPacket(rawData.subarray(nal_start_idx), header, dts, pts); + } else if (NALU.FU_A === header.type || NALU.FU_B === header.type) { + unit = this.parseFragmentationUnit(rawData.subarray(nal_start_idx), header, dts, pts); + } else if (NALU.STAP_A === header.type || NALU.STAP_B === header.type) { + return this.parseAggregationPacket(rawData.subarray(nal_start_idx), header, dts, pts); + } else { + /* 30 - 31 is undefined, ignore those (RFC3984). */ + Log.log('Undefined NAL unit, type: ' + header.type); + return null; + } + if (unit) { + return [unit]; + } + return null; + } +} + +class AACFrame { + + constructor(data, dts, pts) { + this.dts = dts; + this.pts = pts ? pts : this.dts; + + this.data=data;//.subarray(offset); + } + + getData() { + return this.data; + } + + getSize() { + return this.data.byteLength; + } +} + +// import {AACParser} from "../parsers/aac.js"; +// TODO: asm.js +class AACAsm { + constructor() { + this.config = null; + } + + onAACFragment(pkt) { + let rawData = pkt.getPayload(); + if (!pkt.media) { + return null; + } + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + + let sizeLength = Number(pkt.media.fmtp['sizelength'] || 0); + let indexLength = Number(pkt.media.fmtp['indexlength'] || 0); + let indexDeltaLength = Number(pkt.media.fmtp['indexdeltalength'] || 0); + let CTSDeltaLength = Number(pkt.media.fmtp['ctsdeltalength'] || 0); + let DTSDeltaLength = Number(pkt.media.fmtp['dtsdeltalength'] || 0); + let RandomAccessIndication = Number(pkt.media.fmtp['randomaccessindication'] || 0); + let StreamStateIndication = Number(pkt.media.fmtp['streamstateindication'] || 0); + let AuxiliaryDataSizeLength = Number(pkt.media.fmtp['auxiliarydatasizelength'] || 0); + + let configHeaderLength = + sizeLength + Math.max(indexLength, indexDeltaLength) + CTSDeltaLength + DTSDeltaLength + + RandomAccessIndication + StreamStateIndication + AuxiliaryDataSizeLength; + + + let auHeadersLengthPadded = 0; + let offset = 0; + let ts = (Math.round(pkt.getTimestampMS()/1024) << 10) * 90000 / this.config.samplerate; + if (0 !== configHeaderLength) { + /* The AU header section is not empty, read it from payload */ + let auHeadersLengthInBits = data.getUint16(0); // Always 2 octets, without padding + auHeadersLengthPadded = 2 + (auHeadersLengthInBits>>>3) + ((auHeadersLengthInBits & 0x7)?1:0); // Add padding + + //this.config = AACParser.parseAudioSpecificConfig(new Uint8Array(rawData, 0 , auHeadersLengthPadded)); + // TODO: parse config + let frames = []; + let frameOffset=0; + let bits = new BitArray(rawData.subarray(2 + offset)); + let cts = 0; + let dts = 0; + for (let offset=0; offset{ + if (this.connected) { + while (this.transport.dataQueue.length) { + this.onData(this.transport.dataQueue.pop()); + } + } + }; + this._onConnect = this.onConnected.bind(this); + this._onDisconnect = this.onDisconnected.bind(this); + } + + static streamType() { + return null; + } + + destroy() { + this.detachTransport(); + } + + attachTransport(transport) { + if (this.transport) { + this.detachTransport(); + } + this.transport = transport; + this.transport.eventSource.addEventListener('data', this._onData); + this.transport.eventSource.addEventListener('connected', this._onConnect); + this.transport.eventSource.addEventListener('disconnected', this._onDisconnect); + } + + detachTransport() { + if (this.transport) { + this.transport.eventSource.removeEventListener('data', this._onData); + this.transport.eventSource.removeEventListener('connected', this._onConnect); + this.transport.eventSource.removeEventListener('disconnected', this._onDisconnect); + this.transport = null; + } + } + reset() { + + } + + start() { + Log.log('Client started'); + this.paused = false; + // this.startStreamFlush(); + } + + stop() { + Log.log('Client paused'); + this.paused = true; + // this.stopStreamFlush(); + } + + seek(timeOffset) { + + } + + setSource(source) { + this.stop(); + this.endpoint = source; + this.sourceUrl = source.urlpath; + } + + startStreamFlush() { + this.flushInterval = setInterval(()=>{ + if (!this.paused) { + this.eventSource.dispatchEvent('flush'); + } + }, this.options.flush); + } + + stopStreamFlush() { + clearInterval(this.flushInterval); + } + + onData(data) { + + } + + onConnected() { + if (!this.seekable) { + this.transport.dataQueue = []; + this.eventSource.dispatchEvent('clear'); + } + this.connected = true; + } + + onDisconnected() { + this.connected = false; + } + + queryCredentials() { + return Promise.resolve(); + } + + setCredentials(user, password) { + this.endpoint.user = user; + this.endpoint.pass = password; + this.endpoint.auth = `${user}:${password}`; + } +} + +class AACParser { + static get SampleRates() {return [ + 96000, 88200, + 64000, 48000, + 44100, 32000, + 24000, 22050, + 16000, 12000, + 11025, 8000, + 7350];} + + // static Profile = [ + // 0: Null + // 1: AAC Main + // 2: AAC LC (Low Complexity) + // 3: AAC SSR (Scalable Sample Rate) + // 4: AAC LTP (Long Term Prediction) + // 5: SBR (Spectral Band Replication) + // 6: AAC Scalable + // ] + + static parseAudioSpecificConfig(bytesOrBits) { + let config; + if (bytesOrBits.byteLength) { // is byteArray + config = new BitArray(bytesOrBits); + } else { + config = bytesOrBits; + } + + let bitpos = config.bitpos+(config.src.byteOffset+config.bytepos)*8; + let prof = config.readBits(5); + this.codec = `mp4a.40.${prof}`; + let sfi = config.readBits(4); + if (sfi == 0xf) config.skipBits(24); + let channels = config.readBits(4); + + return { + config: bitSlice(new Uint8Array(config.src.buffer), bitpos, bitpos+16), + codec: `mp4a.40.${prof}`, + samplerate: AACParser.SampleRates[sfi], + channels: channels + } + } + + static parseStreamMuxConfig(bytes) { + // ISO_IEC_14496-3 Part 3 Audio. StreamMuxConfig + let config = new BitArray(bytes); + + if (!config.readBits(1)) { + config.skipBits(14); + return AACParser.parseAudioSpecificConfig(config); + } + } +} + +const LOG_TAG$3 = "rtsp:session"; +const Log$8 = getTagged(LOG_TAG$3); + +class RTSPSession { + + constructor(client, sessionId) { + this.state = null; + this.client = client; + this.sessionId = sessionId; + this.url = this.getControlURL(); + } + + reset() { + this.client = null; + } + + start() { + return this.sendPlay(); + } + + stop() { + return this.sendTeardown(); + } + + getControlURL() { + let ctrl = this.client.sdp.getSessionBlock().control; + if (Url.isAbsolute(ctrl)) { + return ctrl; + } else if (!ctrl || '*' === ctrl) { + return this.client.contentBase; + } else { + return `${this.client.contentBase}${ctrl}`; + } + } + + sendRequest(_cmd, _params = {}) { + let params = {}; + if (this.sessionId) { + params['Session'] = this.sessionId; + } + Object.assign(params, _params); + return this.client.sendRequest(_cmd, this.getControlURL(), params); + } + + async sendPlay(pos = 0) { + this.state = RTSPClientSM.STATE_PLAY; + let params = {}; + let range = this.client.sdp.sessionBlock.range; + if (range) { + // TODO: seekable + if (range[0] == -1) { + range[0] = 0;// Do not handle now at the moment + } + // params['Range'] = `${range[2]}=${range[0]}-`; + } + let data = await this.sendRequest('PLAY', params); + this.state = RTSPClientSM.STATE_PLAYING; + return {data: data}; + } + + async sendPause() { + if (!this.client.supports("PAUSE")) { + return; + } + this.state = RTSPClientSM.STATE_PAUSE; + await this.sendRequest("PAUSE"); + this.state = RTSPClientSM.STATE_PAUSED; + } + + async sendTeardown() { + if (this.state != RTSPClientSM.STATE_TEARDOWN) { + this.state = RTSPClientSM.STATE_TEARDOWN; + await this.sendRequest("TEARDOWN"); + Log$8.log('RTSPClient: STATE_TEARDOWN'); + ///this.client.connection.disconnect(); + // TODO: Notify client + } + } +} + +// import {RTP} from './rtp/rtp'; +const LOG_TAG$4 = "client:rtsp"; +const Log$9 = getTagged(LOG_TAG$4); + + + +class RTSPClient extends BaseClient { + constructor(options={flush: 200}) { + super(options); + this.clientSM = new RTSPClientSM(this); + this.clientSM.ontracks = (tracks) => { + this.eventSource.dispatchEvent('tracks', tracks); + this.startStreamFlush(); + }; + this.sampleQueues={}; + } + + static streamType() { + return 'rtsp'; + } + + setSource(url) { + super.setSource(url); + this.clientSM.setSource(url); + } + attachTransport(transport) { + super.attachTransport(transport); + this.clientSM.transport = transport; + } + + detachTransport() { + super.detachTransport(); + this.clientSM.transport = null; + } + + reset() { + super.reset(); + this.sampleQueues={}; + } + + destroy() { + this.clientSM.destroy(); + return super.destroy(); + } + + start() { + super.start(); + if (this.transport) { + return this.transport.ready.then(() => { + return this.clientSM.start(); + }); + } else { + return Promise.reject("no transport attached"); + } + } + + stop() { + super.stop(); + return this.clientSM.stop(); + } + + onData(data) { + this.clientSM.onData(data); + } + + onConnected() { + this.clientSM.onConnected(); + super.onConnected(); + } + + onDisconnected() { + super.onDisconnected(); + this.clientSM.onDisconnected(); + } +} + +class AuthError extends Error { + constructor(msg) { + super(msg); + } +} + +class RTSPError extends Error { + constructor(data) { + super(data.msg); + this.data = data; + } +} + +class RTSPClientSM extends StateMachine { + static get USER_AGENT() {return 'SFRtsp 0.3';} + static get STATE_INITIAL() {return 1 << 0;} + static get STATE_OPTIONS() {return 1 << 1;} + static get STATE_DESCRIBE () {return 1 << 2;} + static get STATE_SETUP() {return 1 << 3;} + static get STATE_STREAMS() {return 1 << 4;} + static get STATE_TEARDOWN() {return 1 << 5;} + static get STATE_PLAY() {return 1 << 6;} + static get STATE_PLAYING() {return 1 << 7;} + static get STATE_PAUSE() {return 1 << 8;} + static get STATE_PAUSED() {return 1 << 9;} + // static STATE_PAUSED = 1 << 6; + + constructor(parent) { + super(); + + this.parent = parent; + this.transport = null; + this.payParser = new RTPPayloadParser(); + this.rtp_channels = new Set(); + this.sessions = {}; + this.ontracks = null; + + this.addState(RTSPClientSM.STATE_INITIAL,{ + }).addState(RTSPClientSM.STATE_OPTIONS, { + activate: this.sendOptions, + finishTransition: this.onOptions + }).addState(RTSPClientSM.STATE_DESCRIBE, { + activate: this.sendDescribe, + finishTransition: this.onDescribe + }).addState(RTSPClientSM.STATE_SETUP, { + activate: this.sendSetup, + finishTransition: this.onSetup + }).addState(RTSPClientSM.STATE_STREAMS, { + + }).addState(RTSPClientSM.STATE_TEARDOWN, { + activate: ()=>{ + this.started = false; + }, + finishTransition: ()=>{ + return this.transitionTo(RTSPClientSM.STATE_INITIAL) + } + }).addTransition(RTSPClientSM.STATE_INITIAL, RTSPClientSM.STATE_OPTIONS) + .addTransition(RTSPClientSM.STATE_INITIAL, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_OPTIONS, RTSPClientSM.STATE_DESCRIBE) + .addTransition(RTSPClientSM.STATE_DESCRIBE, RTSPClientSM.STATE_SETUP) + .addTransition(RTSPClientSM.STATE_SETUP, RTSPClientSM.STATE_STREAMS) + .addTransition(RTSPClientSM.STATE_TEARDOWN, RTSPClientSM.STATE_INITIAL) + // .addTransition(RTSPClientSM.STATE_STREAMS, RTSPClientSM.STATE_PAUSED) + // .addTransition(RTSPClientSM.STATE_PAUSED, RTSPClientSM.STATE_STREAMS) + .addTransition(RTSPClientSM.STATE_STREAMS, RTSPClientSM.STATE_TEARDOWN) + // .addTransition(RTSPClientSM.STATE_PAUSED, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_SETUP, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_DESCRIBE, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_OPTIONS, RTSPClientSM.STATE_TEARDOWN); + + this.reset(); + + this.shouldReconnect = false; + + // TODO: remove listeners + // this.connection.eventSource.addEventListener('connected', ()=>{ + // if (this.shouldReconnect) { + // this.reconnect(); + // } + // }); + // this.connection.eventSource.addEventListener('disconnected', ()=>{ + // if (this.started) { + // this.shouldReconnect = true; + // } + // }); + // this.connection.eventSource.addEventListener('data', (data)=>{ + // let channel = new DataView(data).getUint8(1); + // if (this.rtp_channels.has(channel)) { + // this.onRTP({packet: new Uint8Array(data, 4), type: channel}); + // } + // + // }); + } + + destroy() { + this.parent = null; + } + + setSource(url) { + this.reset(); + this.endpoint = url; + this.url = `${url.protocol}://${url.location}${url.urlpath}`; + } + + onConnected() { + if (this.rtpFactory) { + this.rtpFactory = null; + } + if (this.shouldReconnect) { + this.start(); + } + } + + async onDisconnected() { + this.reset(); + this.shouldReconnect = true; + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + + start() { + if (this.currentState.name !== RTSPClientSM.STATE_STREAMS) { + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } else { + // TODO: seekable + let promises = []; + for (let session in this.sessions) { + promises.push(this.sessions[session].sendPlay()); + } + return Promise.all(promises); + } + } + + onData(data) { + let channel = data[1]; + if (this.rtp_channels.has(channel)) { + this.onRTP({packet: data.subarray(4), type: channel}); + } + } + + useRTPChannel(channel) { + this.rtp_channels.add(channel); + } + + forgetRTPChannel(channel) { + this.rtp_channels.delete(channel); + } + + stop() { + this.shouldReconnect = false; + let promises = []; + for (let session in this.sessions) { + promises.push(this.sessions[session].sendPause()); + } + return Promise.all(promises); + // this.mse = null; + } + + async reset() { + this.authenticator = ''; + this.methods = []; + this.tracks = []; + this.rtpBuffer={}; + for (let stream in this.streams) { + this.streams[stream].reset(); + } + for (let session in this.sessions) { + this.sessions[session].reset(); + } + this.streams={}; + this.sessions={}; + this.contentBase = ""; + if (this.currentState) { + if (this.currentState.name != RTSPClientSM.STATE_INITIAL) { + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + } else { + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + this.sdp = null; + this.interleaveChannelIndex = 0; + this.session = null; + this.timeOffset = {}; + this.lastTimestamp = {}; + } + + async reconnect() { + //this.parent.eventSource.dispatchEvent('clear'); + await this.reset(); + if (this.currentState.name != RTSPClientSM.STATE_INITIAL) { + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } else { + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } + } + + supports(method) { + return this.methods.includes(method) + } + + parse(_data) { + Log$9.debug(_data.payload); + let d=_data.payload.split('\r\n\r\n'); + let parsed = MessageBuilder.parse(d[0]); + let len = Number(parsed.headers['content-length']); + if (len) { + let d=_data.payload.split('\r\n\r\n'); + parsed.body = d[1]; + } else { + parsed.body=""; + } + return parsed + } + + sendRequest(_cmd, _host, _params={}, _payload=null) { + this.cSeq++; + Object.assign(_params, { + CSeq: this.cSeq, + 'User-Agent': RTSPClientSM.USER_AGENT + }); + if (this.authenticator) { + _params['Authorization'] = this.authenticator(_cmd); + } + return this.send(MessageBuilder.build(_cmd, _host, _params, _payload), _cmd).catch((e)=>{ + if ((e instanceof AuthError) && !_params['Authorization'] ) { + return this.sendRequest(_cmd, _host, _params, _payload); + } else { + throw e; + } + }); + } + + async send(_data, _method) { + if (this.transport) { + try { + await this.transport.ready; + } catch(e) { + this.onDisconnected(); + throw e; + } + Log$9.debug(_data); + let response = await this.transport.send(_data); + let parsed = this.parse(response); + // TODO: parse status codes + if (parsed.code == 401 /*&& !this.authenticator */) { + Log$9.debug(parsed.headers['www-authenticate']); + let auth = parsed.headers['www-authenticate']; + let method = auth.substring(0, auth.indexOf(' ')); + auth = auth.substr(method.length+1); + let chunks = auth.split(','); + + let ep = this.parent.endpoint; + if (!ep.user || !ep.pass) { + try { + await this.parent.queryCredentials.call(this.parent); + } catch (e) { + throw new AuthError(); + } + } + + if (method.toLowerCase() == 'digest') { + let parsedChunks = {}; + for (let chunk of chunks) { + let c = chunk.trim(); + let [k,v] = c.split('='); + parsedChunks[k] = v.substr(1, v.length-2); + } + this.authenticator = (_method)=>{ + let ep = this.parent.endpoint; + let ha1 = md5(`${ep.user}:${parsedChunks.realm}:${ep.pass}`); + let ha2 = md5(`${_method}:${this.url}`); + let response = md5(`${ha1}:${parsedChunks.nonce}:${ha2}`); + let tail=''; // TODO: handle other params + return `Digest username="${ep.user}", realm="${parsedChunks.realm}", nonce="${parsedChunks.nonce}", uri="${this.url}", response="${response}"${tail}`; + }; + } else { + this.authenticator = ()=>{return `Basic ${btoa(this.parent.endpoint.auth)}`;}; + } + + throw new AuthError(parsed); + } + if (parsed.code >= 300) { + Log$9.error(parsed.statusLine); + throw new RTSPError({msg: `RTSP error: ${parsed.code} ${parsed.statusLine}`, parsed: parsed}); + } + return parsed; + } else { + return Promise.reject("No transport attached"); + } + } + + sendOptions() { + this.reset(); + this.started = true; + this.cSeq = 0; + return this.sendRequest('OPTIONS', '*', {}); + } + + onOptions(data) { + this.methods = data.headers['public'].split(',').map((e)=>e.trim()); + this.transitionTo(RTSPClientSM.STATE_DESCRIBE); + } + + sendDescribe() { + return this.sendRequest('DESCRIBE', this.url, { + 'Accept': 'application/sdp' + }).then((data)=>{ + this.sdp = new SDPParser(); + return this.sdp.parse(data.body).catch(()=>{ + throw new Error("Failed to parse SDP"); + }).then(()=>{return data;}); + }); + } + + onDescribe(data) { + this.contentBase = data.headers['content-base'] || this.url;// `${this.endpoint.protocol}://${this.endpoint.location}${this.endpoint.urlpath}/`; + this.tracks = this.sdp.getMediaBlockList(); + this.rtpFactory = new RTPFactory(this.sdp); + + Log$9.log('SDP contained ' + this.tracks.length + ' track(s). Calling SETUP for each.'); + + if (data.headers['session']) { + this.session = data.headers['session']; + } + + if (!this.tracks.length) { + throw new Error("No tracks in SDP"); + } + + this.transitionTo(RTSPClientSM.STATE_SETUP); + } + + sendSetup() { + let streams=[]; + + // TODO: select first video and first audio tracks + for (let track_type of this.tracks) { + Log$9.log("setup track: "+track_type); + // if (track_type=='audio') continue; + // if (track_type=='video') continue; + let track = this.sdp.getMediaBlock(track_type); + if (!PayloadType.string_map[track.rtpmap[track.fmt[0]].name]) continue; + + this.streams[track_type] = new RTSPStream(this, track); + let setupPromise = this.streams[track_type].start(); + this.parent.sampleQueues[PayloadType.string_map[track.rtpmap[track.fmt[0]].name]]=[]; + this.rtpBuffer[track.fmt[0]]=[]; + streams.push(setupPromise.then(({track, data})=>{ + this.timeOffset[track.fmt[0]] = 0; + try { + let rtp_info = data.headers["rtp-info"].split(';'); + for (let chunk of rtp_info) { + let [key, val] = chunk.split("="); + if (key === "rtptime") { + this.timeOffset[track.fmt[0]] = 0;//Number(val); + } + } + } catch (e) { + // new Date().getTime(); + } + let params = { + timescale: 0, + scaleFactor: 0 + }; + if (track.fmtp['sprop-parameter-sets']) { + let sps_pps = track.fmtp['sprop-parameter-sets'].split(','); + params = { + sps:base64ToArrayBuffer(sps_pps[0]), + pps:base64ToArrayBuffer(sps_pps[1]) + }; + } else if (track.fmtp['config']) { + let config = track.fmtp['config']; + this.has_config = track.fmtp['cpresent']!='0'; + let generic = track.rtpmap[track.fmt[0]].name == 'MPEG4-GENERIC'; + if (generic) { + params={config: + AACParser.parseAudioSpecificConfig(hexToByteArray(config)) + }; + this.payParser.aacparser.setConfig(params.config); + } else if (config) { + // todo: parse audio specific config for mpeg4-generic + params={config: + AACParser.parseStreamMuxConfig(hexToByteArray(config)) + }; + this.payParser.aacparser.setConfig(params.config); + } + } + params.duration = this.sdp.sessionBlock.range?this.sdp.sessionBlock.range[1]-this.sdp.sessionBlock.range[0]:1; + this.parent.seekable = (params.duration > 1); + let res = { + track: track, + offset: this.timeOffset[track.fmt[0]], + type: PayloadType.string_map[track.rtpmap[track.fmt[0]].name], + params: params, + duration: params.duration + }; + console.log(res, this.timeOffset); + let session = data.headers.session.split(';')[0]; + if (!this.sessions[session]) { + this.sessions[session] = new RTSPSession(this, session); + } + return res; + })); + } + return Promise.all(streams).then((tracks)=>{ + let sessionPromises = []; + for (let session in this.sessions) { + sessionPromises.push(this.sessions[session].start()); + } + return Promise.all(sessionPromises).then(()=>{ + if (this.ontracks) { + this.ontracks(tracks); + } + }) + }).catch((e)=>{ + console.error(e); + this.stop(); + this.reset(); + }); + } + + onSetup() { + this.transitionTo(RTSPClientSM.STATE_STREAMS); + } + + onRTP(_data) { + if (!this.rtpFactory) return; + + let rtp = this.rtpFactory.build(_data.packet, this.sdp); + if (!rtp.type) { + return; + } + + if (this.timeOffset[rtp.pt] === undefined) { + //console.log(rtp.pt, this.timeOffset[rtp.pt]); + this.rtpBuffer[rtp.pt].push(rtp); + return; + } + + if (this.lastTimestamp[rtp.pt] === undefined) { + this.lastTimestamp[rtp.pt] = rtp.timestamp-this.timeOffset[rtp.pt]; + } + + let queue = this.rtpBuffer[rtp.pt]; + queue.push(rtp); + + while (queue.length) { + let rtp = queue.shift(); + + rtp.timestamp = rtp.timestamp-this.timeOffset[rtp.pt]-this.lastTimestamp[rtp.pt]; + // TODO: overflow + // if (rtp.timestamp < 0) { + // rtp.timestamp = (rtp.timestamp + Number.MAX_SAFE_INTEGER) % 0x7fffffff; + // } + if (rtp.media) { + let pay = this.payParser.parse(rtp); + if (pay) { + // if (typeof pay == typeof []) { + this.parent.sampleQueues[rtp.type].push(pay); + // } else { + // this.parent.sampleQueues[rtp.type].push([pay]); + // } + } + } + } + // this.remuxer.feedRTP(); + } +} + +// ASN.1 JavaScript decoder +// Copyright (c) 2008-2013 Lapo Luchini + +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +/*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ +/*global oids */ + +var hardLimit = 100; +var ellipsis = "\u2026"; +var DOM = { + tag: function (tagName, className) { + var t = document.createElement(tagName); + t.className = className; + return t; + }, + text: function (str) { + return document.createTextNode(str); + } + }; + +class Stream { + static get hexDigits() { + return "0123456789ABCDEF"; + }; + + static get reTime() { + return /^((?:1[89]|2\d)?\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/; + } + + + constructor(enc, pos) { + if (enc instanceof Stream) { + this.enc = enc.enc; + this.pos = enc.pos; + } else { + this.enc = enc; + this.pos = pos; + } + } + + get(pos) { + if (pos === undefined) + pos = this.pos++; + if (pos >= this.enc.length) + throw 'Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length; + return this.enc[pos]; + }; + + hexByte(b) { + return Stream.hexDigits.charAt((b >> 4) & 0xF) + Stream.hexDigits.charAt(b & 0xF); + }; + + hexDump(start, end, raw) { + var s = ""; + for (var i = start; i < end; ++i) { + s += this.hexByte(this.get(i)); + if (raw !== true) + switch (i & 0xF) { + case 0x7: + s += " "; + break; + case 0xF: + s += "\n"; + break; + default: + s += " "; + } + } + return s; + }; + + parseStringISO(start, end) { + var s = ""; + for (var i = start; i < end; ++i) + s += String.fromCharCode(this.get(i)); + return s; + }; + + parseStringUTF(start, end) { + var s = ""; + for (var i = start; i < end;) { + var c = this.get(i++); + if (c < 128) + s += String.fromCharCode(c); + else if ((c > 191) && (c < 224)) + s += String.fromCharCode(((c & 0x1F) << 6) | (this.get(i++) & 0x3F)); + else + s += String.fromCharCode(((c & 0x0F) << 12) | ((this.get(i++) & 0x3F) << 6) | (this.get(i++) & 0x3F)); + } + return s; + }; + + parseStringBMP(start, end) { + var str = ""; + for (var i = start; i < end; i += 2) { + var high_byte = this.get(i); + var low_byte = this.get(i + 1); + str += String.fromCharCode((high_byte << 8) + low_byte); + } + + return str; + }; + + parseTime(start, end) { + var s = this.parseStringISO(start, end), + m = Stream.reTime.exec(s); + if (!m) + return "Unrecognized time: " + s; + s = m[1] + "-" + m[2] + "-" + m[3] + " " + m[4]; + if (m[5]) { + s += ":" + m[5]; + if (m[6]) { + s += ":" + m[6]; + if (m[7]) + s += "." + m[7]; + } + } + if (m[8]) { + s += " UTC"; + if (m[8] != 'Z') { + s += m[8]; + if (m[9]) + s += ":" + m[9]; + } + } + return s; + }; + + parseInteger(start, end) { + //TODO support negative numbers + var len = end - start; + if (len > 4) { + len <<= 3; + var s = this.get(start); + if (s === 0) + len -= 8; + else + while (s < 128) { + s <<= 1; + --len; + } + return "(" + len + " bit)"; + } + var n = 0; + for (var i = start; i < end; ++i) + n = (n << 8) | this.get(i); + return n; + }; + + parseBitString(start, end) { + var unusedBit = this.get(start), + lenBit = ((end - start - 1) << 3) - unusedBit, + s = "(" + lenBit + " bit)"; + if (lenBit <= 20) { + var skip = unusedBit; + s += " "; + for (var i = end - 1; i > start; --i) { + var b = this.get(i); + for (var j = skip; j < 8; ++j) + s += (b >> j) & 1 ? "1" : "0"; + skip = 0; + } + } + return s; + }; + + parseOctetString(start, end) { + var len = end - start, + s = "(" + len + " byte) "; + if (len > hardLimit) + end = start + hardLimit; + for (var i = start; i < end; ++i) + s += this.hexByte(this.get(i)); //TODO: also try Latin1? + if (len > hardLimit) + s += ellipsis; + return s; + }; + + parseOID(start, end) { + var s = '', + n = 0, + bits = 0; + for (var i = start; i < end; ++i) { + var v = this.get(i); + n = (n << 7) | (v & 0x7F); + bits += 7; + if (!(v & 0x80)) { // finished + if (s === '') { + var m = n < 80 ? n < 40 ? 0 : 1 : 2; + s = m + "." + (n - m * 40); + } else + s += "." + ((bits >= 31) ? "bigint" : n); + n = bits = 0; + } + } + return s; + }; +} + +class ASN1 { + static get reSeemsASCII() { + return /^[ -~]+$/; + } + + constructor(stream, header, length, tag, sub) { + this.stream = stream; + this.header = header; + this.length = length; + this.tag = tag; + this.sub = sub; + } + + typeName() { + if (this.tag === undefined) + return "unknown"; + var tagClass = this.tag >> 6, + tagConstructed = (this.tag >> 5) & 1, + tagNumber = this.tag & 0x1F; + switch (tagClass) { + case 0: // universal + switch (tagNumber) { + case 0x00: + return "EOC"; + case 0x01: + return "BOOLEAN"; + case 0x02: + return "INTEGER"; + case 0x03: + return "BIT_STRING"; + case 0x04: + return "OCTET_STRING"; + case 0x05: + return "NULL"; + case 0x06: + return "OBJECT_IDENTIFIER"; + case 0x07: + return "ObjectDescriptor"; + case 0x08: + return "EXTERNAL"; + case 0x09: + return "REAL"; + case 0x0A: + return "ENUMERATED"; + case 0x0B: + return "EMBEDDED_PDV"; + case 0x0C: + return "UTF8String"; + case 0x10: + return "SEQUENCE"; + case 0x11: + return "SET"; + case 0x12: + return "NumericString"; + case 0x13: + return "PrintableString"; // ASCII subset + case 0x14: + return "TeletexString"; // aka T61String + case 0x15: + return "VideotexString"; + case 0x16: + return "IA5String"; // ASCII + case 0x17: + return "UTCTime"; + case 0x18: + return "GeneralizedTime"; + case 0x19: + return "GraphicString"; + case 0x1A: + return "VisibleString"; // ASCII subset + case 0x1B: + return "GeneralString"; + case 0x1C: + return "UniversalString"; + case 0x1E: + return "BMPString"; + default: + return "Universal_" + tagNumber.toString(16); + } + case 1: + return "Application_" + tagNumber.toString(16); + case 2: + return "[" + tagNumber + "]"; // Context + case 3: + return "Private_" + tagNumber.toString(16); + } + } + + content() { + if (this.tag === undefined) + return null; + var tagClass = this.tag >> 6, + tagNumber = this.tag & 0x1F, + content = this.posContent(), + len = Math.abs(this.length); + if (tagClass !== 0) { // universal + if (this.sub !== null) + return "(" + this.sub.length + " elem)"; + //TODO: TRY TO PARSE ASCII STRING + var s = this.stream.parseStringISO(content, content + Math.min(len, hardLimit)); + if (ASN1.reSeemsASCII.test(s)) + return s.substring(0, 2 * hardLimit) + ((s.length > 2 * hardLimit) ? ellipsis : ""); + else + return this.stream.parseOctetString(content, content + len); + } + switch (tagNumber) { + case 0x01: // BOOLEAN + return (this.stream.get(content) === 0) ? "false" : "true"; + case 0x02: // INTEGER + return this.stream.parseInteger(content, content + len); + case 0x03: // BIT_STRING + return this.sub ? "(" + this.sub.length + " elem)" : + this.stream.parseBitString(content, content + len); + case 0x04: // OCTET_STRING + return this.sub ? "(" + this.sub.length + " elem)" : + this.stream.parseOctetString(content, content + len); + //case 0x05: // NULL + case 0x06: // OBJECT_IDENTIFIER + return this.stream.parseOID(content, content + len); + //case 0x07: // ObjectDescriptor + //case 0x08: // EXTERNAL + //case 0x09: // REAL + //case 0x0A: // ENUMERATED + //case 0x0B: // EMBEDDED_PDV + case 0x10: // SEQUENCE + case 0x11: // SET + return "(" + this.sub.length + " elem)"; + case 0x0C: // UTF8String + return this.stream.parseStringUTF(content, content + len); + case 0x12: // NumericString + case 0x13: // PrintableString + case 0x14: // TeletexString + case 0x15: // VideotexString + case 0x16: // IA5String + //case 0x19: // GraphicString + case 0x1A: // VisibleString + //case 0x1B: // GeneralString + //case 0x1C: // UniversalString + return this.stream.parseStringISO(content, content + len); + case 0x1E: // BMPString + return this.stream.parseStringBMP(content, content + len); + case 0x17: // UTCTime + case 0x18: // GeneralizedTime + return this.stream.parseTime(content, content + len); + } + return null; + }; + + toString() { + return this.typeName() + "@" + this.stream.pos + "[header:" + this.header + ",length:" + this.length + ",sub:" + ((this.sub === null) ? 'null' : this.sub.length) + "]"; + }; + + print(indent) { + if (indent === undefined) indent = ''; + document.writeln(indent + this); + if (this.sub !== null) { + indent += ' '; + for (var i = 0, max = this.sub.length; i < max; ++i) + this.sub[i].print(indent); + } + }; + + toPrettyString(indent) { + if (indent === undefined) indent = ''; + var s = indent + this.typeName() + " @" + this.stream.pos; + if (this.length >= 0) + s += "+"; + s += this.length; + if (this.tag & 0x20) + s += " (constructed)"; + else if (((this.tag == 0x03) || (this.tag == 0x04)) && (this.sub !== null)) + s += " (encapsulates)"; + s += "\n"; + if (this.sub !== null) { + indent += ' '; + for (var i = 0, max = this.sub.length; i < max; ++i) + s += this.sub[i].toPrettyString(indent); + } + return s; + }; + + toDOM() { + var node = DOM.tag("div", "node"); + node.asn1 = this; + var head = DOM.tag("div", "head"); + var s = this.typeName().replace(/_/g, " "); + head.innerHTML = s; + var content = this.content(); + if (content !== null) { + content = String(content).replace(/"; + s += "Length: " + this.header + "+"; + if (this.length >= 0) + s += this.length; + else + s += (-this.length) + " (undefined)"; + if (this.tag & 0x20) + s += "
(constructed)"; + else if (((this.tag == 0x03) || (this.tag == 0x04)) && (this.sub !== null)) + s += "
(encapsulates)"; + //TODO if (this.tag == 0x03) s += "Unused bits: " + if (content !== null) { + s += "
Value:
" + content + ""; + if ((typeof oids === 'object') && (this.tag == 0x06)) { + var oid = oids[content]; + if (oid) { + if (oid.d) s += "
" + oid.d; + if (oid.c) s += "
" + oid.c; + if (oid.w) s += "
(warning!)"; + } + } + } + value.innerHTML = s; + node.appendChild(value); + var sub = DOM.tag("div", "sub"); + if (this.sub !== null) { + for (var i = 0, max = this.sub.length; i < max; ++i) + sub.appendChild(this.sub[i].toDOM()); + } + node.appendChild(sub); + head.onclick = function () { + node.className = (node.className == "node collapsed") ? "node" : "node collapsed"; + }; + return node; + }; + + posStart() { + return this.stream.pos; + }; + + posContent() { + return this.stream.pos + this.header; + }; + + posEnd() { + return this.stream.pos + this.header + Math.abs(this.length); + }; + + fakeHover(current) { + this.node.className += " hover"; + if (current) + this.head.className += " hover"; + }; + + fakeOut(current) { + var re = / ?hover/; + this.node.className = this.node.className.replace(re, ""); + if (current) + this.head.className = this.head.className.replace(re, ""); + }; + + toHexDOM_sub(node, className, stream, start, end) { + if (start >= end) + return; + var sub = DOM.tag("span", className); + sub.appendChild(DOM.text( + stream.hexDump(start, end))); + node.appendChild(sub); + }; + + toHexDOM(root) { + var node = DOM.tag("span", "hex"); + if (root === undefined) root = node; + this.head.hexNode = node; + this.head.onmouseover = function () { + this.hexNode.className = "hexCurrent"; + }; + this.head.onmouseout = function () { + this.hexNode.className = "hex"; + }; + node.asn1 = this; + node.onmouseover = function () { + var current = !root.selected; + if (current) { + root.selected = this.asn1; + this.className = "hexCurrent"; + } + this.asn1.fakeHover(current); + }; + node.onmouseout = function () { + var current = (root.selected == this.asn1); + this.asn1.fakeOut(current); + if (current) { + root.selected = null; + this.className = "hex"; + } + }; + this.toHexDOM_sub(node, "tag", this.stream, this.posStart(), this.posStart() + 1); + this.toHexDOM_sub(node, (this.length >= 0) ? "dlen" : "ulen", this.stream, this.posStart() + 1, this.posContent()); + if (this.sub === null) + node.appendChild(DOM.text( + this.stream.hexDump(this.posContent(), this.posEnd()))); + else if (this.sub.length > 0) { + var first = this.sub[0]; + var last = this.sub[this.sub.length - 1]; + this.toHexDOM_sub(node, "intro", this.stream, this.posContent(), first.posStart()); + for (var i = 0, max = this.sub.length; i < max; ++i) + node.appendChild(this.sub[i].toHexDOM(root)); + this.toHexDOM_sub(node, "outro", this.stream, last.posEnd(), this.posEnd()); + } + return node; + }; + + toHexString(root) { + return this.stream.hexDump(this.posStart(), this.posEnd(), true); + }; + +} + +ASN1.decodeLength = function (stream) { + var buf = stream.get(), + len = buf & 0x7F; + if (len == buf) + return len; + if (len > 3) + throw "Length over 24 bits not supported at position " + (stream.pos - 1); + if (len === 0) + return -1; // undefined + buf = 0; + for (var i = 0; i < len; ++i) + buf = (buf << 8) | stream.get(); + return buf; +}; +ASN1.hasContent = function (tag, len, stream) { + if (tag & 0x20) // constructed + return true; + if ((tag < 0x03) || (tag > 0x04)) + return false; + var p = new Stream(stream); + if (tag == 0x03) p.get(); // BitString unused bits, must be in [0, 7] + var subTag = p.get(); + if ((subTag >> 6) & 0x01) // not (universal or context) + return false; + try { + var subLength = ASN1.decodeLength(p); + return ((p.pos - stream.pos) + subLength == len); + } catch (exception) { + return false; + } +}; +ASN1.decode = function (stream) { + if (!(stream instanceof Stream)) + stream = new Stream(stream, 0); + var streamStart = new Stream(stream), + tag = stream.get(), + len = ASN1.decodeLength(stream), + header = stream.pos - streamStart.pos, + sub = null; + if (ASN1.hasContent(tag, len, stream)) { + // it has content, so we decode it + var start = stream.pos; + if (tag == 0x03) stream.get(); // skip BitString unused bits, must be in [0, 7] + sub = []; + if (len >= 0) { + // definite length + var end = start + len; + while (stream.pos < end) + sub[sub.length] = ASN1.decode(stream); + if (stream.pos != end) + throw "Content size is not correct for container starting at offset " + start; + } else { + // undefined length + try { + for (; ;) { + var s = ASN1.decode(stream); + if (s.tag === 0) + break; + sub[sub.length] = s; + } + len = start - stream.pos; + } catch (e) { + throw "Exception while decoding undefined length content: " + e; + } + } + } else + stream.pos += len; // skip content + return new ASN1(streamStart, header, len, tag, sub); +}; +ASN1.test = function () { + var test = [ + {value: [0x27], expected: 0x27}, + {value: [0x81, 0xC9], expected: 0xC9}, + {value: [0x83, 0xFE, 0xDC, 0xBA], expected: 0xFEDCBA} + ]; + for (var i = 0, max = test.length; i < max; ++i) { + var pos = 0, + stream = new Stream(test[i].value, 0), + res = ASN1.decodeLength(stream); + if (res != test[i].expected) + document.write("In test[" + i + "] expected " + test[i].expected + " got " + res + "\n"); + } +}; + +// prng4.js - uses Arcfour as a PRNG + +class Arcfour { + constructor() { + this.i = 0; + this.j = 0; + this.S = []; + } +} + +// Initialize arcfour context from key, an array of ints, each from [0..255] +function ARC4init(key) { + var i, j, t; + for(i = 0; i < 256; ++i) + this.S[i] = i; + j = 0; + for(i = 0; i < 256; ++i) { + j = (j + this.S[i] + key[i % key.length]) & 255; + t = this.S[i]; + this.S[i] = this.S[j]; + this.S[j] = t; + } + this.i = 0; + this.j = 0; +} + +function ARC4next() { + var t; + this.i = (this.i + 1) & 255; + this.j = (this.j + this.S[this.i]) & 255; + t = this.S[this.i]; + this.S[this.i] = this.S[this.j]; + this.S[this.j] = t; + return this.S[(t + this.S[this.i]) & 255]; +} + +Arcfour.prototype.init = ARC4init; +Arcfour.prototype.next = ARC4next; + +// Plug in your RNG constructor here +function prng_newstate() { + return new Arcfour(); +} + +// Pool size must be a multiple of 4 and greater than 32. +// An array of bytes the size of the pool will be passed to init() +var rng_psize = 256; + +// Random number generator - requires a PRNG backend, e.g. prng4.js +var rng_state; +var rng_pool; +var rng_pptr; + +// Initialize the pool with junk if needed. +if(rng_pool == null) { + rng_pool = new Array(); + rng_pptr = 0; + var t; + if(window.crypto && window.crypto.getRandomValues) { + // Extract entropy (2048 bits) from RNG if available + var z = new Uint32Array(256); + window.crypto.getRandomValues(z); + for (t = 0; t < z.length; ++t) + rng_pool[rng_pptr++] = z[t] & 255; + } + + // Use mouse events for entropy, if we do not have enough entropy by the time + // we need it, entropy will be generated by Math.random. + var onMouseMoveListener = function(ev) { + this.count = this.count || 0; + if (this.count >= 256 || rng_pptr >= rng_psize) { + if (window.removeEventListener) + window.removeEventListener("mousemove", onMouseMoveListener, false); + else if (window.detachEvent) + window.detachEvent("onmousemove", onMouseMoveListener); + return; + } + try { + var mouseCoordinates = ev.x + ev.y; + rng_pool[rng_pptr++] = mouseCoordinates & 255; + this.count += 1; + } catch (e) { + // Sometimes Firefox will deny permission to access event properties for some reason. Ignore. + } + }; + if (window.addEventListener) + window.addEventListener("mousemove", onMouseMoveListener, false); + else if (window.attachEvent) + window.attachEvent("onmousemove", onMouseMoveListener); + +} + +function rng_get_byte() { + if(rng_state == null) { + rng_state = prng_newstate(); + // At this point, we may not have collected enough entropy. If not, fall back to Math.random + while (rng_pptr < rng_psize) { + var random = Math.floor(65536 * Math.random()); + rng_pool[rng_pptr++] = random & 255; + } + rng_state.init(rng_pool); + for(rng_pptr = 0; rng_pptr < rng_pool.length; ++rng_pptr) + rng_pool[rng_pptr] = 0; + rng_pptr = 0; + } + // TODO: allow reseeding after first request + return rng_state.next(); +} + +function rng_get_bytes(ba) { + var i; + for(i = 0; i < ba.length; ++i) ba[i] = rng_get_byte(); +} + +class SecureRandom { + constructor(){} +} + +SecureRandom.prototype.nextBytes = rng_get_bytes; + +// Copyright (c) 2005 Tom Wu +// All Rights Reserved. +// See "LICENSE" for details. + +// Basic JavaScript BN library - subset useful for RSA encryption. + +// Bits per digit +var dbits; + +// JavaScript engine analysis +var canary = 0xdeadbeefcafe; +var j_lm = ((canary&0xffffff)==0xefcafe); + +// (public) Constructor +class BigInteger { + constructor(a,b,c) { + if (a != null) + if ("number" == typeof a) this.fromNumber(a, b, c); + else if (b == null && "string" != typeof a) this.fromString(a, 256); + else this.fromString(a, b); + } +} + +// return new, unset BigInteger +function nbi() { return new BigInteger(null); } + +// am: Compute w_j += (x*this_i), propagate carries, +// c is initial carry, returns final carry. +// c < 3*dvalue, x < 2*dvalue, this_i < dvalue +// We need to select the fastest one that works in this environment. + +// am1: use a single mult and divide to get the high bits, +// max digit bits should be 26 because +// max internal value = 2*dvalue^2-2*dvalue (< 2^53) +function am1(i,x,w,j,c,n) { + while(--n >= 0) { + var v = x*this[i++]+w[j]+c; + c = Math.floor(v/0x4000000); + w[j++] = v&0x3ffffff; + } + return c; +} +// am2 avoids a big mult-and-extract completely. +// Max digit bits should be <= 30 because we do bitwise ops +// on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) +function am2(i,x,w,j,c,n) { + var xl = x&0x7fff, xh = x>>15; + while(--n >= 0) { + var l = this[i]&0x7fff; + var h = this[i++]>>15; + var m = xh*l+h*xl; + l = xl*l+((m&0x7fff)<<15)+w[j]+(c&0x3fffffff); + c = (l>>>30)+(m>>>15)+xh*h+(c>>>30); + w[j++] = l&0x3fffffff; + } + return c; +} +// Alternately, set max digit bits to 28 since some +// browsers slow down when dealing with 32-bit numbers. +function am3(i,x,w,j,c,n) { + var xl = x&0x3fff, xh = x>>14; + while(--n >= 0) { + var l = this[i]&0x3fff; + var h = this[i++]>>14; + var m = xh*l+h*xl; + l = xl*l+((m&0x3fff)<<14)+w[j]+c; + c = (l>>28)+(m>>14)+xh*h; + w[j++] = l&0xfffffff; + } + return c; +} +if(j_lm && (navigator.appName == "Microsoft Internet Explorer")) { + BigInteger.prototype.am = am2; + dbits = 30; +} +else if(j_lm && (navigator.appName != "Netscape")) { + BigInteger.prototype.am = am1; + dbits = 26; +} +else { // Mozilla/Netscape seems to prefer am3 + BigInteger.prototype.am = am3; + dbits = 28; +} + +BigInteger.prototype.DB = dbits; +BigInteger.prototype.DM = ((1<= 0; --i) r[i] = this[i]; + r.t = this.t; + r.s = this.s; +} + +// (protected) set from integer value x, -DV <= x < DV +function bnpFromInt(x) { + this.t = 1; + this.s = (x<0)?-1:0; + if(x > 0) this[0] = x; + else if(x < -1) this[0] = x+this.DV; + else this.t = 0; +} + +// return bigint initialized to value +function nbv(i) { var r = nbi(); r.fromInt(i); return r; } + +// (protected) set from string and radix +function bnpFromString(s,b) { + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 256) k = 8; // byte array + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else { this.fromRadix(s,b); return; } + this.t = 0; + this.s = 0; + var i = s.length, mi = false, sh = 0; + while(--i >= 0) { + var x = (k==8)?s[i]&0xff:intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-") mi = true; + continue; + } + mi = false; + if(sh == 0) + this[this.t++] = x; + else if(sh+k > this.DB) { + this[this.t-1] |= (x&((1<<(this.DB-sh))-1))<>(this.DB-sh)); + } + else + this[this.t-1] |= x<= this.DB) sh -= this.DB; + } + if(k == 8 && (s[0]&0x80) != 0) { + this.s = -1; + if(sh > 0) this[this.t-1] |= ((1<<(this.DB-sh))-1)< 0 && this[this.t-1] == c) --this.t; +} + +// (public) return string representation in given radix +function bnToString(b) { + if(this.s < 0) return "-"+this.negate().toString(b); + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else return this.toRadix(b); + var km = (1< 0) { + if(p < this.DB && (d = this[i]>>p) > 0) { m = true; r = int2char(d); } + while(i >= 0) { + if(p < k) { + d = (this[i]&((1<>(p+=this.DB-k); + } + else { + d = (this[i]>>(p-=k))&km; + if(p <= 0) { p += this.DB; --i; } + } + if(d > 0) m = true; + if(m) r += int2char(d); + } + } + return m?r:"0"; +} + +// (public) -this +function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; } + +// (public) |this| +function bnAbs() { return (this.s<0)?this.negate():this; } + +// (public) return + if this > a, - if this < a, 0 if equal +function bnCompareTo(a) { + var r = this.s-a.s; + if(r != 0) return r; + var i = this.t; + r = i-a.t; + if(r != 0) return (this.s<0)?-r:r; + while(--i >= 0) if((r=this[i]-a[i]) != 0) return r; + return 0; +} + +// returns bit length of the integer x +function nbits(x) { + var r = 1, t; + if((t=x>>>16) != 0) { x = t; r += 16; } + if((t=x>>8) != 0) { x = t; r += 8; } + if((t=x>>4) != 0) { x = t; r += 4; } + if((t=x>>2) != 0) { x = t; r += 2; } + if((t=x>>1) != 0) { x = t; r += 1; } + return r; +} + +// (public) return the number of bits in "this" +function bnBitLength() { + if(this.t <= 0) return 0; + return this.DB*(this.t-1)+nbits(this[this.t-1]^(this.s&this.DM)); +} + +// (protected) r = this << n*DB +function bnpDLShiftTo(n,r) { + var i; + for(i = this.t-1; i >= 0; --i) r[i+n] = this[i]; + for(i = n-1; i >= 0; --i) r[i] = 0; + r.t = this.t+n; + r.s = this.s; +} + +// (protected) r = this >> n*DB +function bnpDRShiftTo(n,r) { + for(var i = n; i < this.t; ++i) r[i-n] = this[i]; + r.t = Math.max(this.t-n,0); + r.s = this.s; +} + +// (protected) r = this << n +function bnpLShiftTo(n,r) { + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<= 0; --i) { + r[i+ds+1] = (this[i]>>cbs)|c; + c = (this[i]&bm)<= 0; --i) r[i] = 0; + r[ds] = c; + r.t = this.t+ds+1; + r.s = this.s; + r.clamp(); +} + +// (protected) r = this >> n +function bnpRShiftTo(n,r) { + r.s = this.s; + var ds = Math.floor(n/this.DB); + if(ds >= this.t) { r.t = 0; return; } + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<>bs; + for(var i = ds+1; i < this.t; ++i) { + r[i-ds-1] |= (this[i]&bm)<>bs; + } + if(bs > 0) r[this.t-ds-1] |= (this.s&bm)<>= this.DB; + } + if(a.t < this.t) { + c -= a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c -= a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c -= a.s; + } + r.s = (c<0)?-1:0; + if(c < -1) r[i++] = this.DV+c; + else if(c > 0) r[i++] = c; + r.t = i; + r.clamp(); +} + +// (protected) r = this * a, r != this,a (HAC 14.12) +// "this" should be the larger one if appropriate. +function bnpMultiplyTo(a,r) { + var x = this.abs(), y = a.abs(); + var i = x.t; + r.t = i+y.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < y.t; ++i) r[i+x.t] = x.am(0,y[i],r,i,0,x.t); + r.s = 0; + r.clamp(); + if(this.s != a.s) BigInteger.ZERO.subTo(r,r); +} + +// (protected) r = this^2, r != this (HAC 14.16) +function bnpSquareTo(r) { + var x = this.abs(); + var i = r.t = 2*x.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < x.t-1; ++i) { + var c = x.am(i,x[i],r,2*i,0,1); + if((r[i+x.t]+=x.am(i+1,2*x[i],r,2*i+1,c,x.t-i-1)) >= x.DV) { + r[i+x.t] -= x.DV; + r[i+x.t+1] = 1; + } + } + if(r.t > 0) r[r.t-1] += x.am(i,x[i],r,2*i,0,1); + r.s = 0; + r.clamp(); +} + +// (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) +// r != q, this != m. q or r may be null. +function bnpDivRemTo(m,q,r) { + var pm = m.abs(); + if(pm.t <= 0) return; + var pt = this.abs(); + if(pt.t < pm.t) { + if(q != null) q.fromInt(0); + if(r != null) this.copyTo(r); + return; + } + if(r == null) r = nbi(); + var y = nbi(), ts = this.s, ms = m.s; + var nsh = this.DB-nbits(pm[pm.t-1]); // normalize modulus + if(nsh > 0) { pm.lShiftTo(nsh,y); pt.lShiftTo(nsh,r); } + else { pm.copyTo(y); pt.copyTo(r); } + var ys = y.t; + var y0 = y[ys-1]; + if(y0 == 0) return; + var yt = y0*(1<1)?y[ys-2]>>this.F2:0); + var d1 = this.FV/yt, d2 = (1<= 0) { + r[r.t++] = 1; + r.subTo(t,r); + } + BigInteger.ONE.dlShiftTo(ys,t); + t.subTo(y,y); // "negative" y so we can replace sub with am later + while(y.t < ys) y[y.t++] = 0; + while(--j >= 0) { + // Estimate quotient digit + var qd = (r[--i]==y0)?this.DM:Math.floor(r[i]*d1+(r[i-1]+e)*d2); + if((r[i]+=y.am(0,qd,r,j,0,ys)) < qd) { // Try it out + y.dlShiftTo(j,t); + r.subTo(t,r); + while(r[i] < --qd) r.subTo(t,r); + } + } + if(q != null) { + r.drShiftTo(ys,q); + if(ts != ms) BigInteger.ZERO.subTo(q,q); + } + r.t = ys; + r.clamp(); + if(nsh > 0) r.rShiftTo(nsh,r); // Denormalize remainder + if(ts < 0) BigInteger.ZERO.subTo(r,r); +} + +// (public) this mod a +function bnMod(a) { + var r = nbi(); + this.abs().divRemTo(a,null,r); + if(this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r,r); + return r; +} + +// Modular reduction using "classic" algorithm +class Classic{ + constructor(m){ + this.m = m; + } +} +function cConvert(x) { + if(x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); + else return x; +} +function cRevert(x) { return x; } +function cReduce(x) { x.divRemTo(this.m,null,x); } +function cMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } +function cSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +Classic.prototype.convert = cConvert; +Classic.prototype.revert = cRevert; +Classic.prototype.reduce = cReduce; +Classic.prototype.mulTo = cMulTo; +Classic.prototype.sqrTo = cSqrTo; + +// (protected) return "-1/this % 2^DB"; useful for Mont. reduction +// justification: +// xy == 1 (mod m) +// xy = 1+km +// xy(2-xy) = (1+km)(1-km) +// x[y(2-xy)] = 1-k^2m^2 +// x[y(2-xy)] == 1 (mod m^2) +// if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 +// should reduce x and y(2-xy) by m^2 at each step to keep size bounded. +// JS multiply "overflows" differently from C/C++, so care is needed here. +function bnpInvDigit() { + if(this.t < 1) return 0; + var x = this[0]; + if((x&1) == 0) return 0; + var y = x&3; // y == 1/x mod 2^2 + y = (y*(2-(x&0xf)*y))&0xf; // y == 1/x mod 2^4 + y = (y*(2-(x&0xff)*y))&0xff; // y == 1/x mod 2^8 + y = (y*(2-(((x&0xffff)*y)&0xffff)))&0xffff; // y == 1/x mod 2^16 + // last step - calculate inverse mod DV directly; + // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints + y = (y*(2-x*y%this.DV))%this.DV; // y == 1/x mod 2^dbits + // we really want the negative inverse, and -DV < y < DV + return (y>0)?this.DV-y:-y; +} + +// Montgomery reduction +class Montgomery { + constructor(m) { + this.m = m; + this.mp = m.invDigit(); + this.mpl = this.mp & 0x7fff; + this.mph = this.mp >> 15; + this.um = (1 << (m.DB - 15)) - 1; + this.mt2 = 2 * m.t; + } +} + +// xR mod m +function montConvert(x) { + var r = nbi(); + x.abs().dlShiftTo(this.m.t,r); + r.divRemTo(this.m,null,r); + if(x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r,r); + return r; +} + +// x/R mod m +function montRevert(x) { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; +} + +// x = x/R mod m (HAC 14.32) +function montReduce(x) { + while(x.t <= this.mt2) // pad x so am has enough room later + x[x.t++] = 0; + for(var i = 0; i < this.m.t; ++i) { + // faster way of calculating u0 = x[i]*mp mod DV + var j = x[i]&0x7fff; + var u0 = (j*this.mpl+(((j*this.mph+(x[i]>>15)*this.mpl)&this.um)<<15))&x.DM; + // use am to combine the multiply-shift-add into one call + j = i+this.m.t; + x[j] += this.m.am(0,u0,x,i,0,this.m.t); + // propagate carry + while(x[j] >= x.DV) { x[j] -= x.DV; x[++j]++; } + } + x.clamp(); + x.drShiftTo(this.m.t,x); + if(x.compareTo(this.m) >= 0) x.subTo(this.m,x); +} + +// r = "x^2/R mod m"; x != r +function montSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +// r = "xy/R mod m"; x,y != r +function montMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + +Montgomery.prototype.convert = montConvert; +Montgomery.prototype.revert = montRevert; +Montgomery.prototype.reduce = montReduce; +Montgomery.prototype.mulTo = montMulTo; +Montgomery.prototype.sqrTo = montSqrTo; + +// (protected) true iff this is even +function bnpIsEven() { return ((this.t>0)?(this[0]&1):this.s) == 0; } + +// (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) +function bnpExp(e,z) { + if(e > 0xffffffff || e < 1) return BigInteger.ONE; + var r = nbi(), r2 = nbi(), g = z.convert(this), i = nbits(e)-1; + g.copyTo(r); + while(--i >= 0) { + z.sqrTo(r,r2); + if((e&(1< 0) z.mulTo(r2,g,r); + else { var t = r; r = r2; r2 = t; } + } + return z.revert(r); +} + +// (public) this^e % m, 0 <= e < 2^32 +function bnModPowInt(e,m) { + var z; + if(e < 256 || m.isEven()) z = new Classic(m); else z = new Montgomery(m); + return this.exp(e,z); +} + +// protected +BigInteger.prototype.copyTo = bnpCopyTo; +BigInteger.prototype.fromInt = bnpFromInt; +BigInteger.prototype.fromString = bnpFromString; +BigInteger.prototype.clamp = bnpClamp; +BigInteger.prototype.dlShiftTo = bnpDLShiftTo; +BigInteger.prototype.drShiftTo = bnpDRShiftTo; +BigInteger.prototype.lShiftTo = bnpLShiftTo; +BigInteger.prototype.rShiftTo = bnpRShiftTo; +BigInteger.prototype.subTo = bnpSubTo; +BigInteger.prototype.multiplyTo = bnpMultiplyTo; +BigInteger.prototype.squareTo = bnpSquareTo; +BigInteger.prototype.divRemTo = bnpDivRemTo; +BigInteger.prototype.invDigit = bnpInvDigit; +BigInteger.prototype.isEven = bnpIsEven; +BigInteger.prototype.exp = bnpExp; + +// public +BigInteger.prototype.toString = bnToString; +BigInteger.prototype.negate = bnNegate; +BigInteger.prototype.abs = bnAbs; +BigInteger.prototype.compareTo = bnCompareTo; +BigInteger.prototype.bitLength = bnBitLength; +BigInteger.prototype.mod = bnMod; +BigInteger.prototype.modPowInt = bnModPowInt; + +// "constants" +BigInteger.ZERO = nbv(0); +BigInteger.ONE = nbv(1); + +// Copyright (c) 2005-2009 Tom Wu +// All Rights Reserved. +// See "LICENSE" for details. + +// Extended JavaScript BN functions, required for RSA private ops. + +// Version 1.1: new BigInteger("0", 10) returns "proper" zero +// Version 1.2: square() API, isProbablePrime fix + +// (public) +function bnClone() { var r = nbi(); this.copyTo(r); return r; } + +// (public) return value as integer +function bnIntValue() { + if(this.s < 0) { + if(this.t == 1) return this[0]-this.DV; + else if(this.t == 0) return -1; + } + else if(this.t == 1) return this[0]; + else if(this.t == 0) return 0; + // assumes 16 < DB < 32 + return ((this[1]&((1<<(32-this.DB))-1))<>24; } + +// (public) return value as short (assumes DB>=16) +function bnShortValue() { return (this.t==0)?this.s:(this[0]<<16)>>16; } + +// (protected) return x s.t. r^x < DV +function bnpChunkSize(r) { return Math.floor(Math.LN2*this.DB/Math.log(r)); } + +// (public) 0 if this == 0, 1 if this > 0 +function bnSigNum() { + if(this.s < 0) return -1; + else if(this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0; + else return 1; +} + +// (protected) convert to radix string +function bnpToRadix(b) { + if(b == null) b = 10; + if(this.signum() == 0 || b < 2 || b > 36) return "0"; + var cs = this.chunkSize(b); + var a = Math.pow(b,cs); + var d = nbv(a), y = nbi(), z = nbi(), r = ""; + this.divRemTo(d,y,z); + while(y.signum() > 0) { + r = (a+z.intValue()).toString(b).substr(1) + r; + y.divRemTo(d,y,z); + } + return z.intValue().toString(b) + r; +} + +// (protected) convert from radix string +function bnpFromRadix(s,b) { + this.fromInt(0); + if(b == null) b = 10; + var cs = this.chunkSize(b); + var d = Math.pow(b,cs), mi = false, j = 0, w = 0; + for(var i = 0; i < s.length; ++i) { + var x = intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-" && this.signum() == 0) mi = true; + continue; + } + w = b*w+x; + if(++j >= cs) { + this.dMultiply(d); + this.dAddOffset(w,0); + j = 0; + w = 0; + } + } + if(j > 0) { + this.dMultiply(Math.pow(b,j)); + this.dAddOffset(w,0); + } + if(mi) BigInteger.ZERO.subTo(this,this); +} + +// (protected) alternate constructor +function bnpFromNumber(a,b,c) { + if("number" == typeof b) { + // new BigInteger(int,int,RNG) + if(a < 2) this.fromInt(1); + else { + this.fromNumber(a,c); + if(!this.testBit(a-1)) // force MSB set + this.bitwiseTo(BigInteger.ONE.shiftLeft(a-1),op_or,this); + if(this.isEven()) this.dAddOffset(1,0); // force odd + while(!this.isProbablePrime(b)) { + this.dAddOffset(2,0); + if(this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a-1),this); + } + } + } + else { + // new BigInteger(int,RNG) + var x = [], t = a&7; + x.length = (a>>3)+1; + b.nextBytes(x); + if(t > 0) x[0] &= ((1< 0) { + if(p < this.DB && (d = this[i]>>p) != (this.s&this.DM)>>p) + r[k++] = d|(this.s<<(this.DB-p)); + while(i >= 0) { + if(p < 8) { + d = (this[i]&((1<>(p+=this.DB-8); + } + else { + d = (this[i]>>(p-=8))&0xff; + if(p <= 0) { p += this.DB; --i; } + } + if((d&0x80) != 0) d |= -256; + if(k == 0 && (this.s&0x80) != (d&0x80)) ++k; + if(k > 0 || d != this.s) r[k++] = d; + } + } + return r; +} + +function bnEquals(a) { return(this.compareTo(a)==0); } +function bnMin(a) { return(this.compareTo(a)<0)?this:a; } +function bnMax(a) { return(this.compareTo(a)>0)?this:a; } + +// (protected) r = this op a (bitwise) +function bnpBitwiseTo(a,op,r) { + var i, f, m = Math.min(a.t,this.t); + for(i = 0; i < m; ++i) r[i] = op(this[i],a[i]); + if(a.t < this.t) { + f = a.s&this.DM; + for(i = m; i < this.t; ++i) r[i] = op(this[i],f); + r.t = this.t; + } + else { + f = this.s&this.DM; + for(i = m; i < a.t; ++i) r[i] = op(f,a[i]); + r.t = a.t; + } + r.s = op(this.s,a.s); + r.clamp(); +} + +// (public) this & a +function op_and(x,y) { return x&y; } +function bnAnd(a) { var r = nbi(); this.bitwiseTo(a,op_and,r); return r; } + +// (public) this | a +function op_or(x,y) { return x|y; } +function bnOr(a) { var r = nbi(); this.bitwiseTo(a,op_or,r); return r; } + +// (public) this ^ a +function op_xor(x,y) { return x^y; } +function bnXor(a) { var r = nbi(); this.bitwiseTo(a,op_xor,r); return r; } + +// (public) this & ~a +function op_andnot(x,y) { return x&~y; } +function bnAndNot(a) { var r = nbi(); this.bitwiseTo(a,op_andnot,r); return r; } + +// (public) ~this +function bnNot() { + var r = nbi(); + for(var i = 0; i < this.t; ++i) r[i] = this.DM&~this[i]; + r.t = this.t; + r.s = ~this.s; + return r; +} + +// (public) this << n +function bnShiftLeft(n) { + var r = nbi(); + if(n < 0) this.rShiftTo(-n,r); else this.lShiftTo(n,r); + return r; +} + +// (public) this >> n +function bnShiftRight(n) { + var r = nbi(); + if(n < 0) this.lShiftTo(-n,r); else this.rShiftTo(n,r); + return r; +} + +// return index of lowest 1-bit in x, x < 2^31 +function lbit(x) { + if(x == 0) return -1; + var r = 0; + if((x&0xffff) == 0) { x >>= 16; r += 16; } + if((x&0xff) == 0) { x >>= 8; r += 8; } + if((x&0xf) == 0) { x >>= 4; r += 4; } + if((x&3) == 0) { x >>= 2; r += 2; } + if((x&1) == 0) ++r; + return r; +} + +// (public) returns index of lowest 1-bit (or -1 if none) +function bnGetLowestSetBit() { + for(var i = 0; i < this.t; ++i) + if(this[i] != 0) return i*this.DB+lbit(this[i]); + if(this.s < 0) return this.t*this.DB; + return -1; +} + +// return number of 1 bits in x +function cbit(x) { + var r = 0; + while(x != 0) { x &= x-1; ++r; } + return r; +} + +// (public) return number of set bits +function bnBitCount() { + var r = 0, x = this.s&this.DM; + for(var i = 0; i < this.t; ++i) r += cbit(this[i]^x); + return r; +} + +// (public) true iff nth bit is set +function bnTestBit(n) { + var j = Math.floor(n/this.DB); + if(j >= this.t) return(this.s!=0); + return((this[j]&(1<<(n%this.DB)))!=0); +} + +// (protected) this op (1<>= this.DB; + } + if(a.t < this.t) { + c += a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c += a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += a.s; + } + r.s = (c<0)?-1:0; + if(c > 0) r[i++] = c; + else if(c < -1) r[i++] = this.DV+c; + r.t = i; + r.clamp(); +} + +// (public) this + a +function bnAdd(a) { var r = nbi(); this.addTo(a,r); return r; } + +// (public) this - a +function bnSubtract(a) { var r = nbi(); this.subTo(a,r); return r; } + +// (public) this * a +function bnMultiply(a) { var r = nbi(); this.multiplyTo(a,r); return r; } + +// (public) this^2 +function bnSquare() { var r = nbi(); this.squareTo(r); return r; } + +// (public) this / a +function bnDivide(a) { var r = nbi(); this.divRemTo(a,r,null); return r; } + +// (public) this % a +function bnRemainder(a) { var r = nbi(); this.divRemTo(a,null,r); return r; } + +// (public) [this/a,this%a] +function bnDivideAndRemainder(a) { + var q = nbi(), r = nbi(); + this.divRemTo(a,q,r); + return new Array(q,r); +} + +// (protected) this *= n, this >= 0, 1 < n < DV +function bnpDMultiply(n) { + this[this.t] = this.am(0,n-1,this,0,0,this.t); + ++this.t; + this.clamp(); +} + +// (protected) this += n << w words, this >= 0 +function bnpDAddOffset(n,w) { + if(n == 0) return; + while(this.t <= w) this[this.t++] = 0; + this[w] += n; + while(this[w] >= this.DV) { + this[w] -= this.DV; + if(++w >= this.t) this[this.t++] = 0; + ++this[w]; + } +} + +// A "null" reducer +function NullExp() {} +function nNop(x) { return x; } +function nMulTo(x,y,r) { x.multiplyTo(y,r); } +function nSqrTo(x,r) { x.squareTo(r); } + +NullExp.prototype.convert = nNop; +NullExp.prototype.revert = nNop; +NullExp.prototype.mulTo = nMulTo; +NullExp.prototype.sqrTo = nSqrTo; + +// (public) this^e +function bnPow(e) { return this.exp(e,new NullExp()); } + +// (protected) r = lower n words of "this * a", a.t <= n +// "this" should be the larger one if appropriate. +function bnpMultiplyLowerTo(a,n,r) { + var i = Math.min(this.t+a.t,n); + r.s = 0; // assumes a,this >= 0 + r.t = i; + while(i > 0) r[--i] = 0; + var j; + for(j = r.t-this.t; i < j; ++i) r[i+this.t] = this.am(0,a[i],r,i,0,this.t); + for(j = Math.min(a.t,n); i < j; ++i) this.am(0,a[i],r,i,0,n-i); + r.clamp(); +} + +// (protected) r = "this * a" without lower n words, n > 0 +// "this" should be the larger one if appropriate. +function bnpMultiplyUpperTo(a,n,r) { + --n; + var i = r.t = this.t+a.t-n; + r.s = 0; // assumes a,this >= 0 + while(--i >= 0) r[i] = 0; + for(i = Math.max(n-this.t,0); i < a.t; ++i) + r[this.t+i-n] = this.am(n-i,a[i],r,0,0,this.t+i-n); + r.clamp(); + r.drShiftTo(1,r); +} + +// Barrett modular reduction +function Barrett(m) { + // setup Barrett + this.r2 = nbi(); + this.q3 = nbi(); + BigInteger.ONE.dlShiftTo(2*m.t,this.r2); + this.mu = this.r2.divide(m); + this.m = m; +} + +function barrettConvert(x) { + if(x.s < 0 || x.t > 2*this.m.t) return x.mod(this.m); + else if(x.compareTo(this.m) < 0) return x; + else { var r = nbi(); x.copyTo(r); this.reduce(r); return r; } +} + +function barrettRevert(x) { return x; } + +// x = x mod m (HAC 14.42) +function barrettReduce(x) { + x.drShiftTo(this.m.t-1,this.r2); + if(x.t > this.m.t+1) { x.t = this.m.t+1; x.clamp(); } + this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3); + this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2); + while(x.compareTo(this.r2) < 0) x.dAddOffset(1,this.m.t+1); + x.subTo(this.r2,x); + while(x.compareTo(this.m) >= 0) x.subTo(this.m,x); +} + +// r = x^2 mod m; x != r +function barrettSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +// r = x*y mod m; x,y != r +function barrettMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + +Barrett.prototype.convert = barrettConvert; +Barrett.prototype.revert = barrettRevert; +Barrett.prototype.reduce = barrettReduce; +Barrett.prototype.mulTo = barrettMulTo; +Barrett.prototype.sqrTo = barrettSqrTo; + +// (public) this^e % m (HAC 14.85) +function bnModPow(e,m) { + var i = e.bitLength(), k, r = nbv(1), z; + if(i <= 0) return r; + else if(i < 18) k = 1; + else if(i < 48) k = 3; + else if(i < 144) k = 4; + else if(i < 768) k = 5; + else k = 6; + if(i < 8) + z = new Classic(m); + else if(m.isEven()) + z = new Barrett(m); + else + z = new Montgomery(m); + + // precomputation + var g = [], n = 3, k1 = k-1, km = (1< 1) { + var g2 = nbi(); + z.sqrTo(g[1],g2); + while(n <= km) { + g[n] = nbi(); + z.mulTo(g2,g[n-2],g[n]); + n += 2; + } + } + + var j = e.t-1, w, is1 = true, r2 = nbi(), t; + i = nbits(e[j])-1; + while(j >= 0) { + if(i >= k1) w = (e[j]>>(i-k1))&km; + else { + w = (e[j]&((1<<(i+1))-1))<<(k1-i); + if(j > 0) w |= e[j-1]>>(this.DB+i-k1); + } + + n = k; + while((w&1) == 0) { w >>= 1; --n; } + if((i -= n) < 0) { i += this.DB; --j; } + if(is1) { // ret == 1, don't bother squaring or multiplying it + g[w].copyTo(r); + is1 = false; + } + else { + while(n > 1) { z.sqrTo(r,r2); z.sqrTo(r2,r); n -= 2; } + if(n > 0) z.sqrTo(r,r2); else { t = r; r = r2; r2 = t; } + z.mulTo(r2,g[w],r); + } + + while(j >= 0 && (e[j]&(1< 0) { + x.rShiftTo(g,x); + y.rShiftTo(g,y); + } + while(x.signum() > 0) { + if((i = x.getLowestSetBit()) > 0) x.rShiftTo(i,x); + if((i = y.getLowestSetBit()) > 0) y.rShiftTo(i,y); + if(x.compareTo(y) >= 0) { + x.subTo(y,x); + x.rShiftTo(1,x); + } + else { + y.subTo(x,y); + y.rShiftTo(1,y); + } + } + if(g > 0) y.lShiftTo(g,y); + return y; +} + +// (protected) this % n, n < 2^26 +function bnpModInt(n) { + if(n <= 0) return 0; + var d = this.DV%n, r = (this.s<0)?n-1:0; + if(this.t > 0) + if(d == 0) r = this[0]%n; + else for(var i = this.t-1; i >= 0; --i) r = (d*r+this[i])%n; + return r; +} + +// (public) 1/this % m (HAC 14.61) +function bnModInverse(m) { + var ac = m.isEven(); + if((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO; + var u = m.clone(), v = this.clone(); + var a = nbv(1), b = nbv(0), c = nbv(0), d = nbv(1); + while(u.signum() != 0) { + while(u.isEven()) { + u.rShiftTo(1,u); + if(ac) { + if(!a.isEven() || !b.isEven()) { a.addTo(this,a); b.subTo(m,b); } + a.rShiftTo(1,a); + } + else if(!b.isEven()) b.subTo(m,b); + b.rShiftTo(1,b); + } + while(v.isEven()) { + v.rShiftTo(1,v); + if(ac) { + if(!c.isEven() || !d.isEven()) { c.addTo(this,c); d.subTo(m,d); } + c.rShiftTo(1,c); + } + else if(!d.isEven()) d.subTo(m,d); + d.rShiftTo(1,d); + } + if(u.compareTo(v) >= 0) { + u.subTo(v,u); + if(ac) a.subTo(c,a); + b.subTo(d,b); + } + else { + v.subTo(u,v); + if(ac) c.subTo(a,c); + d.subTo(b,d); + } + } + if(v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; + if(d.compareTo(m) >= 0) return d.subtract(m); + if(d.signum() < 0) d.addTo(m,d); else return d; + if(d.signum() < 0) return d.add(m); else return d; +} + +var lowprimes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997]; +var lplim = (1<<26)/lowprimes[lowprimes.length-1]; + +// (public) test primality with certainty >= 1-.5^t +function bnIsProbablePrime(t) { + var i, x = this.abs(); + if(x.t == 1 && x[0] <= lowprimes[lowprimes.length-1]) { + for(i = 0; i < lowprimes.length; ++i) + if(x[0] == lowprimes[i]) return true; + return false; + } + if(x.isEven()) return false; + i = 1; + while(i < lowprimes.length) { + var m = lowprimes[i], j = i+1; + while(j < lowprimes.length && m < lplim) m *= lowprimes[j++]; + m = x.modInt(m); + while(i < j) if(m%lowprimes[i++] == 0) return false; + } + return x.millerRabin(t); +} + +// (protected) true if probably prime (HAC 4.24, Miller-Rabin) +function bnpMillerRabin(t) { + var n1 = this.subtract(BigInteger.ONE); + var k = n1.getLowestSetBit(); + if(k <= 0) return false; + var r = n1.shiftRight(k); + t = (t+1)>>1; + if(t > lowprimes.length) t = lowprimes.length; + var a = nbi(); + for(var i = 0; i < t; ++i) { + //Pick bases at random, instead of starting at 2 + a.fromInt(lowprimes[Math.floor(Math.random()*lowprimes.length)]); + var y = a.modPow(r,this); + if(y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) { + var j = 1; + while(j++ < k && y.compareTo(n1) != 0) { + y = y.modPowInt(2,this); + if(y.compareTo(BigInteger.ONE) == 0) return false; + } + if(y.compareTo(n1) != 0) return false; + } + } + return true; +} + +// protected +BigInteger.prototype.chunkSize = bnpChunkSize; +BigInteger.prototype.toRadix = bnpToRadix; +BigInteger.prototype.fromRadix = bnpFromRadix; +BigInteger.prototype.fromNumber = bnpFromNumber; +BigInteger.prototype.bitwiseTo = bnpBitwiseTo; +BigInteger.prototype.changeBit = bnpChangeBit; +BigInteger.prototype.addTo = bnpAddTo; +BigInteger.prototype.dMultiply = bnpDMultiply; +BigInteger.prototype.dAddOffset = bnpDAddOffset; +BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo; +BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo; +BigInteger.prototype.modInt = bnpModInt; +BigInteger.prototype.millerRabin = bnpMillerRabin; + +// public +BigInteger.prototype.clone = bnClone; +BigInteger.prototype.intValue = bnIntValue; +BigInteger.prototype.byteValue = bnByteValue; +BigInteger.prototype.shortValue = bnShortValue; +BigInteger.prototype.signum = bnSigNum; +BigInteger.prototype.toByteArray = bnToByteArray; +BigInteger.prototype.equals = bnEquals; +BigInteger.prototype.min = bnMin; +BigInteger.prototype.max = bnMax; +BigInteger.prototype.and = bnAnd; +BigInteger.prototype.or = bnOr; +BigInteger.prototype.xor = bnXor; +BigInteger.prototype.andNot = bnAndNot; +BigInteger.prototype.not = bnNot; +BigInteger.prototype.shiftLeft = bnShiftLeft; +BigInteger.prototype.shiftRight = bnShiftRight; +BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit; +BigInteger.prototype.bitCount = bnBitCount; +BigInteger.prototype.testBit = bnTestBit; +BigInteger.prototype.setBit = bnSetBit; +BigInteger.prototype.clearBit = bnClearBit; +BigInteger.prototype.flipBit = bnFlipBit; +BigInteger.prototype.add = bnAdd; +BigInteger.prototype.subtract = bnSubtract; +BigInteger.prototype.multiply = bnMultiply; +BigInteger.prototype.divide = bnDivide; +BigInteger.prototype.remainder = bnRemainder; +BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder; +BigInteger.prototype.modPow = bnModPow; +BigInteger.prototype.modInverse = bnModInverse; +BigInteger.prototype.pow = bnPow; +BigInteger.prototype.gcd = bnGCD; +BigInteger.prototype.isProbablePrime = bnIsProbablePrime; + +// JSBN-specific extension +BigInteger.prototype.square = bnSquare; + +// BigInteger interfaces not implemented in jsbn: + +// BigInteger(int signum, byte[] magnitude) +// double doubleValue() +// float floatValue() +// int hashCode() +// long longValue() +// static BigInteger valueOf(long val) + +// Version 1.1: support utf-8 encoding in pkcs1pad2 + +// convert a (hex) string to a bignum object + +function parseBigInt(str,r) { + return new BigInteger(str,r); +} + +// PKCS#1 (type 2, random) pad input string s to n bytes, and return a bigint +function pkcs1pad2(s,n) { + if(n < s.length + 11) { // TODO: fix for utf-8 + console.error("Message too long for RSA"); + return null; + } + var ba = []; + var i = s.length - 1; + while(i >= 0 && n > 0) { + var c = s.charCodeAt(i--); + if(c < 128) { // encode using utf-8 + ba[--n] = c; + } + else if((c > 127) && (c < 2048)) { + ba[--n] = (c & 63) | 128; + ba[--n] = (c >> 6) | 192; + } + else { + ba[--n] = (c & 63) | 128; + ba[--n] = ((c >> 6) & 63) | 128; + ba[--n] = (c >> 12) | 224; + } + } + ba[--n] = 0; + var rng = new SecureRandom(); + var x = []; + while(n > 2) { // random non-zero pad + x[0] = 0; + while(x[0] == 0) rng.nextBytes(x); + ba[--n] = x[0]; + } + ba[--n] = 2; + ba[--n] = 0; + return new BigInteger(ba); +} + +// "empty" RSA key constructor +class RSAKey { + constructor() { + this.n = null; + this.e = 0; + this.d = null; + this.p = null; + this.q = null; + this.dmp1 = null; + this.dmq1 = null; + this.coeff = null; + } +} + +// Set the public key fields N and e from hex strings +function RSASetPublic(N,E) { + if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + } + else + console.error("Invalid RSA public key"); +} + +// Perform raw public operation on "x": return x^e (mod n) +function RSADoPublic(x) { + return x.modPowInt(this.e, this.n); +} + +// Return the PKCS#1 RSA encryption of "text" as an even-length hex string +function RSAEncrypt(text) { + var m = pkcs1pad2(text,(this.n.bitLength()+7)>>3); + if(m == null) return null; + var c = this.doPublic(m); + if(c == null) return null; + var h = c.toString(16); + if((h.length & 1) == 0) return h; else return "0" + h; +} + +// Return the PKCS#1 RSA encryption of "text" as a Base64-encoded string +//function RSAEncryptB64(text) { +// var h = this.encrypt(text); +// if(h) return hex2b64(h); else return null; +//} + +// protected +RSAKey.prototype.doPublic = RSADoPublic; + +// public +RSAKey.prototype.setPublic = RSASetPublic; +RSAKey.prototype.encrypt = RSAEncrypt; +//RSAKey.prototype.encrypt_b64 = RSAEncryptB64; + +// Version 1.1: support utf-8 decoding in pkcs1unpad2 + +// Undo PKCS#1 (type 2, random) padding and, if valid, return the plaintext + +function pkcs1unpad2(d,n) { + var b = d.toByteArray(); + var i = 0; + while(i < b.length && b[i] == 0) ++i; + if(b.length-i != n-1 || b[i] != 2) + return null; + ++i; + while(b[i] != 0) + if(++i >= b.length) return null; + var ret = ""; + while(++i < b.length) { + var c = b[i] & 255; + if(c < 128) { // utf-8 decode + ret += String.fromCharCode(c); + } + else if((c > 191) && (c < 224)) { + ret += String.fromCharCode(((c & 31) << 6) | (b[i+1] & 63)); + ++i; + } + else { + ret += String.fromCharCode(((c & 15) << 12) | ((b[i+1] & 63) << 6) | (b[i+2] & 63)); + i += 2; + } + } + return ret; +} + +// Set the private key fields N, e, and d from hex strings +function RSASetPrivate(N,E,D) { + if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + this.d = parseBigInt(D,16); + } + else + console.error("Invalid RSA private key"); +} + +// Set the private key fields N, e, d and CRT params from hex strings +function RSASetPrivateEx(N,E,D,P,Q,DP,DQ,C) { + if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + this.d = parseBigInt(D,16); + this.p = parseBigInt(P,16); + this.q = parseBigInt(Q,16); + this.dmp1 = parseBigInt(DP,16); + this.dmq1 = parseBigInt(DQ,16); + this.coeff = parseBigInt(C,16); + } + else + console.error("Invalid RSA private key"); +} + +// Generate a new random private key B bits long, using public expt E +function RSAGenerate(B,E) { + var rng = new SecureRandom(); + var qs = B>>1; + this.e = parseInt(E,16); + var ee = new BigInteger(E,16); + for(;;) { + for(;;) { + this.p = new BigInteger(B-qs,1,rng); + if(this.p.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) == 0 && this.p.isProbablePrime(10)) break; + } + for(;;) { + this.q = new BigInteger(qs,1,rng); + if(this.q.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) == 0 && this.q.isProbablePrime(10)) break; + } + if(this.p.compareTo(this.q) <= 0) { + var t = this.p; + this.p = this.q; + this.q = t; + } + var p1 = this.p.subtract(BigInteger.ONE); + var q1 = this.q.subtract(BigInteger.ONE); + var phi = p1.multiply(q1); + if(phi.gcd(ee).compareTo(BigInteger.ONE) == 0) { + this.n = this.p.multiply(this.q); + this.d = ee.modInverse(phi); + this.dmp1 = this.d.mod(p1); + this.dmq1 = this.d.mod(q1); + this.coeff = this.q.modInverse(this.p); + break; + } + } +} + +// Perform raw private operation on "x": return x^d (mod n) +function RSADoPrivate(x) { + if(this.p == null || this.q == null) + return x.modPow(this.d, this.n); + + // TODO: re-calculate any missing CRT params + var xp = x.mod(this.p).modPow(this.dmp1, this.p); + var xq = x.mod(this.q).modPow(this.dmq1, this.q); + + while(xp.compareTo(xq) < 0) + xp = xp.add(this.p); + return xp.subtract(xq).multiply(this.coeff).mod(this.p).multiply(this.q).add(xq); +} + +// Return the PKCS#1 RSA decryption of "ctext". +// "ctext" is an even-length hex string and the output is a plain string. +function RSADecrypt(ctext) { + var c = parseBigInt(ctext, 16); + var m = this.doPrivate(c); + if(m == null) return null; + return pkcs1unpad2(m, (this.n.bitLength()+7)>>3); +} + +// Return the PKCS#1 RSA decryption of "ctext". +// "ctext" is a Base64-encoded string and the output is a plain string. +//function RSAB64Decrypt(ctext) { +// var h = b64tohex(ctext); +// if(h) return this.decrypt(h); else return null; +//} + +// protected +RSAKey.prototype.doPrivate = RSADoPrivate; + +// public +RSAKey.prototype.setPrivate = RSASetPrivate; +RSAKey.prototype.setPrivateEx = RSASetPrivateEx; +RSAKey.prototype.generate = RSAGenerate; +RSAKey.prototype.decrypt = RSADecrypt; +//RSAKey.prototype.b64_decrypt = RSAB64Decrypt; + +// Base64 JavaScript decoder +// Copyright (c) 2008-2013 Lapo Luchini + +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +/*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ + +const Base64 = {}; +let decoder; + +Base64.decode = function (a) { + var i; + if (decoder === undefined) { + var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", + ignore = "= \f\n\r\t\u00A0\u2028\u2029"; + decoder = []; + for (i = 0; i < 64; ++i) + decoder[b64.charAt(i)] = i; + for (i = 0; i < ignore.length; ++i) + decoder[ignore.charAt(i)] = -1; + } + var out = []; + var bits = 0, char_count = 0; + for (i = 0; i < a.length; ++i) { + var c = a.charAt(i); + if (c == '=') + break; + c = decoder[c]; + if (c == -1) + continue; + if (c === undefined) + throw 'Illegal character at offset ' + i; + bits |= c; + if (++char_count >= 4) { + out[out.length] = (bits >> 16); + out[out.length] = (bits >> 8) & 0xFF; + out[out.length] = bits & 0xFF; + bits = 0; + char_count = 0; + } else { + bits <<= 6; + } + } + switch (char_count) { + case 1: + throw "Base64 encoding incomplete: at least 2 bits missing"; + case 2: + out[out.length] = (bits >> 10); + break; + case 3: + out[out.length] = (bits >> 16); + out[out.length] = (bits >> 8) & 0xFF; + break; + } + return out; +}; + +Base64.re = /-----BEGIN [^-]+-----([A-Za-z0-9+\/=\s]+)-----END [^-]+-----|begin-base64[^\n]+\n([A-Za-z0-9+\/=\s]+)====/; +Base64.unarmor = function (a) { + var m = Base64.re.exec(a); + if (m) { + if (m[1]) + a = m[1]; + else if (m[2]) + a = m[2]; + else + throw "RegExp out of sync"; + } + return Base64.decode(a); +}; + +// Hex JavaScript decoder +// Copyright (c) 2008-2013 Lapo Luchini + +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +/*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ +const Hex = {}; +let decoder$1; + +Hex.decode = function(a) { + var i; + if (decoder$1 === undefined) { + var hex = "0123456789ABCDEF", + ignore = " \f\n\r\t\u00A0\u2028\u2029"; + decoder$1 = []; + for (i = 0; i < 16; ++i) + decoder$1[hex.charAt(i)] = i; + hex = hex.toLowerCase(); + for (i = 10; i < 16; ++i) + decoder$1[hex.charAt(i)] = i; + for (i = 0; i < ignore.length; ++i) + decoder$1[ignore.charAt(i)] = -1; + } + var out = [], + bits = 0, + char_count = 0; + for (i = 0; i < a.length; ++i) { + var c = a.charAt(i); + if (c == '=') + break; + c = decoder$1[c]; + if (c == -1) + continue; + if (c === undefined) + throw 'Illegal character at offset ' + i; + bits |= c; + if (++char_count >= 2) { + out[out.length] = bits; + bits = 0; + char_count = 0; + } else { + bits <<= 4; + } + } + if (char_count) + throw "Hex encoding incomplete: 4 bits missing"; + return out; +}; + +/*! asn1-1.0.2.js (c) 2013 Kenji Urushima | kjur.github.com/jsrsasign/license + */ + +const JSX = /*window.JSX || */{}; +JSX.env = JSX.env || {}; + +var L = JSX; +var OP = Object.prototype; +var FUNCTION_TOSTRING = '[object Function]'; +var ADD = ["toString", "valueOf"]; + +JSX.env.parseUA = function(agent) { + + var numberify = function(s) { + var c = 0; + return parseFloat(s.replace(/\./g, function() { + return (c++ == 1) ? '' : '.'; + })); + }, + + nav = navigator, + o = { + ie: 0, + opera: 0, + gecko: 0, + webkit: 0, + chrome: 0, + mobile: null, + air: 0, + ipad: 0, + iphone: 0, + ipod: 0, + ios: null, + android: 0, + webos: 0, + caja: nav && nav.cajaVersion, + secure: false, + os: null + + }, + + ua = agent || (navigator && navigator.userAgent), + loc = window && window.location, + href = loc && loc.href, + m; + + o.secure = href && (href.toLowerCase().indexOf("https") === 0); + + if (ua) { + + if ((/windows|win32/i).test(ua)) { + o.os = 'windows'; + } else if ((/macintosh/i).test(ua)) { + o.os = 'macintosh'; + } else if ((/rhino/i).test(ua)) { + o.os = 'rhino'; + } + if ((/KHTML/).test(ua)) { + o.webkit = 1; + } + m = ua.match(/AppleWebKit\/([^\s]*)/); + if (m && m[1]) { + o.webkit = numberify(m[1]); + if (/ Mobile\//.test(ua)) { + o.mobile = 'Apple'; // iPhone or iPod Touch + m = ua.match(/OS ([^\s]*)/); + if (m && m[1]) { + m = numberify(m[1].replace('_', '.')); + } + o.ios = m; + o.ipad = o.ipod = o.iphone = 0; + m = ua.match(/iPad|iPod|iPhone/); + if (m && m[0]) { + o[m[0].toLowerCase()] = o.ios; + } + } else { + m = ua.match(/NokiaN[^\/]*|Android \d\.\d|webOS\/\d\.\d/); + if (m) { + o.mobile = m[0]; + } + if (/webOS/.test(ua)) { + o.mobile = 'WebOS'; + m = ua.match(/webOS\/([^\s]*);/); + if (m && m[1]) { + o.webos = numberify(m[1]); + } + } + if (/ Android/.test(ua)) { + o.mobile = 'Android'; + m = ua.match(/Android ([^\s]*);/); + if (m && m[1]) { + o.android = numberify(m[1]); + } + } + } + m = ua.match(/Chrome\/([^\s]*)/); + if (m && m[1]) { + o.chrome = numberify(m[1]); // Chrome + } else { + m = ua.match(/AdobeAIR\/([^\s]*)/); + if (m) { + o.air = m[0]; // Adobe AIR 1.0 or better + } + } + } + if (!o.webkit) { + m = ua.match(/Opera[\s\/]([^\s]*)/); + if (m && m[1]) { + o.opera = numberify(m[1]); + m = ua.match(/Version\/([^\s]*)/); + if (m && m[1]) { + o.opera = numberify(m[1]); // opera 10+ + } + m = ua.match(/Opera Mini[^;]*/); + if (m) { + o.mobile = m[0]; // ex: Opera Mini/2.0.4509/1316 + } + } else { // not opera or webkit + m = ua.match(/MSIE\s([^;]*)/); + if (m && m[1]) { + o.ie = numberify(m[1]); + } else { // not opera, webkit, or ie + m = ua.match(/Gecko\/([^\s]*)/); + if (m) { + o.gecko = 1; // Gecko detected, look for revision + m = ua.match(/rv:([^\s\)]*)/); + if (m && m[1]) { + o.gecko = numberify(m[1]); + } + } + } + } + } + } + return o; +}; + +JSX.env.ua = JSX.env.parseUA(); + +JSX.isFunction = function(o) { + return (typeof o === 'function') || OP.toString.apply(o) === FUNCTION_TOSTRING; +}; + +JSX._IEEnumFix = (JSX.env.ua.ie) ? function(r, s) { + var i, fname, f; + for (i=0;iMIT License + */ + +/** + * kjur's class library name space + *

+ * This name space provides following name spaces: + *

    + *
  • {@link KJUR.asn1} - ASN.1 primitive hexadecimal encoder
  • + *
  • {@link KJUR.asn1.x509} - ASN.1 structure for X.509 certificate and CRL
  • + *
  • {@link KJUR.crypto} - Java Cryptographic Extension(JCE) style MessageDigest/Signature + * class and utilities
  • + *
+ *

+ * NOTE: Please ignore method summary and document of this namespace. This caused by a bug of jsdoc2. + * @name KJUR + * @namespace kjur's class library name space + */ +// if (typeof KJUR == "undefined" || !KJUR) +const KJUR = {}; + +/** + * kjur's ASN.1 class library name space + *

+ * This is ITU-T X.690 ASN.1 DER encoder class library and + * class structure and methods is very similar to + * org.bouncycastle.asn1 package of + * well known BouncyCaslte Cryptography Library. + * + *

PROVIDING ASN.1 PRIMITIVES

+ * Here are ASN.1 DER primitive classes. + *
    + *
  • {@link KJUR.asn1.DERBoolean}
  • + *
  • {@link KJUR.asn1.DERInteger}
  • + *
  • {@link KJUR.asn1.DERBitString}
  • + *
  • {@link KJUR.asn1.DEROctetString}
  • + *
  • {@link KJUR.asn1.DERNull}
  • + *
  • {@link KJUR.asn1.DERObjectIdentifier}
  • + *
  • {@link KJUR.asn1.DERUTF8String}
  • + *
  • {@link KJUR.asn1.DERNumericString}
  • + *
  • {@link KJUR.asn1.DERPrintableString}
  • + *
  • {@link KJUR.asn1.DERTeletexString}
  • + *
  • {@link KJUR.asn1.DERIA5String}
  • + *
  • {@link KJUR.asn1.DERUTCTime}
  • + *
  • {@link KJUR.asn1.DERGeneralizedTime}
  • + *
  • {@link KJUR.asn1.DERSequence}
  • + *
  • {@link KJUR.asn1.DERSet}
  • + *
+ * + *

OTHER ASN.1 CLASSES

+ *
    + *
  • {@link KJUR.asn1.ASN1Object}
  • + *
  • {@link KJUR.asn1.DERAbstractString}
  • + *
  • {@link KJUR.asn1.DERAbstractTime}
  • + *
  • {@link KJUR.asn1.DERAbstractStructured}
  • + *
  • {@link KJUR.asn1.DERTaggedObject}
  • + *
+ *

+ * NOTE: Please ignore method summary and document of this namespace. This caused by a bug of jsdoc2. + * @name KJUR.asn1 + * @namespace + */ +if (typeof KJUR.asn1 == "undefined" || !KJUR.asn1) KJUR.asn1 = {}; + +/** + * ASN1 utilities class + * @name KJUR.asn1.ASN1Util + * @classs ASN1 utilities class + * @since asn1 1.0.2 + */ +KJUR.asn1.ASN1Util = new function() { + this.integerToByteHex = function(i) { + var h = i.toString(16); + if ((h.length % 2) == 1) h = '0' + h; + return h; + }; + this.bigIntToMinTwosComplementsHex = function(bigIntegerValue) { + var h = bigIntegerValue.toString(16); + if (h.substr(0, 1) != '-') { + if (h.length % 2 == 1) { + h = '0' + h; + } else { + if (! h.match(/^[0-7]/)) { + h = '00' + h; + } + } + } else { + var hPos = h.substr(1); + var xorLen = hPos.length; + if (xorLen % 2 == 1) { + xorLen += 1; + } else { + if (! h.match(/^[0-7]/)) { + xorLen += 2; + } + } + var hMask = ''; + for (var i = 0; i < xorLen; i++) { + hMask += 'f'; + } + var biMask = new BigInteger(hMask, 16); + var biNeg = biMask.xor(bigIntegerValue).add(BigInteger.ONE); + h = biNeg.toString(16).replace(/^-/, ''); + } + return h; + }; + /** + * get PEM string from hexadecimal data and header string + * @name getPEMStringFromHex + * @memberOf KJUR.asn1.ASN1Util + * @function + * @param {String} dataHex hexadecimal string of PEM body + * @param {String} pemHeader PEM header string (ex. 'RSA PRIVATE KEY') + * @return {String} PEM formatted string of input data + * @description + * @example + * var pem = KJUR.asn1.ASN1Util.getPEMStringFromHex('616161', 'RSA PRIVATE KEY'); + * // value of pem will be: + * -----BEGIN PRIVATE KEY----- + * YWFh + * -----END PRIVATE KEY----- + */ + this.getPEMStringFromHex = function(dataHex, pemHeader) { + var dataWA = CryptoJS.enc.Hex.parse(dataHex); + var dataB64 = CryptoJS.enc.Base64.stringify(dataWA); + var pemBody = dataB64.replace(/(.{64})/g, "$1\r\n"); + pemBody = pemBody.replace(/\r\n$/, ''); + return "-----BEGIN " + pemHeader + "-----\r\n" + + pemBody + + "\r\n-----END " + pemHeader + "-----\r\n"; + }; +}; + +// ******************************************************************** +// Abstract ASN.1 Classes +// ******************************************************************** + +// ******************************************************************** + +/** + * base class for ASN.1 DER encoder object + * @name KJUR.asn1.ASN1Object + * @class base class for ASN.1 DER encoder object + * @property {Boolean} isModified flag whether internal data was changed + * @property {String} hTLV hexadecimal string of ASN.1 TLV + * @property {String} hT hexadecimal string of ASN.1 TLV tag(T) + * @property {String} hL hexadecimal string of ASN.1 TLV length(L) + * @property {String} hV hexadecimal string of ASN.1 TLV value(V) + * @description + */ +KJUR.asn1.ASN1Object = function() { + var hV = ''; + + /** + * get hexadecimal ASN.1 TLV length(L) bytes from TLV value(V) + * @name getLengthHexFromValue + * @memberOf KJUR.asn1.ASN1Object + * @function + * @return {String} hexadecimal string of ASN.1 TLV length(L) + */ + this.getLengthHexFromValue = function() { + if (typeof this.hV == "undefined" || this.hV == null) { + throw "this.hV is null or undefined."; + } + if (this.hV.length % 2 == 1) { + throw "value hex must be even length: n=" + hV.length + ",v=" + this.hV; + } + var n = this.hV.length / 2; + var hN = n.toString(16); + if (hN.length % 2 == 1) { + hN = "0" + hN; + } + if (n < 128) { + return hN; + } else { + var hNlen = hN.length / 2; + if (hNlen > 15) { + throw "ASN.1 length too long to represent by 8x: n = " + n.toString(16); + } + var head = 128 + hNlen; + return head.toString(16) + hN; + } + }; + + /** + * get hexadecimal string of ASN.1 TLV bytes + * @name getEncodedHex + * @memberOf KJUR.asn1.ASN1Object + * @function + * @return {String} hexadecimal string of ASN.1 TLV + */ + this.getEncodedHex = function() { + if (this.hTLV == null || this.isModified) { + this.hV = this.getFreshValueHex(); + this.hL = this.getLengthHexFromValue(); + this.hTLV = this.hT + this.hL + this.hV; + this.isModified = false; + //console.error("first time: " + this.hTLV); + } + return this.hTLV; + }; + + /** + * get hexadecimal string of ASN.1 TLV value(V) bytes + * @name getValueHex + * @memberOf KJUR.asn1.ASN1Object + * @function + * @return {String} hexadecimal string of ASN.1 TLV value(V) bytes + */ + this.getValueHex = function() { + this.getEncodedHex(); + return this.hV; + }; + + this.getFreshValueHex = function() { + return ''; + }; +}; + +// == BEGIN DERAbstractString ================================================ +/** + * base class for ASN.1 DER string classes + * @name KJUR.asn1.DERAbstractString + * @class base class for ASN.1 DER string classes + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @property {String} s internal string of value + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • str - specify initial ASN.1 value(V) by a string
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERAbstractString = function(params) { + KJUR.asn1.DERAbstractString.superclass.constructor.call(this); + this.getString = function() { + return this.s; + }; + + /** + * set value by a string + * @name setString + * @memberOf KJUR.asn1.DERAbstractString + * @function + * @param {String} newS value by a string to set + */ + this.setString = function(newS) { + this.hTLV = null; + this.isModified = true; + this.s = newS; + this.hV = stohex(this.s); + }; + + /** + * set value by a hexadecimal string + * @name setStringHex + * @memberOf KJUR.asn1.DERAbstractString + * @function + * @param {String} newHexString value by a hexadecimal string to set + */ + this.setStringHex = function(newHexString) { + this.hTLV = null; + this.isModified = true; + this.s = null; + this.hV = newHexString; + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['str'] != "undefined") { + this.setString(params['str']); + } else if (typeof params['hex'] != "undefined") { + this.setStringHex(params['hex']); + } + } +}; +JSX.extend(KJUR.asn1.DERAbstractString, KJUR.asn1.ASN1Object); +// == END DERAbstractString ================================================ + +// == BEGIN DERAbstractTime ================================================== +/** + * base class for ASN.1 DER Generalized/UTCTime class + * @name KJUR.asn1.DERAbstractTime + * @class base class for ASN.1 DER Generalized/UTCTime class + * @param {Array} params associative array of parameters (ex. {'str': '130430235959Z'}) + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ +KJUR.asn1.DERAbstractTime = function(params) { + KJUR.asn1.DERAbstractTime.superclass.constructor.call(this); + this.localDateToUTC = function(d) { + utc = d.getTime() + (d.getTimezoneOffset() * 60000); + var utcDate = new Date(utc); + return utcDate; + }; + + this.formatDate = function(dateObject, type) { + var pad = this.zeroPadding; + var d = this.localDateToUTC(dateObject); + var year = String(d.getFullYear()); + if (type == 'utc') year = year.substr(2, 2); + var month = pad(String(d.getMonth() + 1), 2); + var day = pad(String(d.getDate()), 2); + var hour = pad(String(d.getHours()), 2); + var min = pad(String(d.getMinutes()), 2); + var sec = pad(String(d.getSeconds()), 2); + return year + month + day + hour + min + sec + 'Z'; + }; + + this.zeroPadding = function(s, len) { + if (s.length >= len) return s; + return new Array(len - s.length + 1).join('0') + s; + }; + + // --- PUBLIC METHODS -------------------- + /** + * get string value of this string object + * @name getString + * @memberOf KJUR.asn1.DERAbstractTime + * @function + * @return {String} string value of this time object + */ + this.getString = function() { + return this.s; + }; + + /** + * set value by a string + * @name setString + * @memberOf KJUR.asn1.DERAbstractTime + * @function + * @param {String} newS value by a string to set such like "130430235959Z" + */ + this.setString = function(newS) { + this.hTLV = null; + this.isModified = true; + this.s = newS; + this.hV = stohex(this.s); + }; + + /** + * set value by a Date object + * @name setByDateValue + * @memberOf KJUR.asn1.DERAbstractTime + * @function + * @param {Integer} year year of date (ex. 2013) + * @param {Integer} month month of date between 1 and 12 (ex. 12) + * @param {Integer} day day of month + * @param {Integer} hour hours of date + * @param {Integer} min minutes of date + * @param {Integer} sec seconds of date + */ + this.setByDateValue = function(year, month, day, hour, min, sec) { + var dateObject = new Date(Date.UTC(year, month - 1, day, hour, min, sec, 0)); + this.setByDate(dateObject); + }; + + this.getFreshValueHex = function() { + return this.hV; + }; +}; +JSX.extend(KJUR.asn1.DERAbstractTime, KJUR.asn1.ASN1Object); +// == END DERAbstractTime ================================================== + +// == BEGIN DERAbstractStructured ============================================ +/** + * base class for ASN.1 DER structured class + * @name KJUR.asn1.DERAbstractStructured + * @class base class for ASN.1 DER structured class + * @property {Array} asn1Array internal array of ASN1Object + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ +KJUR.asn1.DERAbstractStructured = function(params) { + KJUR.asn1.DERAbstractString.superclass.constructor.call(this); + this.setByASN1ObjectArray = function(asn1ObjectArray) { + this.hTLV = null; + this.isModified = true; + this.asn1Array = asn1ObjectArray; + }; + + /** + * append an ASN1Object to internal array + * @name appendASN1Object + * @memberOf KJUR.asn1.DERAbstractStructured + * @function + * @param {ASN1Object} asn1Object to add + */ + this.appendASN1Object = function(asn1Object) { + this.hTLV = null; + this.isModified = true; + this.asn1Array.push(asn1Object); + }; + + this.asn1Array = new Array(); + if (typeof params != "undefined") { + if (typeof params['array'] != "undefined") { + this.asn1Array = params['array']; + } + } +}; +JSX.extend(KJUR.asn1.DERAbstractStructured, KJUR.asn1.ASN1Object); + + +// ******************************************************************** +// ASN.1 Object Classes +// ******************************************************************** + +// ******************************************************************** +/** + * class for ASN.1 DER Boolean + * @name KJUR.asn1.DERBoolean + * @class class for ASN.1 DER Boolean + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ +KJUR.asn1.DERBoolean = function() { + KJUR.asn1.DERBoolean.superclass.constructor.call(this); + this.hT = "01"; + this.hTLV = "0101ff"; +}; +JSX.extend(KJUR.asn1.DERBoolean, KJUR.asn1.ASN1Object); + +// ******************************************************************** +/** + * class for ASN.1 DER Integer + * @name KJUR.asn1.DERInteger + * @class class for ASN.1 DER Integer + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • int - specify initial ASN.1 value(V) by integer value
  • + *
  • bigint - specify initial ASN.1 value(V) by BigInteger object
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERInteger = function(params) { + KJUR.asn1.DERInteger.superclass.constructor.call(this); + this.hT = "02"; + + /** + * set value by Tom Wu's BigInteger object + * @name setByBigInteger + * @memberOf KJUR.asn1.DERInteger + * @function + * @param {BigInteger} bigIntegerValue to set + */ + this.setByBigInteger = function(bigIntegerValue) { + this.hTLV = null; + this.isModified = true; + this.hV = KJUR.asn1.ASN1Util.bigIntToMinTwosComplementsHex(bigIntegerValue); + }; + + /** + * set value by integer value + * @name setByInteger + * @memberOf KJUR.asn1.DERInteger + * @function + * @param {Integer} integer value to set + */ + this.setByInteger = function(intValue) { + var bi = new BigInteger(String(intValue), 10); + this.setByBigInteger(bi); + }; + + /** + * set value by integer value + * @name setValueHex + * @memberOf KJUR.asn1.DERInteger + * @function + * @param {String} hexadecimal string of integer value + * @description + *
+ * NOTE: Value shall be represented by minimum octet length of + * two's complement representation. + */ + this.setValueHex = function(newHexString) { + this.hV = newHexString; + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['bigint'] != "undefined") { + this.setByBigInteger(params['bigint']); + } else if (typeof params['int'] != "undefined") { + this.setByInteger(params['int']); + } else if (typeof params['hex'] != "undefined") { + this.setValueHex(params['hex']); + } + } +}; +JSX.extend(KJUR.asn1.DERInteger, KJUR.asn1.ASN1Object); + +// ******************************************************************** +/** + * class for ASN.1 DER encoded BitString primitive + * @name KJUR.asn1.DERBitString + * @class class for ASN.1 DER encoded BitString primitive + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • bin - specify binary string (ex. '10111')
  • + *
  • array - specify array of boolean (ex. [true,false,true,true])
  • + *
  • hex - specify hexadecimal string of ASN.1 value(V) including unused bits
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERBitString = function(params) { + KJUR.asn1.DERBitString.superclass.constructor.call(this); + this.hT = "03"; + + /** + * set ASN.1 value(V) by a hexadecimal string including unused bits + * @name setHexValueIncludingUnusedBits + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {String} newHexStringIncludingUnusedBits + */ + this.setHexValueIncludingUnusedBits = function(newHexStringIncludingUnusedBits) { + this.hTLV = null; + this.isModified = true; + this.hV = newHexStringIncludingUnusedBits; + }; + + /** + * set ASN.1 value(V) by unused bit and hexadecimal string of value + * @name setUnusedBitsAndHexValue + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {Integer} unusedBits + * @param {String} hValue + */ + this.setUnusedBitsAndHexValue = function(unusedBits, hValue) { + if (unusedBits < 0 || 7 < unusedBits) { + throw "unused bits shall be from 0 to 7: u = " + unusedBits; + } + var hUnusedBits = "0" + unusedBits; + this.hTLV = null; + this.isModified = true; + this.hV = hUnusedBits + hValue; + }; + + /** + * set ASN.1 DER BitString by binary string + * @name setByBinaryString + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {String} binaryString binary value string (i.e. '10111') + * @description + * Its unused bits will be calculated automatically by length of + * 'binaryValue'.
+ * NOTE: Trailing zeros '0' will be ignored. + */ + this.setByBinaryString = function(binaryString) { + binaryString = binaryString.replace(/0+$/, ''); + var unusedBits = 8 - binaryString.length % 8; + if (unusedBits == 8) unusedBits = 0; + for (var i = 0; i <= unusedBits; i++) { + binaryString += '0'; + } + var h = ''; + for (var i = 0; i < binaryString.length - 1; i += 8) { + var b = binaryString.substr(i, 8); + var x = parseInt(b, 2).toString(16); + if (x.length == 1) x = '0' + x; + h += x; + } + this.hTLV = null; + this.isModified = true; + this.hV = '0' + unusedBits + h; + }; + + /** + * set ASN.1 TLV value(V) by an array of boolean + * @name setByBooleanArray + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {array} booleanArray array of boolean (ex. [true, false, true]) + * @description + * NOTE: Trailing falses will be ignored. + */ + this.setByBooleanArray = function(booleanArray) { + var s = ''; + for (var i = 0; i < booleanArray.length; i++) { + if (booleanArray[i] == true) { + s += '1'; + } else { + s += '0'; + } + } + this.setByBinaryString(s); + }; + + /** + * generate an array of false with specified length + * @name newFalseArray + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {Integer} nLength length of array to generate + * @return {array} array of boolean faluse + * @description + * This static method may be useful to initialize boolean array. + */ + this.newFalseArray = function(nLength) { + var a = new Array(nLength); + for (var i = 0; i < nLength; i++) { + a[i] = false; + } + return a; + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['hex'] != "undefined") { + this.setHexValueIncludingUnusedBits(params['hex']); + } else if (typeof params['bin'] != "undefined") { + this.setByBinaryString(params['bin']); + } else if (typeof params['array'] != "undefined") { + this.setByBooleanArray(params['array']); + } + } +}; +JSX.extend(KJUR.asn1.DERBitString, KJUR.asn1.ASN1Object); + +// ******************************************************************** +/** + * class for ASN.1 DER OctetString + * @name KJUR.asn1.DEROctetString + * @class class for ASN.1 DER OctetString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DEROctetString = function(params) { + KJUR.asn1.DEROctetString.superclass.constructor.call(this, params); + this.hT = "04"; +}; +JSX.extend(KJUR.asn1.DEROctetString, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER Null + * @name KJUR.asn1.DERNull + * @class class for ASN.1 DER Null + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ +KJUR.asn1.DERNull = function() { + KJUR.asn1.DERNull.superclass.constructor.call(this); + this.hT = "05"; + this.hTLV = "0500"; +}; +JSX.extend(KJUR.asn1.DERNull, KJUR.asn1.ASN1Object); + +// ******************************************************************** +/** + * class for ASN.1 DER ObjectIdentifier + * @name KJUR.asn1.DERObjectIdentifier + * @class class for ASN.1 DER ObjectIdentifier + * @param {Array} params associative array of parameters (ex. {'oid': '2.5.4.5'}) + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • oid - specify initial ASN.1 value(V) by a oid string (ex. 2.5.4.13)
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERObjectIdentifier = function(params) { + var itox = function(i) { + var h = i.toString(16); + if (h.length == 1) h = '0' + h; + return h; + }; + var roidtox = function(roid) { + var h = ''; + var bi = new BigInteger(roid, 10); + var b = bi.toString(2); + var padLen = 7 - b.length % 7; + if (padLen == 7) padLen = 0; + var bPad = ''; + for (var i = 0; i < padLen; i++) bPad += '0'; + b = bPad + b; + for (var i = 0; i < b.length - 1; i += 7) { + var b8 = b.substr(i, 7); + if (i != b.length - 7) b8 = '1' + b8; + h += itox(parseInt(b8, 2)); + } + return h; + }; + + KJUR.asn1.DERObjectIdentifier.superclass.constructor.call(this); + this.hT = "06"; + + /** + * set value by a hexadecimal string + * @name setValueHex + * @memberOf KJUR.asn1.DERObjectIdentifier + * @function + * @param {String} newHexString hexadecimal value of OID bytes + */ + this.setValueHex = function(newHexString) { + this.hTLV = null; + this.isModified = true; + this.s = null; + this.hV = newHexString; + }; + + /** + * set value by a OID string + * @name setValueOidString + * @memberOf KJUR.asn1.DERObjectIdentifier + * @function + * @param {String} oidString OID string (ex. 2.5.4.13) + */ + this.setValueOidString = function(oidString) { + if (! oidString.match(/^[0-9.]+$/)) { + throw "malformed oid string: " + oidString; + } + var h = ''; + var a = oidString.split('.'); + var i0 = parseInt(a[0]) * 40 + parseInt(a[1]); + h += itox(i0); + a.splice(0, 2); + for (var i = 0; i < a.length; i++) { + h += roidtox(a[i]); + } + this.hTLV = null; + this.isModified = true; + this.s = null; + this.hV = h; + }; + + /** + * set value by a OID name + * @name setValueName + * @memberOf KJUR.asn1.DERObjectIdentifier + * @function + * @param {String} oidName OID name (ex. 'serverAuth') + * @since 1.0.1 + * @description + * OID name shall be defined in 'KJUR.asn1.x509.OID.name2oidList'. + * Otherwise raise error. + */ + this.setValueName = function(oidName) { + if (typeof KJUR.asn1.x509.OID.name2oidList[oidName] != "undefined") { + var oid = KJUR.asn1.x509.OID.name2oidList[oidName]; + this.setValueOidString(oid); + } else { + throw "DERObjectIdentifier oidName undefined: " + oidName; + } + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['oid'] != "undefined") { + this.setValueOidString(params['oid']); + } else if (typeof params['hex'] != "undefined") { + this.setValueHex(params['hex']); + } else if (typeof params['name'] != "undefined") { + this.setValueName(params['name']); + } + } +}; +JSX.extend(KJUR.asn1.DERObjectIdentifier, KJUR.asn1.ASN1Object); + +// ******************************************************************** +/** + * class for ASN.1 DER UTF8String + * @name KJUR.asn1.DERUTF8String + * @class class for ASN.1 DER UTF8String + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DERUTF8String = function(params) { + KJUR.asn1.DERUTF8String.superclass.constructor.call(this, params); + this.hT = "0c"; +}; +JSX.extend(KJUR.asn1.DERUTF8String, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER NumericString + * @name KJUR.asn1.DERNumericString + * @class class for ASN.1 DER NumericString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DERNumericString = function(params) { + KJUR.asn1.DERNumericString.superclass.constructor.call(this, params); + this.hT = "12"; +}; +JSX.extend(KJUR.asn1.DERNumericString, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER PrintableString + * @name KJUR.asn1.DERPrintableString + * @class class for ASN.1 DER PrintableString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DERPrintableString = function(params) { + KJUR.asn1.DERPrintableString.superclass.constructor.call(this, params); + this.hT = "13"; +}; +JSX.extend(KJUR.asn1.DERPrintableString, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER TeletexString + * @name KJUR.asn1.DERTeletexString + * @class class for ASN.1 DER TeletexString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DERTeletexString = function(params) { + KJUR.asn1.DERTeletexString.superclass.constructor.call(this, params); + this.hT = "14"; +}; +JSX.extend(KJUR.asn1.DERTeletexString, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER IA5String + * @name KJUR.asn1.DERIA5String + * @class class for ASN.1 DER IA5String + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DERIA5String = function(params) { + KJUR.asn1.DERIA5String.superclass.constructor.call(this, params); + this.hT = "16"; +}; +JSX.extend(KJUR.asn1.DERIA5String, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER UTCTime + * @name KJUR.asn1.DERUTCTime + * @class class for ASN.1 DER UTCTime + * @param {Array} params associative array of parameters (ex. {'str': '130430235959Z'}) + * @extends KJUR.asn1.DERAbstractTime + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • str - specify initial ASN.1 value(V) by a string (ex.'130430235959Z')
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
  • date - specify Date object.
  • + *
+ * NOTE: 'params' can be omitted. + *

EXAMPLES

+ * @example + * var d1 = new KJUR.asn1.DERUTCTime(); + * d1.setString('130430125959Z'); + * + * var d2 = new KJUR.asn1.DERUTCTime({'str': '130430125959Z'}); + * + * var d3 = new KJUR.asn1.DERUTCTime({'date': new Date(Date.UTC(2015, 0, 31, 0, 0, 0, 0))}); + */ +KJUR.asn1.DERUTCTime = function(params) { + KJUR.asn1.DERUTCTime.superclass.constructor.call(this, params); + this.hT = "17"; + + /** + * set value by a Date object + * @name setByDate + * @memberOf KJUR.asn1.DERUTCTime + * @function + * @param {Date} dateObject Date object to set ASN.1 value(V) + */ + this.setByDate = function(dateObject) { + this.hTLV = null; + this.isModified = true; + this.date = dateObject; + this.s = this.formatDate(this.date, 'utc'); + this.hV = stohex(this.s); + }; + + if (typeof params != "undefined") { + if (typeof params['str'] != "undefined") { + this.setString(params['str']); + } else if (typeof params['hex'] != "undefined") { + this.setStringHex(params['hex']); + } else if (typeof params['date'] != "undefined") { + this.setByDate(params['date']); + } + } +}; +JSX.extend(KJUR.asn1.DERUTCTime, KJUR.asn1.DERAbstractTime); + +// ******************************************************************** +/** + * class for ASN.1 DER GeneralizedTime + * @name KJUR.asn1.DERGeneralizedTime + * @class class for ASN.1 DER GeneralizedTime + * @param {Array} params associative array of parameters (ex. {'str': '20130430235959Z'}) + * @extends KJUR.asn1.DERAbstractTime + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • str - specify initial ASN.1 value(V) by a string (ex.'20130430235959Z')
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
  • date - specify Date object.
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERGeneralizedTime = function(params) { + KJUR.asn1.DERGeneralizedTime.superclass.constructor.call(this, params); + this.hT = "18"; + + /** + * set value by a Date object + * @name setByDate + * @memberOf KJUR.asn1.DERGeneralizedTime + * @function + * @param {Date} dateObject Date object to set ASN.1 value(V) + * @example + * When you specify UTC time, use 'Date.UTC' method like this:
+ * var o = new DERUTCTime(); + * var date = new Date(Date.UTC(2015, 0, 31, 23, 59, 59, 0)); #2015JAN31 23:59:59 + * o.setByDate(date); + */ + this.setByDate = function(dateObject) { + this.hTLV = null; + this.isModified = true; + this.date = dateObject; + this.s = this.formatDate(this.date, 'gen'); + this.hV = stohex(this.s); + }; + + if (typeof params != "undefined") { + if (typeof params['str'] != "undefined") { + this.setString(params['str']); + } else if (typeof params['hex'] != "undefined") { + this.setStringHex(params['hex']); + } else if (typeof params['date'] != "undefined") { + this.setByDate(params['date']); + } + } +}; +JSX.extend(KJUR.asn1.DERGeneralizedTime, KJUR.asn1.DERAbstractTime); + +// ******************************************************************** +/** + * class for ASN.1 DER Sequence + * @name KJUR.asn1.DERSequence + * @class class for ASN.1 DER Sequence + * @extends KJUR.asn1.DERAbstractStructured + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • array - specify array of ASN1Object to set elements of content
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERSequence = function(params) { + KJUR.asn1.DERSequence.superclass.constructor.call(this, params); + this.hT = "30"; + this.getFreshValueHex = function() { + var h = ''; + for (var i = 0; i < this.asn1Array.length; i++) { + var asn1Obj = this.asn1Array[i]; + h += asn1Obj.getEncodedHex(); + } + this.hV = h; + return this.hV; + }; +}; +JSX.extend(KJUR.asn1.DERSequence, KJUR.asn1.DERAbstractStructured); + +// ******************************************************************** +/** + * class for ASN.1 DER Set + * @name KJUR.asn1.DERSet + * @class class for ASN.1 DER Set + * @extends KJUR.asn1.DERAbstractStructured + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • array - specify array of ASN1Object to set elements of content
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERSet = function(params) { + KJUR.asn1.DERSet.superclass.constructor.call(this, params); + this.hT = "31"; + this.getFreshValueHex = function() { + var a = new Array(); + for (var i = 0; i < this.asn1Array.length; i++) { + var asn1Obj = this.asn1Array[i]; + a.push(asn1Obj.getEncodedHex()); + } + a.sort(); + this.hV = a.join(''); + return this.hV; + }; +}; +JSX.extend(KJUR.asn1.DERSet, KJUR.asn1.DERAbstractStructured); + +// ******************************************************************** +/** + * class for ASN.1 DER TaggedObject + * @name KJUR.asn1.DERTaggedObject + * @class class for ASN.1 DER TaggedObject + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * Parameter 'tagNoNex' is ASN.1 tag(T) value for this object. + * For example, if you find '[1]' tag in a ASN.1 dump, + * 'tagNoHex' will be 'a1'. + *
+ * As for optional argument 'params' for constructor, you can specify *ANY* of + * following properties: + *
    + *
  • explicit - specify true if this is explicit tag otherwise false + * (default is 'true').
  • + *
  • tag - specify tag (default is 'a0' which means [0])
  • + *
  • obj - specify ASN1Object which is tagged
  • + *
+ * @example + * d1 = new KJUR.asn1.DERUTF8String({'str':'a'}); + * d2 = new KJUR.asn1.DERTaggedObject({'obj': d1}); + * hex = d2.getEncodedHex(); + */ +KJUR.asn1.DERTaggedObject = function(params) { + KJUR.asn1.DERTaggedObject.superclass.constructor.call(this); + this.hT = "a0"; + this.hV = ''; + this.isExplicit = true; + this.asn1Object = null; + + /** + * set value by an ASN1Object + * @name setString + * @memberOf KJUR.asn1.DERTaggedObject + * @function + * @param {Boolean} isExplicitFlag flag for explicit/implicit tag + * @param {Integer} tagNoHex hexadecimal string of ASN.1 tag + * @param {ASN1Object} asn1Object ASN.1 to encapsulate + */ + this.setASN1Object = function(isExplicitFlag, tagNoHex, asn1Object) { + this.hT = tagNoHex; + this.isExplicit = isExplicitFlag; + this.asn1Object = asn1Object; + if (this.isExplicit) { + this.hV = this.asn1Object.getEncodedHex(); + this.hTLV = null; + this.isModified = true; + } else { + this.hV = null; + this.hTLV = asn1Object.getEncodedHex(); + this.hTLV = this.hTLV.replace(/^../, tagNoHex); + this.isModified = false; + } + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['tag'] != "undefined") { + this.hT = params['tag']; + } + if (typeof params['explicit'] != "undefined") { + this.isExplicit = params['explicit']; + } + if (typeof params['obj'] != "undefined") { + this.asn1Object = params['obj']; + this.setASN1Object(this.isExplicit, this.hT, this.asn1Object); + } + } +}; +JSX.extend(KJUR.asn1.DERTaggedObject, KJUR.asn1.ASN1Object); + +var b64map="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +var b64pad="="; + +function hex2b64(h) { + var i; + var c; + var ret = ""; + for(i = 0; i+3 <= h.length; i+=3) { + c = parseInt(h.substring(i,i+3),16); + ret += b64map.charAt(c >> 6) + b64map.charAt(c & 63); + } + if(i+1 == h.length) { + c = parseInt(h.substring(i,i+1),16); + ret += b64map.charAt(c << 2); + } + else if(i+2 == h.length) { + c = parseInt(h.substring(i,i+2),16); + ret += b64map.charAt(c >> 2) + b64map.charAt((c & 3) << 4); + } + while((ret.length & 3) > 0) ret += b64pad; + return ret; +} + +// convert a base64 string to hex +function b64tohex(s) { + var ret = ""; + var i; + var k = 0; // b64 state, 0-3 + var slop; + for(i = 0; i < s.length; ++i) { + if(s.charAt(i) == b64pad) break; + let v = b64map.indexOf(s.charAt(i)); + if(v < 0) continue; + if(k == 0) { + ret += int2char(v >> 2); + slop = v & 3; + k = 1; + } + else if(k == 1) { + ret += int2char((slop << 2) | (v >> 4)); + slop = v & 0xf; + k = 2; + } + else if(k == 2) { + ret += int2char(slop); + ret += int2char(v >> 2); + slop = v & 3; + k = 3; + } + else { + ret += int2char((slop << 2) | (v >> 4)); + ret += int2char(v & 0xf); + k = 0; + } + } + if(k == 1) + ret += int2char(slop << 2); + return ret; +} + +// convert a base64 string to a byte/number array + +/** + * Retrieve the hexadecimal value (as a string) of the current ASN.1 element + * @returns {string} + * @public + */ +ASN1.prototype.getHexStringValue = function () { + var hexString = this.toHexString(); + var offset = this.header * 2; + var length = this.length * 2; + return hexString.substr(offset, length); +}; + +/** + * Method to parse a pem encoded string containing both a public or private key. + * The method will translate the pem encoded string in a der encoded string and + * will parse private key and public key parameters. This method accepts public key + * in the rsaencryption pkcs #1 format (oid: 1.2.840.113549.1.1.1). + * + * @todo Check how many rsa formats use the same format of pkcs #1. + * + * The format is defined as: + * PublicKeyInfo ::= SEQUENCE { + * algorithm AlgorithmIdentifier, + * PublicKey BIT STRING + * } + * Where AlgorithmIdentifier is: + * AlgorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, the OID of the enc algorithm + * parameters ANY DEFINED BY algorithm OPTIONAL (NULL for PKCS #1) + * } + * and PublicKey is a SEQUENCE encapsulated in a BIT STRING + * RSAPublicKey ::= SEQUENCE { + * modulus INTEGER, -- n + * publicExponent INTEGER -- e + * } + * it's possible to examine the structure of the keys obtained from openssl using + * an asn.1 dumper as the one used here to parse the components: http://lapo.it/asn1js/ + * @argument {string} pem the pem encoded string, can include the BEGIN/END header/footer + * @private + */ +RSAKey.prototype.parseKey = function (pem) { + try { + var modulus = 0; + var public_exponent = 0; + var reHex = /^\s*(?:[0-9A-Fa-f][0-9A-Fa-f]\s*)+$/; + var der = reHex.test(pem) ? Hex.decode(pem) : Base64.unarmor(pem); + var asn1 = ASN1.decode(der); + + //Fixes a bug with OpenSSL 1.0+ private keys + if(asn1.sub.length === 3){ + asn1 = asn1.sub[2].sub[0]; + } + if (asn1.sub.length === 9) { + + // Parse the private key. + modulus = asn1.sub[1].getHexStringValue(); //bigint + this.n = parseBigInt(modulus, 16); + + public_exponent = asn1.sub[2].getHexStringValue(); //int + this.e = parseInt(public_exponent, 16); + + var private_exponent = asn1.sub[3].getHexStringValue(); //bigint + this.d = parseBigInt(private_exponent, 16); + + var prime1 = asn1.sub[4].getHexStringValue(); //bigint + this.p = parseBigInt(prime1, 16); + + var prime2 = asn1.sub[5].getHexStringValue(); //bigint + this.q = parseBigInt(prime2, 16); + + var exponent1 = asn1.sub[6].getHexStringValue(); //bigint + this.dmp1 = parseBigInt(exponent1, 16); + + var exponent2 = asn1.sub[7].getHexStringValue(); //bigint + this.dmq1 = parseBigInt(exponent2, 16); + + var coefficient = asn1.sub[8].getHexStringValue(); //bigint + this.coeff = parseBigInt(coefficient, 16); + + } + else if (asn1.sub.length === 2) { + + // Parse the public key. + var bit_string = asn1.sub[1]; + var sequence = bit_string.sub[0]; + + modulus = sequence.sub[0].getHexStringValue(); + this.n = parseBigInt(modulus, 16); + public_exponent = sequence.sub[1].getHexStringValue(); + this.e = parseInt(public_exponent, 16); + + } + else { + return false; + } + return true; + } + catch (ex) { + return false; + } +}; + +/** + * Translate rsa parameters in a hex encoded string representing the rsa key. + * + * The translation follow the ASN.1 notation : + * RSAPrivateKey ::= SEQUENCE { + * version Version, + * modulus INTEGER, -- n + * publicExponent INTEGER, -- e + * privateExponent INTEGER, -- d + * prime1 INTEGER, -- p + * prime2 INTEGER, -- q + * exponent1 INTEGER, -- d mod (p1) + * exponent2 INTEGER, -- d mod (q-1) + * coefficient INTEGER, -- (inverse of q) mod p + * } + * @returns {string} DER Encoded String representing the rsa private key + * @private + */ +RSAKey.prototype.getPrivateBaseKey = function () { + var options = { + 'array': [ + new KJUR.asn1.DERInteger({'int': 0}), + new KJUR.asn1.DERInteger({'bigint': this.n}), + new KJUR.asn1.DERInteger({'int': this.e}), + new KJUR.asn1.DERInteger({'bigint': this.d}), + new KJUR.asn1.DERInteger({'bigint': this.p}), + new KJUR.asn1.DERInteger({'bigint': this.q}), + new KJUR.asn1.DERInteger({'bigint': this.dmp1}), + new KJUR.asn1.DERInteger({'bigint': this.dmq1}), + new KJUR.asn1.DERInteger({'bigint': this.coeff}) + ] + }; + var seq = new KJUR.asn1.DERSequence(options); + return seq.getEncodedHex(); +}; + +/** + * base64 (pem) encoded version of the DER encoded representation + * @returns {string} pem encoded representation without header and footer + * @public + */ +RSAKey.prototype.getPrivateBaseKeyB64 = function () { + return hex2b64(this.getPrivateBaseKey()); +}; + +/** + * Translate rsa parameters in a hex encoded string representing the rsa public key. + * The representation follow the ASN.1 notation : + * PublicKeyInfo ::= SEQUENCE { + * algorithm AlgorithmIdentifier, + * PublicKey BIT STRING + * } + * Where AlgorithmIdentifier is: + * AlgorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, the OID of the enc algorithm + * parameters ANY DEFINED BY algorithm OPTIONAL (NULL for PKCS #1) + * } + * and PublicKey is a SEQUENCE encapsulated in a BIT STRING + * RSAPublicKey ::= SEQUENCE { + * modulus INTEGER, -- n + * publicExponent INTEGER -- e + * } + * @returns {string} DER Encoded String representing the rsa public key + * @private + */ +RSAKey.prototype.getPublicBaseKey = function () { + var options = { + 'array': [ + new KJUR.asn1.DERObjectIdentifier({'oid': '1.2.840.113549.1.1.1'}), //RSA Encryption pkcs #1 oid + new KJUR.asn1.DERNull() + ] + }; + var first_sequence = new KJUR.asn1.DERSequence(options); + + options = { + 'array': [ + new KJUR.asn1.DERInteger({'bigint': this.n}), + new KJUR.asn1.DERInteger({'int': this.e}) + ] + }; + var second_sequence = new KJUR.asn1.DERSequence(options); + + options = { + 'hex': '00' + second_sequence.getEncodedHex() + }; + var bit_string = new KJUR.asn1.DERBitString(options); + + options = { + 'array': [ + first_sequence, + bit_string + ] + }; + var seq = new KJUR.asn1.DERSequence(options); + return seq.getEncodedHex(); +}; + +/** + * base64 (pem) encoded version of the DER encoded representation + * @returns {string} pem encoded representation without header and footer + * @public + */ +RSAKey.prototype.getPublicBaseKeyB64 = function () { + return hex2b64(this.getPublicBaseKey()); +}; + +/** + * wrap the string in block of width chars. The default value for rsa keys is 64 + * characters. + * @param {string} str the pem encoded string without header and footer + * @param {Number} [width=64] - the length the string has to be wrapped at + * @returns {string} + * @private + */ +RSAKey.prototype.wordwrap = function (str, width) { + width = width || 64; + if (!str) { + return str; + } + var regex = '(.{1,' + width + '})( +|$\n?)|(.{1,' + width + '})'; + return str.match(RegExp(regex, 'g')).join('\n'); +}; + +/** + * Retrieve the pem encoded private key + * @returns {string} the pem encoded private key with header/footer + * @public + */ +RSAKey.prototype.getPrivateKey = function () { + var key = "-----BEGIN RSA PRIVATE KEY-----\n"; + key += this.wordwrap(this.getPrivateBaseKeyB64()) + "\n"; + key += "-----END RSA PRIVATE KEY-----"; + return key; +}; + +/** + * Retrieve the pem encoded public key + * @returns {string} the pem encoded public key with header/footer + * @public + */ +RSAKey.prototype.getPublicKey = function () { + var key = "-----BEGIN PUBLIC KEY-----\n"; + key += this.wordwrap(this.getPublicBaseKeyB64()) + "\n"; + key += "-----END PUBLIC KEY-----"; + return key; +}; + +/** + * Check if the object contains the necessary parameters to populate the rsa modulus + * and public exponent parameters. + * @param {Object} [obj={}] - An object that may contain the two public key + * parameters + * @returns {boolean} true if the object contains both the modulus and the public exponent + * properties (n and e) + * @todo check for types of n and e. N should be a parseable bigInt object, E should + * be a parseable integer number + * @private + */ +RSAKey.prototype.hasPublicKeyProperty = function (obj) { + obj = obj || {}; + return ( + obj.hasOwnProperty('n') && + obj.hasOwnProperty('e') + ); +}; + +/** + * Check if the object contains ALL the parameters of an RSA key. + * @param {Object} [obj={}] - An object that may contain nine rsa key + * parameters + * @returns {boolean} true if the object contains all the parameters needed + * @todo check for types of the parameters all the parameters but the public exponent + * should be parseable bigint objects, the public exponent should be a parseable integer number + * @private + */ +RSAKey.prototype.hasPrivateKeyProperty = function (obj) { + obj = obj || {}; + return ( + obj.hasOwnProperty('n') && + obj.hasOwnProperty('e') && + obj.hasOwnProperty('d') && + obj.hasOwnProperty('p') && + obj.hasOwnProperty('q') && + obj.hasOwnProperty('dmp1') && + obj.hasOwnProperty('dmq1') && + obj.hasOwnProperty('coeff') + ); +}; + +/** + * Parse the properties of obj in the current rsa object. Obj should AT LEAST + * include the modulus and public exponent (n, e) parameters. + * @param {Object} obj - the object containing rsa parameters + * @private + */ +RSAKey.prototype.parsePropertiesFrom = function (obj) { + this.n = obj.n; + this.e = obj.e; + + if (obj.hasOwnProperty('d')) { + this.d = obj.d; + this.p = obj.p; + this.q = obj.q; + this.dmp1 = obj.dmp1; + this.dmq1 = obj.dmq1; + this.coeff = obj.coeff; + } +}; + +/** + * Create a new JSEncryptRSAKey that extends Tom Wu's RSA key object. + * This object is just a decorator for parsing the key parameter + * @param {string|Object} key - The key in string format, or an object containing + * the parameters needed to build a RSAKey object. + * @constructor + */ +class JSEncryptRSAKey extends RSAKey { + constructor(key) { + // Call the super constructor. + super(); + // If a key key was provided. + if (key) { + // If this is a string... + if (typeof key === 'string') { + this.parseKey(key); + } + else if ( + this.hasPrivateKeyProperty(key) || + this.hasPublicKeyProperty(key) + ) { + // Set the values for the key. + this.parsePropertiesFrom(key); + } + } + } +} + +/** + * + * @param {Object} [options = {}] - An object to customize JSEncrypt behaviour + * possible parameters are: + * - default_key_size {number} default: 1024 the key size in bit + * - default_public_exponent {string} default: '010001' the hexadecimal representation of the public exponent + * - log {boolean} default: false whether log warn/error or not + * @constructor + */ +class JSEncrypt{ + constructor (options) { + options = options || {}; + this.default_key_size = parseInt(options.default_key_size) || 1024; + this.default_public_exponent = options.default_public_exponent || '010001'; //65537 default openssl public exponent for rsa key type + this.log = options.log || false; + // The private and public key. + this.key = null; + } +} + +/** + * Method to set the rsa key parameter (one method is enough to set both the public + * and the private key, since the private key contains the public key paramenters) + * Log a warning if logs are enabled + * @param {Object|string} key the pem encoded string or an object (with or without header/footer) + * @public + */ +JSEncrypt.prototype.setKey = function (key) { + if (this.log && this.key) { + console.warn('A key was already set, overriding existing.'); + } + this.key = new JSEncryptRSAKey(key); +}; + +/** + * Proxy method for setKey, for api compatibility + * @see setKey + * @public + */ +JSEncrypt.prototype.setPrivateKey = function (privkey) { + // Create the key. + this.setKey(privkey); +}; + +/** + * Proxy method for setKey, for api compatibility + * @see setKey + * @public + */ +JSEncrypt.prototype.setPublicKey = function (pubkey) { + // Sets the public key. + this.setKey(pubkey); +}; + +/** + * Proxy method for RSAKey object's decrypt, decrypt the string using the private + * components of the rsa key object. Note that if the object was not set will be created + * on the fly (by the getKey method) using the parameters passed in the JSEncrypt constructor + * @param {string} string base64 encoded crypted string to decrypt + * @return {string} the decrypted string + * @public + */ +JSEncrypt.prototype.decrypt = function (string) { + // Return the decrypted string. + try { + return this.getKey().decrypt(b64tohex(string)); + } + catch (ex) { + return false; + } +}; + +/** + * Proxy method for RSAKey object's encrypt, encrypt the string using the public + * components of the rsa key object. Note that if the object was not set will be created + * on the fly (by the getKey method) using the parameters passed in the JSEncrypt constructor + * @param {string} string the string to encrypt + * @return {string} the encrypted string encoded in base64 + * @public + */ +JSEncrypt.prototype.encrypt = function (string) { + // Return the encrypted string. + try { + return hex2b64(this.getKey().encrypt(string)); + } + catch (ex) { + return false; + } +}; + +/** + * Getter for the current JSEncryptRSAKey object. If it doesn't exists a new object + * will be created and returned + * @param {callback} [cb] the callback to be called if we want the key to be generated + * in an async fashion + * @returns {JSEncryptRSAKey} the JSEncryptRSAKey object + * @public + */ +JSEncrypt.prototype.getKey = function (cb) { + // Only create new if it does not exist. + if (!this.key) { + // Get a new private key. + this.key = new JSEncryptRSAKey(); + if (cb && {}.toString.call(cb) === '[object Function]') { + this.key.generateAsync(this.default_key_size, this.default_public_exponent, cb); + return; + } + // Generate the key. + this.key.generate(this.default_key_size, this.default_public_exponent); + } + return this.key; +}; + +/** + * Returns the pem encoded representation of the private key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the private key WITH header and footer + * @public + */ +JSEncrypt.prototype.getPrivateKey = function () { + // Return the private representation of this key. + return this.getKey().getPrivateKey(); +}; + +/** + * Returns the pem encoded representation of the private key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the private key WITHOUT header and footer + * @public + */ +JSEncrypt.prototype.getPrivateKeyB64 = function () { + // Return the private representation of this key. + return this.getKey().getPrivateBaseKeyB64(); +}; + + +/** + * Returns the pem encoded representation of the public key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the public key WITH header and footer + * @public + */ +JSEncrypt.prototype.getPublicKey = function () { + // Return the private representation of this key. + return this.getKey().getPublicKey(); +}; + +/** + * Returns the pem encoded representation of the public key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the public key WITHOUT header and footer + * @public + */ +JSEncrypt.prototype.getPublicKeyB64 = function () { + // Return the private representation of this key. + return this.getKey().getPublicBaseKeyB64(); +}; + +// export * from 'jsencrypt'; + +class BaseTransport { + constructor(endpoint, stream_type, config={}) { + this.stream_type = stream_type; + this.endpoint = endpoint; + this.eventSource = new EventEmitter(); + this.dataQueue = []; + } + + static canTransfer(stream_type) { + return BaseTransport.streamTypes().includes(stream_type); + } + + static streamTypes() { + return []; + } + + destroy() { + this.eventSource.destroy(); + } + + connect() { + // TO be impemented + } + + disconnect() { + // TO be impemented + } + + reconnect() { + return this.disconnect().then(()=>{ + return this.connect(); + }); + } + + setEndpoint(endpoint) { + this.endpoint = endpoint; + return this.reconnect(); + } + + send(data) { + // TO be impemented + // return this.prepare(data).send(); + } + + prepare(data) { + // TO be impemented + // return new Request(data); + } + + // onData(type, data) { + // this.eventSource.dispatchEvent(type, data); + // } +} + +const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + +//navigator.hardwareConcurrency || 3; + +const LOG_TAG$5 = "transport:ws"; +const Log$10 = getTagged(LOG_TAG$5); +class WebsocketTransport extends BaseTransport { + constructor(endpoint, stream_type, options={ + socket:`${location.protocol.replace('http', 'ws')}//${location.host}/ws/`, + workers: 1 + }) { + super(endpoint, stream_type); + this.proxies = []; + this.currentProxy = 0; + this.workers = 1; + this.socket_url = options.socket; + this.ready = this.connect(); + } + + destroy() { + return this.disconnect().then(()=>{ + return super.destroy(); + }); + + } + + static canTransfer(stream_type) { + return WebsocketTransport.streamTypes().includes(stream_type); + } + + static streamTypes() { + return ['hls', 'rtsp']; + } + + connect() { + return this.disconnect().then(()=>{ + let promises = []; + // TODO: get mirror list + for (let i=0; i { + this.eventSource.dispatchEvent('disconnected', {code: e.code, reason: e.reason}); + // TODO: only reconnect on demand + if ([1000, 1006, 1013, 1011].includes(e.code)) { + setTimeout(()=> { + if (this.ready && this.ready.reject) { + this.ready.reject(); + } + this.ready = this.connect(); + }, 3000); + } + }); + + proxy.set_data_handler((data)=> { + this.dataQueue.push(new Uint8Array(data)); + this.eventSource.dispatchEvent('data'); + }); + + promises.push(proxy.connect().then(()=> { + this.eventSource.dispatchEvent('connected'); + }).catch((e)=> { + this.eventSource.dispatchEvent('error'); + throw new Error(e); + })); + this.proxies.push(proxy); + } + return Promise.all(promises); + }); + } + + disconnect() { + let promises = []; + for (let i=0; i{}; + this.disconnect_handler = ()=>{}; + this.builder = new WSPProtocol(WSPProtocol.V1_1); + this.awaitingPromises = {}; + this.seq = 0; + this.encryptor = new JSEncrypt(); + } + + set_data_handler(handler) { + this.data_handler = handler; + } + + set_disconnect_handler(handler) { + this.disconnect_handler = handler; + } + + close() { + Log$10.log('closing connection'); + return new Promise((resolve)=>{ + this.ctrlChannel.onclose = ()=>{ + if (this.dataChannel) { + this.dataChannel.onclose = ()=>{ + Log$10.log('closed'); + resolve(); + }; + this.dataChannel.close(); + } else { + Log$10.log('closed'); + resolve(); + } + }; + this.ctrlChannel.close(); + }); + } + + onDisconnect(){ + this.ctrlChannel.onclose=null; + this.ctrlChannel.close(); + if (this.dataChannel) { + this.dataChannel.onclose = null; + this.dataChannel.close(); + } + this.disconnect_handler(this); + } + + initDataChannel(channel_id) { + return new Promise((resolve, reject)=>{ + this.dataChannel = new WebSocket(this.url, WebSocketProxy.CHN_DATA); + this.dataChannel.binaryType = 'arraybuffer'; + this.dataChannel.onopen = ()=>{ + let msg = this.builder.build(WSPProtocol.CMD_JOIN, { + channel: channel_id + }); + Log$10.debug(msg); + this.dataChannel.send(msg); + }; + this.dataChannel.onmessage = (ev)=>{ + Log$10.debug(`[data]\r\n${ev.data}`); + let res = WSPProtocol.parse(ev.data); + if (!res) { + return reject(); + } + + this.dataChannel.onmessage=(e)=>{ + // Log.debug('got data'); + if (this.data_handler) { + this.data_handler(e.data); + } + }; + resolve(); + }; + this.dataChannel.onerror = (e)=>{ + Log$10.error(`[data] ${e.type}`); + this.dataChannel.close(); + }; + this.dataChannel.onclose = (e)=>{ + Log$10.error(`[data] ${e.type}. code: ${e.code}, reason: ${e.reason || 'unknown reason'}`); + this.onDisconnect(e); + }; + }); + } + + connect() { + this.encryptionKey = null; + return new Promise((resolve, reject)=>{ + this.ctrlChannel = new WebSocket(this.url, WebSocketProxy.CHN_CONTROL); + + this.connected = false; + + this.ctrlChannel.onopen = ()=>{ + let headers = { + proto: this.stream_type + }; + if (this.endpoint.socket) { + headers.socket = this.endpoint.socket; + } else { + Object.assign(headers, { + host: this.endpoint.host, + port: this.endpoint.port + }); + } + let msg = this.builder.build(WSPProtocol.CMD_INIT, headers); + Log$10.debug(msg); + this.ctrlChannel.send(msg); + }; + + this.ctrlChannel.onmessage = (ev)=>{ + Log$10.debug(`[ctrl]\r\n${ev.data}`); + + let res = WSPProtocol.parse(ev.data); + if (!res) { + return reject(); + } + + if (res.code >= 300) { + Log$10.error(`[ctrl]\r\n${res.code}: ${res.msg}`); + return reject(); + } + this.ctrlChannel.onmessage = (e)=> { + let res = WSPProtocol.parse(e.data); + Log$10.debug(`[ctrl]\r\n${e.data}`); + if (res.data.seq in this.awaitingPromises) { + if (res.code < 300) { + this.awaitingPromises[res.data.seq].resolve(res); + } else { + this.awaitingPromises[res.data.seq].reject(res); + } + delete this.awaitingPromises[res.data.seq]; + } + }; + this.encryptionKey = res.data.pubkey || null; + if (this.encryptionKey) { + this.encryptor.setPublicKey(this.encryptionKey); + // TODO: check errors + } + this.initDataChannel(res.data.channel).then(resolve).catch(reject); + }; + + this.ctrlChannel.onerror = (e)=>{ + Log$10.error(`[ctrl] ${e.type}`); + this.ctrlChannel.close(); + }; + this.ctrlChannel.onclose = (e)=>{ + Log$10.error(`[ctrl] ${e.type}. code: ${e.code} ${e.reason || 'unknown reason'}`); + this.onDisconnect(e); + }; + }); + } + + encrypt(msg) { + if (this.encryptionKey) { + let crypted = this.encryptor.encrypt(msg); + if (crypted === false) { + throw new Error("Encryption failed. Stopping") + } + return crypted; + } + return msg; + } + + send(payload) { + if (this.ctrlChannel.readyState != WebSocket.OPEN) { + this.close(); + // .then(this.connect.bind(this)); + // return; + throw new Error('disconnected'); + } + // Log.debug(payload); + let data = { + contentLength: payload.length, + seq: ++WSPProtocol.seq + }; + return { + seq:data.seq, + promise: new Promise((resolve, reject)=>{ + this.awaitingPromises[data.seq] = {resolve, reject}; + let msg = this.builder.build(WSPProtocol.CMD_WRAP, data, payload); + Log$10.debug(msg); + this.ctrlChannel.send(this.encrypt(msg)); + })}; + } +} + +const Log$11 = getTagged('wsp'); + +class StreamType$1 { + static get HLS() {return 'hls';} + static get RTSP() {return 'rtsp';} + + static isSupported(type) { + return [StreamType$1.HLS, StreamType$1.RTSP].includes(type); + } + + static fromUrl(url) { + let parsed; + try { + parsed = Url.parse(url); + } catch (e) { + return null; + } + switch (parsed.protocol) { + case 'rtsp': + return StreamType$1.RTSP; + case 'http': + case 'https': + if (url.indexOf('.m3u8')>=0) { + return StreamType$1.HLS; + } else { + return null; + } + default: + return null; + } + } + + static fromMime(mime) { + switch (mime) { + case 'application/x-rtsp': + return StreamType$1.RTSP; + case 'application/vnd.apple.mpegurl': + case 'application/x-mpegurl': + return StreamType$1.HLS; + default: + return null; + } + } +} + +class WSPlayer { + + constructor(node, opts) { + if (typeof node == typeof '') { + this.player = document.getElementById(node); + } else { + this.player = node; + } + + let modules = opts.modules || { + client: RTSPClient, + transport: { + constructor: WebsocketTransport + } + }; + this.errorHandler = opts.errorHandler || null; + this.queryCredentials = opts.queryCredentials || null; + + this.modules = {}; + for (let module of modules) { + let transport = module.transport || WebsocketTransport; + let client = module.client || RTSPClient; + if (transport.constructor.canTransfer(client.streamType())) { + this.modules[client.streamType()] = { + client: client, + transport: transport + }; + } else { + Log$11.warn(`Client stream type ${client.streamType()} is incompatible with transport types [${transport.streamTypes().join(', ')}]. Skip`); + } + } + + this.type = StreamType$1.RTSP; + this.url = null; + if (opts.url && opts.type) { + this.url = opts.url; + this.type = opts.type; + } else { + if (!this._checkSource(this.player)) { + for (let i=0; i{ + if (!this.isPlaying()) { + this.client.start(); + } + }, false); + + this.player.addEventListener('pause', ()=>{ + this.client.stop(); + }, false); + } + + // TODO: check native support + + isPlaying() { + return !(this.player.paused || this.client.paused); + } + + static canPlayWithModules(mimeType, modules) { + + let filteredModules = {}; + for (let module of modules) { + let transport = module.transport || WebsocketTransport; + let client = module.client || RTSPClient; + if (transport.canTransfer(client.streamType())) { + filteredModules[client.streamType()] = true; + } + } + + for (let type in filteredModules) { + if (type == StreamType$1.fromMime(mimeType)) { + return true; + } + } + return false; + } + + /// TODO: deprecate it? + static canPlay(resource) { + return StreamType$1.fromMime(resource.type) || StreamType$1.fromUrl(resource.src); + } + + canPlayUrl(src) { + let type = StreamType$1.fromUrl(src); + return (type in this.modules); + } + + _checkSource(src) { + if (!src.dataset['ignore'] && src.src && !this.player.canPlayType(src.type) && (StreamType$1.fromMime(src.type) || StreamType$1.fromUrl(src.src))) { + this.url = src.src; + this.type = src.type ? StreamType$1.fromMime(src.type) : StreamType$1.fromUrl(src.src); + return true; + } + return false; + } + + async setSource(url, type) { + if (this.transport) { + if (this.client) { + await this.client.detachTransport(); + } + await this.transport.destroy(); + } + try { + this.endpoint = Url.parse(url); + } catch (e) { + return; + } + this.url = url; + let transport = this.modules[type].transport; + this.transport = new transport.constructor(this.endpoint, this.type, transport.options); + + + let lastType = this.type; + this.type = (StreamType$1.isSupported(type)?type:false) || StreamType$1.fromMime(type); + if (!this.type) { + throw new Error("Bad stream type"); + } + + if (lastType!=this.type || !this.client) { + if (this.client) { + await this.client.destroy(); + } + let client = this.modules[type].client; + this.client = new client(); + } else { + this.client.reset(); + } + + if (this.queryCredentials) { + this.client.queryCredentials = this.queryCredentials; + } + if (this.remuxer) { + this.remuxer.destroy(); + this.remuxer = null; + } + this.remuxer = new Remuxer(this.player); + this.remuxer.attachClient(this.client); + + this.client.attachTransport(this.transport); + this.client.setSource(this.endpoint); + + if (this.player.autoplay) { + this.start(); + } + } + + start() { + if (this.client) { + this.client.start().catch((e)=>{ + if (this.errorHandler) { + this.errorHandler(e); + } + }); + } + } + + stop() { + if (this.client) { + this.client.stop(); + } + } + + async destroy() { + if (this.transport) { + if (this.client) { + await this.client.detachTransport(); + } + await this.transport.destroy(); + } + if (this.client) { + await this.client.destroy(); + } + if (this.remuxer) { + this.remuxer.destroy(); + this.remuxer = null; + } + } + +} + +setDefaultLogLevel(LogLevel.Error); +getTagged("transport:ws").setLevel(LogLevel.Error); +getTagged("client:rtsp").setLevel(LogLevel.Debug); +getTagged("mse").setLevel(LogLevel.Debug); + +window.Streamedian = { + logger(tag) { + return getTagged(tag) + }, + player(node, opts) { + if (!opts.socket) { + throw new Error("socket parameter is not set"); + } + let _options = { + modules: [ + { + client: RTSPClient, + transport: { + constructor: WebsocketTransport, + options: { + socket: opts.socket + } + } + } + ], + errorHandler(e) { + alert(`Failed to start player: ${e.message}`); + }, + queryCredentials(client) { + return new Promise((resolve, reject) => { + let c = prompt('input credentials in format user:password'); + if (c) { + client.setCredentials.apply(client, c.split(':')); + resolve(); + } else { + reject(); + } + }); + } + }; + return new WSPlayer(node, _options); + } +}; + +}()); +//# sourceMappingURL=streamedian.min.js.map diff --git a/demos/wsp/index.html b/demos/wsp/index.html new file mode 100755 index 0000000..d824291 --- /dev/null +++ b/demos/wsp/index.html @@ -0,0 +1,251 @@ + + + + + RTSP player example(based streamedian) + + + + +
+
+ + +
+
+

Enter your rtsp link to the stream, for example: "rtsp://localhost:1554/test/live1"

+

If need token,for example: "rtsp://localhost:1554/test/live1?token=4df8f5d5d680385cb07c2e354dd0f3f3"

+
+ +
+ + 120sec. +
+ +
+

Change buffer duration

+
+ + + +
+
+ Playback rate:  + + live +
+
+ +
+
+ +

View HTML5 RTSP video player log

+
+ + + + +

+ + + + + + + + diff --git a/demos/wsp/style.css b/demos/wsp/style.css new file mode 100755 index 0000000..dac210c --- /dev/null +++ b/demos/wsp/style.css @@ -0,0 +1,28 @@ +body { + max-width: 720px; + margin: 50px auto; +} + +#test_video { + width: 720px; +} + +.controls { + display: flex; + justify-content: space-around; + align-items: center; +} +input.input, .form-inline .input-group>.form-control { + width: 300px; +} +.logs { + overflow: auto; + width: 720px; + height: 150px; + padding: 5px; + border-top: solid 1px gray; + border-bottom: solid 1px gray; +} +button { + margin: 5px +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3302292 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/cnotch/tomatox + +go 1.14 + +require ( + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/cnotch/loader v0.0.0-20200405015128-d9d964d09439 + github.com/cnotch/scheduler v0.0.0-20200522024700-1d2da93eefc5 + github.com/cnotch/xlog v0.0.0-20201208005456-cfda439cd3a0 + github.com/emitter-io/address v1.0.0 + github.com/gorilla/websocket v1.4.2 + github.com/kelindar/process v0.0.0-20170730150328-69a29e249ec3 + github.com/kelindar/rate v1.0.0 + github.com/stretchr/testify v1.6.1 + golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0d09b13 --- /dev/null +++ b/go.sum @@ -0,0 +1,63 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cnotch/loader v0.0.0-20200405015128-d9d964d09439 h1:iNWyllf6zuby+nDNC6zKEkM7aUFbp4RccfWVdQ3HFfQ= +github.com/cnotch/loader v0.0.0-20200405015128-d9d964d09439/go.mod h1:oWpDagHB6p+Kqqq7RoRZKyC4XAXft50hR8pbTxdbYYs= +github.com/cnotch/scheduler v0.0.0-20200522024700-1d2da93eefc5 h1:m9Wx/d4iPXFmE0f2zJ6iQ8tXZ52kOZO9qs/kMevEHxk= +github.com/cnotch/scheduler v0.0.0-20200522024700-1d2da93eefc5/go.mod h1:F4GE3SZkJZ8an1Y0ZCqvSM3jeozNuKzoC67erG1PhIo= +github.com/cnotch/xlog v0.0.0-20201208005456-cfda439cd3a0 h1:YXATGJEn/ymZjZOGCFfE5248ABcLbfwpd/dQGfByxGQ= +github.com/cnotch/xlog v0.0.0-20201208005456-cfda439cd3a0/go.mod h1:RW9oHsR79ffl3sR3yMGgxYupMn2btzdtJUwoxFPUE5E= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emitter-io/address v1.0.0 h1:j8mAEIV2TipN2TOf/sTNveJjf8nTBq2ov7/qBG/19vg= +github.com/emitter-io/address v1.0.0/go.mod h1:GfZb5+S/o8694B1GMGK2imUYQyn2skszMvGNA5D84Ug= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kelindar/process v0.0.0-20170730150328-69a29e249ec3 h1:6If+E1dikQbdT7DlhZqLplfGkEt6dSoz7+MK+TFC7+U= +github.com/kelindar/process v0.0.0-20170730150328-69a29e249ec3/go.mod h1:+lTCLnZFXOkqwD8sLPl6u4erAc0cP8wFegQHfipz7KE= +github.com/kelindar/rate v1.0.0 h1:JNZdufLjtDzr/E/rCtWkqo2OVU4yJSScZngJ8LuZ7kU= +github.com/kelindar/rate v1.0.0/go.mod h1:AjT4G+hTItNwt30lucEGZIz8y7Uk5zPho6vurIZ+1Es= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 h1:sYNJzB4J8toYPQTM6pAkcmBRgw9SnQKP9oXCHfgy604= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100755 index 0000000..0a98296 --- /dev/null +++ b/main.go @@ -0,0 +1,40 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package main + +import ( + "github.com/cnotch/scheduler" + "github.com/cnotch/tomatox/config" + "github.com/cnotch/tomatox/provider/auth" + "github.com/cnotch/tomatox/provider/route" + "github.com/cnotch/xlog" +) + +func main() { + // 初始化配置 + config.InitConfig() + // 初始化全局计划任务 + scheduler.SetPanicHandler(func(job *scheduler.ManagedJob, r interface{}) { + xlog.Errorf("scheduler task panic. tag: %v, recover: %v", job.Tag, r) + }) + + // 初始化各类提供者 + // 路由表提供者 + routetableProvider := config.LoadRoutetableProvider(route.JSON) + route.Reset(routetableProvider.(route.Provider)) + + // 用户提供者 + userProvider := config.LoadUsersProvider(auth.JSON) + auth.Reset(userProvider.(auth.UserProvider)) + + // // Start new service + // svc, err := service.NewService(context.Background(), xlog.L()) + // if err != nil { + // xlog.L().Panic(err.Error()) + // } + + // // Listen and serve + // svc.Listen() +} diff --git a/makefile b/makefile new file mode 100755 index 0000000..425892a --- /dev/null +++ b/makefile @@ -0,0 +1,52 @@ +# Go parameters +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOGET=$(GOCMD) get +ENABLED_CGO=0 +BINARY_NAME=tomatox +BINARY_DIR= bin/v1.0.0 + +build: + CGO_ENABLED=$(ENABLED_CGO) $(GOBUILD) -o bin/$(BINARY_NAME) . + cp -r demos bin/ + cp -r docs bin/ +# linux compilation +build-linux-amd64: + CGO_ENABLED=$(ENABLED_CGO) GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINARY_DIR)/linux/amd64/$(BINARY_NAME)$(VERSION) . + cp -r demos $(BINARY_DIR)/linux/amd64/ + cp -r docs $(BINARY_DIR)/linux/amd64/ +build-linux-386: + CGO_ENABLED=$(ENABLED_CGO) GOOS=linux GOARCH=386 $(GOBUILD) -o $(BINARY_DIR)/linux/386/$(BINARY_NAME)$(VERSION) . +build-linux-arm: + CGO_ENABLED=$(ENABLED_CGO) GOOS=linux GOARCH=arm $(GOBUILD) -o $(BINARY_DIR)/linux/arm/$(BINARY_NAME)$(VERSION) . + +# window compilation +build-windows-amd64: + CGO_ENABLED=$(ENABLED_CGO) GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BINARY_DIR)/windows/amd64/$(BINARY_NAME)$(VERSION).exe . + cp -r demos $(BINARY_DIR)/windows/amd64/ + cp -r docs $(BINARY_DIR)/windows/amd64/ +build-windows-386: + CGO_ENABLED=$(ENABLED_CGO) GOOS=windows GOARCH=386 $(GOBUILD) -o $(BINARY_DIR)/windows/386/$(BINARY_NAME)$(VERSION).exe . + +# darwin compilation +build-darwin-amd64: + CGO_ENABLED=$(ENABLED_CGO) GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BINARY_DIR)/darwin/amd64/$(BINARY_NAME)$(VERSION) . + cp -r demos $(BINARY_DIR)/darwin/amd64/ + cp -r docs $(BINARY_DIR)/darwin/amd64/ +build-darwin-386: + CGO_ENABLED=$(ENABLED_CGO) GOOS=darwin GOARCH=386 $(GOBUILD) -o $(BINARY_DIR)/darwin/386/$(BINARY_NAME)$(VERSION) . + +# amd64 all platform compilation +build-amd64: build-linux-amd64 build-windows-amd64 build-darwin-amd64 + +# all +build-all: build-linux-amd64 build-windows-amd64 build-darwin-amd64 build-linux-386 build-windows-386 build-darwin-386 build-linux-arm + +test: + $(GOTEST) -v ./... +clean: + $(GOCLEAN) + rm -f bin/$(BINARY_NAME) + rm -rf $(BINARY_DIR) \ No newline at end of file diff --git a/network/socket/buffered/conn.go b/network/socket/buffered/conn.go new file mode 100644 index 0000000..452adda --- /dev/null +++ b/network/socket/buffered/conn.go @@ -0,0 +1,202 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package buffered + +import ( + "bufio" + "bytes" + "net" + "time" + + "github.com/kelindar/rate" +) + +const ( + defaultRate = 50 + defaultBufferSize = 64 * 1024 + minBufferSize = 8 * 1024 +) + +// Conn wraps a net.Conn and provides buffered ability. +type Conn struct { + socket net.Conn // The underlying network connection. + reader *bufio.Reader // The buffered reader + writer *bytes.Buffer // The buffered write queue. + limit *rate.Limiter // The write rate limiter. + bufferSize int // The read and write max buffer size +} + +// NewConn creates a new sniffed connection. +func NewConn(c net.Conn, options ...Option) *Conn { + conn, ok := c.(*Conn) + if !ok { + conn = &Conn{ + socket: c, + } + } + + for _, option := range options { + option.apply(conn) + } + + // 设置默认值刷新频率 + if conn.limit == nil { + conn.limit = rate.New(defaultRate, time.Second) + } + + if conn.bufferSize <= 0 { + conn.bufferSize = defaultBufferSize + } + + // 设置IO缓冲对象 + conn.reader = bufio.NewReaderSize(conn.socket, conn.bufferSize) + conn.writer = bytes.NewBuffer(make([]byte, 0, conn.bufferSize)) + return conn +} + +// Buffered returns the pending buffer size. +func (m *Conn) Buffered() (n int) { + return m.writer.Len() +} + +// Reader 返回内部的 bufio.Reader +func (m *Conn) Reader() *bufio.Reader { + return m.reader +} + +// Flush flushes the underlying buffer by writing into the underlying connection. +func (m *Conn) Flush() (n int, err error) { + if m.Buffered() == 0 { + return 0, nil + } + + // Flush everything and reset the buffer + n, err = m.writeFull(m.writer.Bytes()) + m.writer.Reset() + return +} + +// Read reads the block of data from the underlying buffer. +func (m *Conn) Read(p []byte) (int, error) { + return m.reader.Read(p) +} + +// Write writes the block of data into the underlying buffer. +func (m *Conn) Write(p []byte) (nn int, err error) { + var n int + // 没有足够的空间容纳 p + for len(p) > m.bufferSize-m.Buffered() && err == nil { + if m.Buffered() == 0 { + // Large write, empty buffer. + // Write directly from p to avoid copy. + n, err = m.socket.Write(p) + } else { + // write buffer to full state,and flush + n, err = m.writer.Write(p[:m.bufferSize-m.writer.Len()]) + _, err = m.Flush() + } + nn += n + p = p[n:] + } + + if err != nil { + return nn, err + } + + // 未到达时间频率的间隔,直接写到缓存 + if m.limit.Limit() { + n, err = m.writer.Write(p) + return nn + n, err + } + + // 缓存中有数据,flush + if m.Buffered() > 0 { + n, err = m.writer.Write(p) + _, err = m.Flush() + return nn + n, err + } + + // 缓存中无数据,直接写避免内存拷贝 + n, err = m.writeFull(p) + return nn + n, err + +} + +func (m *Conn) writeFull(p []byte) (nn int, err error) { + var n int + for len(p) > 0 && err == nil { + n, err = m.socket.Write(p) + nn += n + p = p[n:] + } + return nn, err +} + +// Close closes the connection. Any blocked Read or Write operations will be unblocked +// and return errors. +func (m *Conn) Close() error { + return m.socket.Close() +} + +// LocalAddr returns the local network address. +func (m *Conn) LocalAddr() net.Addr { + return m.socket.LocalAddr() +} + +// RemoteAddr returns the remote network address. +func (m *Conn) RemoteAddr() net.Addr { + return m.socket.RemoteAddr() +} + +// SetDeadline sets the read and write deadlines associated +// with the connection. It is equivalent to calling both +// SetReadDeadline and SetWriteDeadline. +func (m *Conn) SetDeadline(t time.Time) error { + return m.socket.SetDeadline(t) +} + +// SetReadDeadline sets the deadline for future Read calls +// and any currently-blocked Read call. +func (m *Conn) SetReadDeadline(t time.Time) error { + return m.socket.SetReadDeadline(t) +} + +// SetWriteDeadline sets the deadline for future Write calls +// and any currently-blocked Write call. +func (m *Conn) SetWriteDeadline(t time.Time) error { + return m.socket.SetWriteDeadline(t) +} + +// Option 配置 Conn 的选项接口 +type Option interface { + apply(*Conn) +} + +// OptionFunc 包装函数以便它满足 Option 接口 +type optionFunc func(*Conn) + +func (f optionFunc) apply(c *Conn) { + f(c) +} + +// FlushRate Conn 写操作的每秒刷新频率 +func FlushRate(r int) Option { + return optionFunc(func(c *Conn) { + if r < 1 { // 如果不合规,设置成默认值 + r = defaultRate + } + c.limit = rate.New(r, time.Second) + }) +} + +// BufferSize Conn 缓冲大小 +func BufferSize(bufferSize int) Option { + return optionFunc(func(c *Conn) { + if bufferSize < minBufferSize { // 如果不合规,设置成最小值 + bufferSize = minBufferSize + } + c.bufferSize = bufferSize + }) +} diff --git a/network/socket/buffered/conn_test.go b/network/socket/buffered/conn_test.go new file mode 100644 index 0000000..b1be84e --- /dev/null +++ b/network/socket/buffered/conn_test.go @@ -0,0 +1,76 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package buffered + +import ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/kelindar/rate" +) + +func TestConn(t *testing.T) { + conn := NewConn(new(fakeConn)) + defer conn.Close() + + assert.Equal(t, 0, conn.Buffered()) + assert.Nil(t, conn.LocalAddr()) + assert.Nil(t, conn.RemoteAddr()) + assert.Nil(t, conn.SetDeadline(time.Now())) + assert.Nil(t, conn.SetReadDeadline(time.Now())) + assert.Nil(t, conn.SetWriteDeadline(time.Now())) + + conn.limit = rate.New(1, time.Millisecond) + for i := 0; i < 100; i++ { + _, err := conn.Write([]byte{1, 2, 3}) + assert.NoError(t, err) + } + time.Sleep(10 * time.Millisecond) + _, err := conn.Write([]byte{1, 2, 3}) + assert.NoError(t, err) + conn.Write(make([]byte, 122*1024)) + assert.Equal(t, defaultBufferSize, conn.writer.Cap(), "buffer can't extend") +} + +// ------------------------------------------------------------------------------------ + +type fakeConn struct{} + +func (m *fakeConn) Read(p []byte) (int, error) { + return 0, nil +} + +func (m *fakeConn) Write(p []byte) (int, error) { + if len(p) > minBufferSize { + return minBufferSize, nil + } + return len(p), nil +} + +func (m *fakeConn) Close() error { + return nil +} + +func (m *fakeConn) LocalAddr() net.Addr { + return nil +} + +func (m *fakeConn) RemoteAddr() net.Addr { + return nil +} + +func (m *fakeConn) SetDeadline(t time.Time) error { + return nil +} + +func (m *fakeConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (m *fakeConn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/network/socket/listener/listener.go b/network/socket/listener/listener.go new file mode 100755 index 0000000..e8848e4 --- /dev/null +++ b/network/socket/listener/listener.go @@ -0,0 +1,330 @@ +/********************************************************************************** +* Copyright (c) 2009-2017 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +************************************************************************************/ + +package listener + +import ( + "bytes" + "crypto/tls" + "fmt" + "io" + "net" + "sync" + "time" +) + +// Server represents a server which can serve requests. +type Server interface { + Serve(listener net.Listener) +} + +// Matcher matches a connection based on its content. +type Matcher func(io.Reader) bool + +// SettingsHandler 处理连接使用前的设置 +type SettingsHandler func(net.Conn) + +// ErrorHandler handles an error and notifies the listener on whether +// it should continue serving. +type ErrorHandler func(error) bool + +var _ net.Error = ErrNotMatched{} + +// ErrNotMatched is returned whenever a connection is not matched by any of +// the matchers registered in the multiplexer. +type ErrNotMatched struct { + c net.Conn +} + +func (e ErrNotMatched) Error() string { + return fmt.Sprintf("Unable to match connection %v", e.c.RemoteAddr()) +} + +// Temporary implements the net.Error interface. +func (e ErrNotMatched) Temporary() bool { return true } + +// Timeout implements the net.Error interface. +func (e ErrNotMatched) Timeout() bool { return false } + +type errListenerClosed string + +func (e errListenerClosed) Error() string { return string(e) } +func (e errListenerClosed) Temporary() bool { return false } +func (e errListenerClosed) Timeout() bool { return false } + +// ErrListenerClosed is returned from muxListener.Accept when the underlying +// listener is closed. +var ErrListenerClosed = errListenerClosed("mux: listener closed") + +// for readability of readTimeout +var noTimeout time.Duration + +// New announces on the local network address laddr. The syntax of laddr is +// "host:port", like "127.0.0.1:8080". If host is omitted, as in ":8080", +// New listens on all available interfaces instead of just the interface +// with the given host address. Listening on a hostname is not recommended +// because this creates a socket for at most one of its IP addresses. +func New(address string, config *tls.Config) (*Listener, error) { + l, err := net.Listen("tcp", address) + if err != nil { + return nil, err + } + + // If we have a TLS configuration provided, wrap the listener in TLS + if config != nil { + l = tls.NewListener(l, config) + } + + return &Listener{ + root: l, + bufferSize: 1024, + errorHandler: func(_ error) bool { return true }, + closing: make(chan struct{}), + readTimeout: noTimeout, + settingsHandler: func(_ net.Conn) {}, + }, nil +} + +type processor struct { + matchers []Matcher + listen muxListener +} + +// Listener represents a listener used for multiplexing protocols. +type Listener struct { + root net.Listener + bufferSize int + errorHandler ErrorHandler + closing chan struct{} + matchers []processor + readTimeout time.Duration + settingsHandler SettingsHandler +} + +// Accept waits for and returns the next connection to the listener. +func (m *Listener) Accept() (net.Conn, error) { + return m.root.Accept() +} + +// ServeAsync adds a protocol based on the matcher and serves it. +func (m *Listener) ServeAsync(matcher Matcher, serve func(l net.Listener) error) { + l := m.Match(matcher) + go serve(l) +} + +// Match returns a net.Listener that sees (i.e., accepts) only +// the connections matched by at least one of the matcher. +func (m *Listener) Match(matchers ...Matcher) net.Listener { + ml := muxListener{ + Listener: m.root, + connections: make(chan net.Conn, m.bufferSize), + } + m.matchers = append(m.matchers, processor{matchers: matchers, listen: ml}) + return ml +} + +// SetReadTimeout sets a timeout for the read of matchers. +func (m *Listener) SetReadTimeout(t time.Duration) { + m.readTimeout = t +} + +// Serve starts multiplexing the listener. +func (m *Listener) Serve() error { + var wg sync.WaitGroup + + defer func() { + close(m.closing) + wg.Wait() + + for _, sl := range m.matchers { + close(sl.listen.connections) + // Drain the connections enqueued for the listener. + for c := range sl.listen.connections { + _ = c.Close() + } + } + }() + + for { + c, err := m.root.Accept() + if err != nil { + if !m.handleErr(err) { + return err + } + continue + } + + wg.Add(1) + go m.serve(c, m.closing, &wg) + } +} + +func (m *Listener) serve(c net.Conn, donec <-chan struct{}, wg *sync.WaitGroup) { + defer wg.Done() + + m.settingsHandler(c) + + muc := newConn(c) + if m.readTimeout > noTimeout { + _ = c.SetReadDeadline(time.Now().Add(m.readTimeout)) + } + for _, sl := range m.matchers { + for _, processor := range sl.matchers { + matched := processor(muc.startSniffing()) + if matched { + muc.doneSniffing() + if m.readTimeout > noTimeout { + _ = c.SetReadDeadline(time.Time{}) + } + select { + case sl.listen.connections <- muc: + case <-donec: + _ = c.Close() + } + return + } + } + } + + _ = c.Close() + err := ErrNotMatched{c: c} + if !m.handleErr(err) { + _ = m.root.Close() + } +} + +// HandleSettings 处理连接设置的函数,给予调用者一个干预系统级设置的机会 +func (m *Listener) HandleSettings(h SettingsHandler) { + if h != nil { + m.settingsHandler = h + } +} + +// HandleError registers an error handler that handles listener errors. +func (m *Listener) HandleError(h ErrorHandler) { + m.errorHandler = h +} + +func (m *Listener) handleErr(err error) bool { + if !m.errorHandler(err) { + return false + } + + if ne, ok := err.(net.Error); ok { + return ne.Temporary() + } + + return false +} + +// Close closes the listener +func (m *Listener) Close() error { + return m.root.Close() +} + +// Addr returns the listener's network address. +func (m *Listener) Addr() net.Addr { + return m.root.Addr() +} + +// ------------------------------------------------------------------------------------ + +type muxListener struct { + net.Listener + connections chan net.Conn +} + +func (l muxListener) Accept() (net.Conn, error) { + c, ok := <-l.connections + if !ok { + return nil, ErrListenerClosed + } + return c, nil +} + +// ------------------------------------------------------------------------------------ + +// Conn wraps a net.Conn and provides transparent sniffing of connection data. +type Conn struct { + net.Conn + sniffer sniffer + reader io.Reader +} + +// NewConn creates a new sniffed connection. +func newConn(c net.Conn) *Conn { + m := &Conn{ + Conn: c, + sniffer: sniffer{source: c}, + } + + m.sniffer.conn = m + m.reader = &m.sniffer + return m +} + +// Read reads the block of data from the underlying buffer. +func (m *Conn) Read(p []byte) (int, error) { + return m.reader.Read(p) +} + +func (m *Conn) startSniffing() io.Reader { + m.sniffer.reset(true) + return &m.sniffer +} + +func (m *Conn) doneSniffing() { + m.sniffer.reset(false) +} + +// ------------------------------------------------------------------------------------ + +// Sniffer represents a io.Reader which can peek incoming bytes and reset back to normal. +type sniffer struct { + conn *Conn + source io.Reader + buffer bytes.Buffer + bufferRead int + bufferSize int + sniffing bool + lastErr error +} + +// Read reads data from the buffer. +func (s *sniffer) Read(p []byte) (int, error) { + if s.bufferSize > s.bufferRead { + bn := copy(p, s.buffer.Bytes()[s.bufferRead:s.bufferSize]) + s.bufferRead += bn + return bn, s.lastErr + } else if !s.sniffing && s.buffer.Cap() != 0 { + s.buffer = bytes.Buffer{} + s.conn.reader = s.conn.Conn // 重置到直接从Conn读取,减少判断 + } + + sn, sErr := s.source.Read(p) + if sn > 0 && s.sniffing { + s.lastErr = sErr + if wn, wErr := s.buffer.Write(p[:sn]); wErr != nil { + return wn, wErr + } + } + return sn, sErr +} + +// Reset resets the buffer. +func (s *sniffer) reset(snif bool) { + s.sniffing = snif + s.bufferRead = 0 + s.bufferSize = s.buffer.Len() +} diff --git a/network/socket/listener/listener_test.go b/network/socket/listener/listener_test.go new file mode 100755 index 0000000..b0c912e --- /dev/null +++ b/network/socket/listener/listener_test.go @@ -0,0 +1,329 @@ +/********************************************************************************** +* Copyright (c) 2009-2017 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +************************************************************************************/ + +package listener + +import ( + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "net/rpc" + "runtime" + "sort" + "strings" + "sync" + "testing" + "time" +) + +const ( + testHTTP1Resp = "http1" + rpcVal = 1234 +) + +func safeServe(errCh chan<- error, muxl *Listener) { + if err := muxl.Serve(); !strings.Contains(err.Error(), "use of closed") { + errCh <- err + } +} + +func safeDial(t *testing.T, addr net.Addr) (*rpc.Client, func()) { + c, err := rpc.Dial(addr.Network(), addr.String()) + if err != nil { + t.Fatal(err) + } + return c, func() { + if err := c.Close(); err != nil { + t.Fatal(err) + } + } +} + +func testListener(t *testing.T) (*Listener, func()) { + l, err := New(":0", nil) + if err != nil { + t.Fatal(err) + } + + var once sync.Once + return l, func() { + once.Do(func() { + if err := l.Close(); err != nil { + t.Fatal(err) + } + }) + } +} + +type testHTTP1Handler struct{} + +func (h *testHTTP1Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, testHTTP1Resp) +} + +func runTestHTTPServer(errCh chan<- error, l net.Listener) { + var mu sync.Mutex + conns := make(map[net.Conn]struct{}) + + defer func() { + mu.Lock() + for c := range conns { + if err := c.Close(); err != nil { + errCh <- err + } + } + mu.Unlock() + }() + + s := &http.Server{ + Handler: &testHTTP1Handler{}, + ConnState: func(c net.Conn, state http.ConnState) { + mu.Lock() + switch state { + case http.StateNew: + conns[c] = struct{}{} + case http.StateClosed: + delete(conns, c) + } + mu.Unlock() + }, + } + if err := s.Serve(l); err != ErrListenerClosed { + errCh <- err + } +} + +func runTestHTTP1Client(t *testing.T, addr net.Addr) { + r, err := http.Get("http://" + addr.String()) + if err != nil { + t.Fatal(err) + } + + defer func() { + if err = r.Body.Close(); err != nil { + t.Fatal(err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + if string(b) != testHTTP1Resp { + t.Fatalf("invalid response: want=%s got=%s", testHTTP1Resp, b) + } +} + +type TestRPCRcvr struct{} + +func (r TestRPCRcvr) Test(i int, j *int) error { + *j = i + return nil +} + +func runTestRPCServer(errCh chan<- error, l net.Listener) { + s := rpc.NewServer() + if err := s.Register(TestRPCRcvr{}); err != nil { + errCh <- err + } + for { + c, err := l.Accept() + if err != nil { + if err != ErrListenerClosed { + errCh <- err + } + return + } + go s.ServeConn(c) + } +} + +func runTestRPCClient(t *testing.T, addr net.Addr) { + c, cleanup := safeDial(t, addr) + defer cleanup() + + var num int + if err := c.Call("TestRPCRcvr.Test", rpcVal, &num); err != nil { + t.Fatal(err) + } + + if num != rpcVal { + t.Errorf("wrong rpc response: want=%d got=%v", rpcVal, num) + } +} + +const ( + handleHTTP1Close = 1 + handleHTTP1Request = 2 + handleAnyClose = 3 + handleAnyRequest = 4 +) + +func TestTimeout(t *testing.T) { + defer leakCheck(t)() + m, Close := testListener(t) + defer Close() + result := make(chan int, 5) + testDuration := time.Millisecond * 100 + m.SetReadTimeout(testDuration) + http1 := m.Match(MatchHTTP()) + any := m.Match(MatchAny()) + go func() { + _ = m.Serve() + }() + go func() { + con, err := http1.Accept() + if err != nil { + result <- handleHTTP1Close + } else { + _, _ = con.Write([]byte("http1")) + _ = con.Close() + result <- handleHTTP1Request + } + }() + go func() { + con, err := any.Accept() + if err != nil { + result <- handleAnyClose + } else { + _, _ = con.Write([]byte("any")) + _ = con.Close() + result <- handleAnyRequest + } + }() + + time.Sleep(testDuration) // wait to prevent timeouts on slow test-runners + client, err := net.Dial("tcp", m.Addr().String()) + if err != nil { + log.Fatal("testTimeout client failed: ", err) + } + defer func() { + _ = client.Close() + }() + time.Sleep(testDuration / 2) + if len(result) != 0 { + log.Print("tcp ") + t.Fatal("testTimeout failed: accepted to fast: ", len(result)) + } + _ = client.SetReadDeadline(time.Now().Add(testDuration * 3)) + buffer := make([]byte, 10) + rl, err := client.Read(buffer) + if err != nil { + t.Fatal("testTimeout failed: client error: ", err, rl) + } + Close() + if rl != 3 { + log.Print("testTimeout failed: response from wrong service ", rl) + } + if string(buffer[0:3]) != "any" { + log.Print("testTimeout failed: response from wrong service ") + } + time.Sleep(testDuration * 2) + if len(result) != 2 { + t.Fatal("testTimeout failed: accepted to less: ", len(result)) + } + if a := <-result; a != handleAnyRequest { + t.Fatal("testTimeout failed: any rule did not match") + } + if a := <-result; a != handleHTTP1Close { + t.Fatal("testTimeout failed: no close an http rule") + } +} + +func TestAny(t *testing.T) { + defer leakCheck(t)() + errCh := make(chan error) + defer func() { + select { + case err := <-errCh: + t.Fatal(err) + default: + } + }() + muxl, cleanup := testListener(t) + defer cleanup() + + httpl := muxl.Match(MatchAny()) + + go runTestHTTPServer(errCh, httpl) + go safeServe(errCh, muxl) + + runTestHTTP1Client(t, muxl.Addr()) +} + +// interestingGoroutines returns all goroutines we care about for the purpose +// of leak checking. It excludes testing or runtime ones. +func interestingGoroutines() (gs []string) { + buf := make([]byte, 2<<20) + buf = buf[:runtime.Stack(buf, true)] + for _, g := range strings.Split(string(buf), "\n\n") { + sl := strings.SplitN(g, "\n", 2) + if len(sl) != 2 { + continue + } + stack := strings.TrimSpace(sl[1]) + if strings.HasPrefix(stack, "testing.RunTests") { + continue + } + + if stack == "" || + strings.Contains(stack, "main.main()") || + strings.Contains(stack, "testing.Main(") || + strings.Contains(stack, "runtime.goexit") || + strings.Contains(stack, "created by runtime.gc") || + strings.Contains(stack, "interestingGoroutines") || + strings.Contains(stack, "runtime.MHeap_Scavenger") { + continue + } + gs = append(gs, g) + } + sort.Strings(gs) + return +} + +// leakCheck snapshots the currently-running goroutines and returns a +// function to be run at the end of tests to see whether any +// goroutines leaked. +func leakCheck(t testing.TB) func() { + orig := map[string]bool{} + for _, g := range interestingGoroutines() { + orig[g] = true + } + return func() { + // Loop, waiting for goroutines to shut down. + // Wait up to 5 seconds, but finish as quickly as possible. + deadline := time.Now().Add(5 * time.Second) + for { + var leaked []string + for _, g := range interestingGoroutines() { + if !orig[g] { + leaked = append(leaked, g) + } + } + if len(leaked) == 0 { + return + } + if time.Now().Before(deadline) { + time.Sleep(50 * time.Millisecond) + continue + } + for _, g := range leaked { + t.Errorf("Leaked goroutine: %v", g) + } + return + } + } +} diff --git a/network/socket/listener/matcher.go b/network/socket/listener/matcher.go new file mode 100755 index 0000000..26bf29e --- /dev/null +++ b/network/socket/listener/matcher.go @@ -0,0 +1,221 @@ +/********************************************************************************** +* Copyright (c) 2009-2017 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +* +* This file was originally developed by The CMux Authors and released under Apache +* License, Version 2.0 in 2016. +************************************************************************************/ + +package listener + +import ( + "bytes" + "io" +) + +var defaultHTTPMethods = []string{ + "OPTIONS", + "GET", + "HEAD", + "POST", + "PATCH", + "PUT", + "DELETE", + "TRACE", + "CONNECT", +} + +// ------------------------------------------------------------------------------------ + +// MatchAny matches any connection. +func MatchAny() Matcher { + return func(r io.Reader) bool { return true } +} + +// MatchPrefix returns a matcher that matches a connection if it +// starts with any of the strings in strs. +func MatchPrefix(strs ...string) Matcher { + pt := newPatriciaTreeString(strs...) + return pt.matchPrefix +} + +// MatchHTTP only matches the methods in the HTTP request. +func MatchHTTP(extMethods ...string) Matcher { + return MatchPrefix(append(defaultHTTPMethods, extMethods...)...) +} + +// MatchPrefixBytes 匹配前缀字节数组 +func MatchPrefixBytes(bs ...[]byte) Matcher { + pt := newPatriciaTree(bs...) + return pt.matchPrefix +} + +// ------------------------------------------------------------------------------------ + +// patriciaTree is a simple patricia tree that handles []byte instead of string +// and cannot be changed after instantiation. +type patriciaTree struct { + root *ptNode + maxDepth int // max depth of the tree. +} + +func newPatriciaTree(bs ...[]byte) *patriciaTree { + max := 0 + for _, b := range bs { + if max < len(b) { + max = len(b) + } + } + return &patriciaTree{ + root: newNode(bs), + maxDepth: max + 1, + } +} + +func newPatriciaTreeString(strs ...string) *patriciaTree { + b := make([][]byte, len(strs)) + for i, s := range strs { + b[i] = []byte(s) + } + return newPatriciaTree(b...) +} + +func (t *patriciaTree) matchPrefix(r io.Reader) bool { + buf := make([]byte, t.maxDepth) + n, _ := io.ReadFull(r, buf) + return t.root.match(buf[:n], true) +} + +func (t *patriciaTree) match(r io.Reader) bool { + buf := make([]byte, t.maxDepth) + n, _ := io.ReadFull(r, buf) + return t.root.match(buf[:n], false) +} + +type ptNode struct { + prefix []byte + next map[byte]*ptNode + terminal bool +} + +func newNode(strs [][]byte) *ptNode { + if len(strs) == 0 { + return &ptNode{ + prefix: []byte{}, + terminal: true, + } + } + + if len(strs) == 1 { + return &ptNode{ + prefix: strs[0], + terminal: true, + } + } + + p, strs := splitPrefix(strs) + n := &ptNode{ + prefix: p, + } + + nexts := make(map[byte][][]byte) + for _, s := range strs { + if len(s) == 0 { + n.terminal = true + continue + } + nexts[s[0]] = append(nexts[s[0]], s[1:]) + } + + n.next = make(map[byte]*ptNode) + for first, rests := range nexts { + n.next[first] = newNode(rests) + } + + return n +} + +func splitPrefix(bss [][]byte) (prefix []byte, rest [][]byte) { + if len(bss) == 0 || len(bss[0]) == 0 { + return prefix, bss + } + + if len(bss) == 1 { + return bss[0], [][]byte{{}} + } + + for i := 0; ; i++ { + var cur byte + eq := true + for j, b := range bss { + if len(b) <= i { + eq = false + break + } + + if j == 0 { + cur = b[i] + continue + } + + if cur != b[i] { + eq = false + break + } + } + + if !eq { + break + } + + prefix = append(prefix, cur) + } + + rest = make([][]byte, 0, len(bss)) + for _, b := range bss { + rest = append(rest, b[len(prefix):]) + } + + return prefix, rest +} + +func (n *ptNode) match(b []byte, prefix bool) bool { + l := len(n.prefix) + if l > 0 { + if l > len(b) { + l = len(b) + } + if !bytes.Equal(b[:l], n.prefix) { + return false + } + } + + if n.terminal && (prefix || len(n.prefix) == len(b)) { + return true + } + + if l >= len(b) { + return false + } + + nextN, ok := n.next[b[l]] + if !ok { + return false + } + + if l == len(b) { + b = b[l:l] + } else { + b = b[l+1:] + } + return nextN.match(b, prefix) +} diff --git a/network/socket/listener/matcher_test.go b/network/socket/listener/matcher_test.go new file mode 100755 index 0000000..577788a --- /dev/null +++ b/network/socket/listener/matcher_test.go @@ -0,0 +1,59 @@ +/********************************************************************************** +* Copyright (c) 2009-2017 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +* +* This file was originally developed by The CMux Authors and released under Apache +* License, Version 2.0 in 2016. +************************************************************************************/ + +package listener + +import ( + "strings" + "testing" +) + +func testPTree(t *testing.T, strs ...string) { + pt := newPatriciaTreeString(strs...) + for _, s := range strs { + if !pt.match(strings.NewReader(s)) { + t.Errorf("%s is not matched by %s", s, s) + } + + if !pt.matchPrefix(strings.NewReader(s + s)) { + t.Errorf("%s is not matched as a prefix by %s", s+s, s) + } + + if pt.match(strings.NewReader(s + s)) { + t.Errorf("%s matches %s", s+s, s) + } + + // The following tests are just to catch index out of + // range and off-by-one errors and not the functionality. + pt.matchPrefix(strings.NewReader(s[:len(s)-1])) + pt.match(strings.NewReader(s[:len(s)-1])) + pt.matchPrefix(strings.NewReader(s + "$")) + pt.match(strings.NewReader(s + "$")) + } +} + +func TestPatriciaOnePrefix(t *testing.T) { + testPTree(t, "prefix") +} + +func TestPatriciaNonOverlapping(t *testing.T) { + testPTree(t, "foo", "bar", "dummy") +} + +func TestPatriciaOverlapping(t *testing.T) { + testPTree(t, "foo", "far", "farther", "boo", "ba", "bar") +} diff --git a/network/websocket/websocket.go b/network/websocket/websocket.go new file mode 100755 index 0000000..8cfc168 --- /dev/null +++ b/network/websocket/websocket.go @@ -0,0 +1,239 @@ +/********************************************************************************** +* Copyright (c) 2009-2017 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +************************************************************************************/ +// +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "io" + "net" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +// Conn websocket连接 +type Conn interface { + net.Conn + Subprotocol() string // 获取子协议 + TextTransport() Conn // 获取文本传输通道 + Path() string // 接入时的ws后的路径 + Username() string // 接入是http验证后的用户名称 +} + +type websocketConn interface { + NextReader() (messageType int, r io.Reader, err error) + NextWriter(messageType int) (io.WriteCloser, error) + Close() error + LocalAddr() net.Addr + RemoteAddr() net.Addr + SetReadDeadline(t time.Time) error + SetWriteDeadline(t time.Time) error + Subprotocol() string +} + +// websocketConn represents a websocket connection. +type websocketTransport struct { + sync.Mutex + socket websocketConn + reader io.Reader + closing chan bool + path string + username string +} + +const ( + writeWait = 10 * time.Second // Time allowed to write a message to the peer. + pongWait = 60 * time.Second // Time allowed to read the next pong message from the peer. + pingPeriod = (pongWait * 9) / 10 // Send pings to peer with this period. Must be less than pongWait. + closeGracePeriod = 10 * time.Second // Time to wait before force close on connection. +) + +// The default upgrader to use +var upgrader = &websocket.Upgrader{ + Subprotocols: []string{"rtsp", "control", "data"}, + CheckOrigin: func(r *http.Request) bool { return true }, + // ReadBufferSize: 64 * 1024, WriteBufferSize: 64 * 1024, +} + +// TryUpgrade attempts to upgrade an HTTP request to rtsp/wsp over websocket. +func TryUpgrade(w http.ResponseWriter, r *http.Request, path, username string) (Conn, bool) { + if w == nil || r == nil { + return nil, false + } + + if ws, err := upgrader.Upgrade(w, r, nil); err == nil { + return newConn(ws, path, username), true + } + + return nil, false +} + +// newConn creates a new transport from websocket. +func newConn(ws websocketConn, path, username string) Conn { + conn := &websocketTransport{ + socket: ws, + closing: make(chan bool), + path: path, + username: username, + } + + /*ws.SetReadLimit(maxMessageSize) + ws.SetReadDeadline(time.Now().Add(pongWait)) + ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + + ws.SetCloseHandler(func(code int, text string) error { + return conn.Close() + }) + + utils.Repeat(func() { + log.Println("ping") + if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil { + log.Println("ping:", err) + } + }, pingPeriod, conn.closing)*/ + + return conn +} + +// Read reads data from the connection. It is possible to allow reader to time +// out and return a Error with Timeout() == true after a fixed time limit by +// using SetDeadline and SetReadDeadline on the websocket. +func (c *websocketTransport) Read(b []byte) (n int, err error) { + var opCode int + if c.reader == nil { + // New message + var r io.Reader + for { + if opCode, r, err = c.socket.NextReader(); err != nil { + return + } + + if opCode != websocket.BinaryMessage && opCode != websocket.TextMessage { + continue + } + + c.reader = r + break + } + } + + // Read from the reader + n, err = c.reader.Read(b) + if err != nil { + if err == io.EOF { + c.reader = nil + err = nil + } + } + return +} + +// Write writes data to the connection. It is possible to allow writer to time +// out and return a Error with Timeout() == true after a fixed time limit by +// using SetDeadline and SetWriteDeadline on the websocket. +func (c *websocketTransport) Write(b []byte) (n int, err error) { + // Serialize write to avoid concurrent write + c.Lock() + defer c.Unlock() + + var w io.WriteCloser + if w, err = c.socket.NextWriter(websocket.BinaryMessage); err == nil { + if n, err = w.Write(b); err == nil { + err = w.Close() + } + } + return +} + +// Close terminates the connection. +func (c *websocketTransport) Close() error { + return c.socket.Close() +} + +// LocalAddr returns the local network address. +func (c *websocketTransport) LocalAddr() net.Addr { + return c.socket.LocalAddr() +} + +// RemoteAddr returns the remote network address. +func (c *websocketTransport) RemoteAddr() net.Addr { + return c.socket.RemoteAddr() +} + +// SetDeadline sets the read and write deadlines associated +// with the connection. It is equivalent to calling both +// SetReadDeadline and SetWriteDeadline. +func (c *websocketTransport) SetDeadline(t time.Time) (err error) { + if err = c.socket.SetReadDeadline(t); err == nil { + err = c.socket.SetWriteDeadline(t) + } + return +} + +// SetReadDeadline sets the deadline for future Read calls +// and any currently-blocked Read call. +func (c *websocketTransport) SetReadDeadline(t time.Time) error { + return c.socket.SetReadDeadline(t) +} + +// SetWriteDeadline sets the deadline for future Write calls +// and any currently-blocked Write call. +func (c *websocketTransport) SetWriteDeadline(t time.Time) error { + return c.socket.SetWriteDeadline(t) +} + +// Subprotocol 获取子协议名称 +func (c *websocketTransport) Subprotocol() string { + return c.socket.Subprotocol() +} + +// TextTransport 获取文本传输Conn +func (c *websocketTransport) TextTransport() Conn { + return &websocketTextTransport{c} +} + +func (c *websocketTransport) Path() string { + return c.path +} + +func (c *websocketTransport) Username() string { + return c.username +} + +type websocketTextTransport struct { + *websocketTransport +} + +// Write writes data to the connection. It is possible to allow writer to time +// out and return a Error with Timeout() == true after a fixed time limit by +// using SetDeadline and SetWriteDeadline on the websocket. +func (c *websocketTextTransport) Write(b []byte) (n int, err error) { + // Serialize write to avoid concurrent write + c.Lock() + defer c.Unlock() + + var w io.WriteCloser + if w, err = c.socket.NextWriter(websocket.TextMessage); err == nil { + if n, err = w.Write(b); err == nil { + err = w.Close() + } + } + return +} diff --git a/network/websocket/websocket_test.go b/network/websocket/websocket_test.go new file mode 100755 index 0000000..52d8e7f --- /dev/null +++ b/network/websocket/websocket_test.go @@ -0,0 +1,135 @@ +package websocket + +import ( + "bytes" + "io" + "net" + "net/http/httptest" + "testing" + "time" + + "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" +) + +type writer bytes.Buffer + +func (w *writer) Close() error { return nil } +func (w *writer) Write(data []byte) (n int, err error) { return ((*bytes.Buffer)(w)).Write(data) } + +type conn struct { + read []byte + write *writer +} + +func (c *conn) NextReader() (messageType int, r io.Reader, err error) { + messageType = websocket.BinaryMessage + r = bytes.NewBuffer(c.read) + if c.read == nil { + err = io.EOF + } + return +} + +func (c *conn) NextWriter(messageType int) (w io.WriteCloser, err error) { + w = c.write + if c.write == nil { + err = io.EOF + } + + return +} +func (c *conn) Close() error { return nil } +func (c *conn) LocalAddr() net.Addr { return &net.IPAddr{} } +func (c *conn) RemoteAddr() net.Addr { return &net.IPAddr{} } +func (c *conn) SetReadDeadline(t time.Time) error { return nil } +func (c *conn) SetWriteDeadline(t time.Time) error { return nil } +func (c *conn) Subprotocol() string { return "" } +func TestTryUpgradeNil(t *testing.T) { + _, ok := TryUpgrade(nil, nil, "", "") + assert.Equal(t, false, ok) +} + +func TestTryUpgrade(t *testing.T) { + //httptest.NewServer(handler) + r := httptest.NewRequest("GET", "http://127.0.0.1/", bytes.NewBuffer([]byte{})) + r.Header.Set("Connection", "upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Extensions", "permessage-deflate; client_max_window_bits") + r.Header.Set("Sec-WebSocket-Key", "D1icfJz+khA9kj5/14dRXQ==") + r.Header.Set("Sec-WebSocket-Protocol", "mqttv3.1") + r.Header.Set("Sec-WebSocket-Version", "13") + + w := httptest.NewRecorder() + + assert.NotPanics(t, func() { + TryUpgrade(w, r, "", "") + }) + + // TODO: need to have a hijackable response writer to test properly + //ws, ok := TryUpgrade(w, r) + //assert.NotNil(t, ws) + //assert.True(t, ok) +} + +func TestRead_EOF(t *testing.T) { + c := newConn(new(conn), "", "") + + _, err := c.Read([]byte{}) + assert.Error(t, io.EOF, err) +} + +func TestRead(t *testing.T) { + message := []byte("hello world") + c := &websocketTransport{ + socket: &conn{ + read: message, + }, + closing: make(chan bool), + } + + buffer := make([]byte, 64) + n, err := c.Read(buffer) + assert.NoError(t, err) + assert.Equal(t, message, buffer[:n]) +} + +func TestWrite(t *testing.T) { + message := []byte("hello world") + buffer := new(bytes.Buffer) + c := &websocketTransport{ + socket: &conn{ + write: (*writer)(buffer), + }, + closing: make(chan bool), + } + + _, err := c.Write(message) + assert.NoError(t, err) + assert.Equal(t, message, buffer.Bytes()) +} + +func TestMisc(t *testing.T) { + c := &websocketTransport{ + socket: &conn{}, + closing: make(chan bool), + } + + err := c.Close() + assert.NoError(t, err) + + err = c.SetDeadline(time.Now()) + assert.NoError(t, err) + + err = c.SetReadDeadline(time.Now()) + assert.NoError(t, err) + + err = c.SetWriteDeadline(time.Now()) + assert.NoError(t, err) + + addr1 := c.LocalAddr() + assert.Equal(t, "", addr1.String()) + + addr2 := c.RemoteAddr() + assert.Equal(t, "", addr2.String()) +} diff --git a/provider/auth/json.go b/provider/auth/json.go new file mode 100755 index 0000000..8c137a1 --- /dev/null +++ b/provider/auth/json.go @@ -0,0 +1,77 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package auth + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/cnotch/tomatox/utils" +) + +// JSON json 提供者 +var JSON = &jsonProvider{} + +type jsonProvider struct { + filePath string +} + +func (p *jsonProvider) Name() string { + return "json" +} + +func (p *jsonProvider) Configure(config map[string]interface{}) error { + path, ok := config["file"] + if ok { + switch v := path.(type) { + case string: + p.filePath = v + default: + return fmt.Errorf("invalid user config, file attr: %v", path) + } + } else { + p.filePath = "users.json" + } + + if !filepath.IsAbs(p.filePath) { + exe, err := os.Executable() + if err != nil { + return err + } + p.filePath = filepath.Join(filepath.Dir(exe), p.filePath) + } + + return nil +} + +func (p *jsonProvider) LoadAll() ([]*User, error) { + path := p.filePath + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + + // 从文件读 + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + var users []*User + if err := json.Unmarshal(b, &users); err != nil { + return nil, err + } + + return users, nil +} + +func (p *jsonProvider) Flush(full []*User, saves []*User, removes []*User) error { + return utils.EncodeJSONFile(p.filePath, full) +} diff --git a/provider/auth/manager.go b/provider/auth/manager.go new file mode 100755 index 0000000..dc84cc2 --- /dev/null +++ b/provider/auth/manager.go @@ -0,0 +1,205 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package auth + +import ( + "strings" + "sync" + + "github.com/cnotch/xlog" +) + +var globalM = &manager{ + m: make(map[string]*User), +} + +func init() { + // 默认为内存提供者,避免没有初始化全局函数调用问题 + globalM.Reset(&memProvider{}) +} + +// Reset 重置用户提供者 +func Reset(provider UserProvider) { + globalM.Reset(provider) +} + +// All 获取所有的用户 +func All() []*User { + return globalM.All() +} + +// Get 获取取指定名称的用户 +func Get(userName string) *User { + return globalM.Get(userName) +} + +// Del 删除指定名称的用户 +func Del(userName string) error { + return globalM.Del(userName) +} + +// Save 保存用户 +func Save(src *User, updatePassword bool) error { + return globalM.Save(src, updatePassword) +} + +// Flush 刷新用户 +func Flush() error { + return globalM.Flush() +} + +type manager struct { + lock sync.RWMutex + m map[string]*User // 用户map + l []*User // 用户list + + saves []*User // 自上次Flush后新的保存和删除的用户 + removes []*User + + provider UserProvider +} + +func (m *manager) Reset(provider UserProvider) { + m.lock.Lock() + defer m.lock.Unlock() + + m.m = make(map[string]*User) + m.l = m.l[:0] + m.saves = m.saves[:0] + m.removes = m.removes[:0] + m.provider = provider + + users, err := provider.LoadAll() + if err != nil { + panic("Load user fail") + } + + if cap(m.l) < len(users) { + m.l = make([]*User, 0, len(users)) + } + + // 加入缓存 + for _, u := range users { + if err := u.init(); err != nil { + xlog.Warnf("user table init failed: `%v`", err) + continue // 忽略错误的配置 + } + m.m[u.Name] = u + m.l = append(m.l, u) + } +} + +func (m *manager) Get(userName string) *User { + m.lock.RLock() + defer m.lock.RUnlock() + + userName = strings.ToLower(userName) + u, ok := m.m[userName] + if ok { + return u + } + return nil +} + +func (m *manager) Del(userName string) error { + m.lock.Lock() + defer m.lock.Unlock() + + userName = strings.ToLower(userName) + u, ok := m.m[userName] + + if ok { + delete(m.m, userName) + + // 从完整列表中删除 + for i, u2 := range m.l { + if u.Name == u2.Name { + m.l = append(m.l[:i], m.l[i+1:]...) + break + } + } + + // 从保存列表中删除 + for i, u2 := range m.saves { + if u.Name == u2.Name { + m.saves = append(m.saves[:i], m.saves[i+1:]...) + break + } + } + + m.removes = append(m.removes, u) + } + return nil +} + +func (m *manager) Save(newu *User, updatePassword bool) error { + m.lock.Lock() + defer m.lock.Unlock() + + err := newu.init() + if err != nil { + return err + } + + u, ok := m.m[newu.Name] + + if ok { // 更新 + u.CopyFrom(newu, updatePassword) + + save := true + // 如果保存列表存在,不新增 + for _, u2 := range m.saves { + if u.Name == u2.Name { + save = false + break + } + } + + if save { + m.saves = append(m.saves, u) + } + } else { // 新增 + u = newu + m.m[u.Name] = u + + m.l = append(m.l, u) + m.saves = append(m.saves, u) + + for i, u2 := range m.removes { + if u.Name == u2.Name { + m.removes = append(m.removes[:i], m.removes[i+1:]...) + break + } + } + } + return nil +} + +func (m *manager) Flush() error { + m.lock.Lock() + defer m.lock.Unlock() + + if len(m.saves)+len(m.removes) == 0 { + return nil + } + + err := m.provider.Flush(m.l, m.saves, m.removes) + if err != nil { + return err + } + + m.saves = m.saves[:0] + m.removes = m.removes[:0] + return nil +} + +func (m *manager) All() []*User { + m.lock.RLock() + defer m.lock.RUnlock() + + users := make([]*User, len(m.l)) + copy(users, m.l) + return users +} diff --git a/provider/auth/memory.go b/provider/auth/memory.go new file mode 100755 index 0000000..42bd628 --- /dev/null +++ b/provider/auth/memory.go @@ -0,0 +1,28 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package auth + +type memProvider struct { +} + +func (p *memProvider) Name() string { + return "memory" +} + +func (p *memProvider) Configure(config map[string]interface{}) error { + return nil +} + +func (p *memProvider) LoadAll() ([]*User, error) { + return []*User{{ + Name: "admin", + Password: "admin", + Admin: true, + }}, nil +} + +func (p *memProvider) Flush(full []*User, saves []*User, removes []*User) error { + return nil +} diff --git a/provider/auth/mode.go b/provider/auth/mode.go new file mode 100755 index 0000000..04a4b78 --- /dev/null +++ b/provider/auth/mode.go @@ -0,0 +1,78 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package auth + +import ( + "bytes" + "errors" + "fmt" +) + +// Mode 认证模式 +type Mode int + +// 认证模式常量 +const ( + NoneAuth Mode = iota + BasicAuth + DigestAuth +) + +var errUnmarshalNilMode = errors.New("can't unmarshal a nil *Mode") + +// String 返回认证模式字串 +func (m Mode) String() string { + switch m { + case NoneAuth: + return "NONE" + case BasicAuth: + return "BASIC" + case DigestAuth: + return "DIGEST" + default: + return fmt.Sprintf("AuthMode(%d)", m) + } +} + +// MarshalText 编入认证模式到文本 +func (m Mode) MarshalText() ([]byte, error) { + return []byte(m.String()), nil +} + +// UnmarshalText 从文本编出认证模式 +// 典型的用于 YAML、TOML、JSON等文件编出 +func (m *Mode) UnmarshalText(text []byte) error { + if m == nil { + return errUnmarshalNilMode + } + if !m.unmarshalText(text) && !m.unmarshalText(bytes.ToLower(text)) { + return fmt.Errorf("unrecognized Mode: %q", text) + } + return nil +} + +func (m *Mode) unmarshalText(text []byte) bool { + switch string(text) { + case "none", "NONE", "": // make the zero value useful + *m = NoneAuth + case "basic", "BASIC": + *m = BasicAuth + case "digest", "DIGEST": + *m = DigestAuth + default: + return false + } + return true +} + +// Set flag.Value 接口实现. +func (m *Mode) Set(s string) error { + return m.UnmarshalText([]byte(s)) +} + +// Get flag.Getter 接口实现 +func (m *Mode) Get() interface{} { + return *m +} diff --git a/provider/auth/path_matcher.go b/provider/auth/path_matcher.go new file mode 100755 index 0000000..aa2682c --- /dev/null +++ b/provider/auth/path_matcher.go @@ -0,0 +1,91 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package auth + +import ( + "strings" + "unicode" + + "github.com/cnotch/tomatox/utils/scan" +) + +const ( + sectionWildcard = "+" // 单段通配符 + endWildcard = "*" // 0-n段通配符,必须位于结尾 +) + +// 行分割 +var pathScanner = scan.NewScanner('/', unicode.IsSpace) + +// PathMatcher 路径匹配接口 +type PathMatcher interface { + Match(path string) bool +} + +// NewPathMatcher 创建匹配器 +func NewPathMatcher(pathMask string) PathMatcher { + if strings.TrimSpace(pathMask) == endWildcard { + return alwaysMatcher{} + } + + parts := strings.Split(strings.ToLower(strings.Trim(pathMask, "/")), "/") + wildcard := parts[len(parts)-1] == endWildcard + if wildcard { + parts = parts[0 : len(parts)-1] + } + return &pathMacher{parts: parts, wildcardEnd: wildcard} +} + +type alwaysMatcher struct { +} + +func (m alwaysMatcher) Match(path string) bool { + return true +} + +type pathMacher struct { + parts []string + wildcardEnd bool +} + +func (m *pathMacher) Match(path string) bool { + path = strings.ToLower(strings.Trim(path, "/")) + count := partCount(path) + 1 + + if count < len(m.parts) { + return false + } + + if count > len(m.parts) && !m.wildcardEnd { + return false + } + + ok := true + advance := path + token := "" + for i := 0; i < len(m.parts) && ok; i++ { + advance, token, ok = pathScanner.Scan(advance) + if sectionWildcard == m.parts[i] { + continue // 跳过 + } + if token != m.parts[i] { + return false + } + } + + return true +} + +func partCount(s string) int { + n := 0 + for { + i := strings.IndexByte(s, '/') + if i == -1 { + return n + } + n++ + s = s[i+1:] + } +} diff --git a/provider/auth/path_matcher_test.go b/provider/auth/path_matcher_test.go new file mode 100755 index 0000000..74fc747 --- /dev/null +++ b/provider/auth/path_matcher_test.go @@ -0,0 +1,55 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package auth + +import "testing" + +func Test_alwaysMatcher_Match(t *testing.T) { + tests := []struct { + name string + path string + want bool + }{ + {"always", "/a/b", true}, + {"always", "/a", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := NewPathMatcher(endWildcard) + if got := m.Match(tt.path); got != tt.want { + t.Errorf("alwaysMatcher.Match() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_pathMacher_Match(t *testing.T) { + tests := []struct { + name string + pathMask string + path string + want bool + }{ + {"g1", "/a", "/a", true}, + {"g2", "/a", "/a/b", false}, + {"e1", "/a/*", "/a", true}, + {"e2", "/a/*", "/a/b", true}, + {"e3", "/a/*", "/a/b/c", true}, + {"e4", "/a/*", "/b", false}, + {"c1", "/a/+/c/*", "/a/b/c", true}, + {"c2", "/a/+/c/*", "/a/d/c", true}, + {"c3", "/a/+/c/*", "/a/b/c/d", true}, + {"c4", "/a/+/c/*", "/a/b/c/d/e", true}, + {"c5", "/a/+/c/*", "/a/c/d/e", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := NewPathMatcher(tt.pathMask) + if got := m.Match(tt.path); got != tt.want { + t.Errorf("pathMacher.Match() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/provider/auth/token.go b/provider/auth/token.go new file mode 100755 index 0000000..c5e3f3c --- /dev/null +++ b/provider/auth/token.go @@ -0,0 +1,87 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package auth + +import ( + "sync" + "time" + + "github.com/cnotch/tomatox/provider/security" +) + +// Token 用户登录后的Token +type Token struct { + Username string `json:"-"` + AToken string `json:"access_token"` + AExp int64 `json:"-"` + RToken string `json:"refresh_token"` + RExp int64 `json:"-"` +} + +// TokenManager token管理 +type TokenManager struct { + tokens sync.Map // token->Token +} + +// NewToken 给用户新建Token +func (tm *TokenManager) NewToken(username string) *Token { + token := &Token{ + Username: username, + AToken: security.NewID().MD5(), + AExp: time.Now().Add(time.Hour * time.Duration(2)).Unix(), + RToken: security.NewID().MD5(), + RExp: time.Now().Add(time.Hour * time.Duration(7*24)).Unix(), + } + + tm.tokens.Store(token.AToken, token) + tm.tokens.Store(token.RToken, token) + return token +} + +// Refresh 刷新指定的Token +func (tm *TokenManager) Refresh(rtoken string) *Token { + ti, ok := tm.tokens.Load(rtoken) + if ok { + oldToken := ti.(*Token) + username := oldToken.Username + if rtoken == oldToken.RToken { // 是refresh token + tm.tokens.Delete(oldToken.AToken) + tm.tokens.Delete(oldToken.RToken) + if oldToken.RExp > time.Now().Unix() { + return tm.NewToken(username) + } + } + } + return nil +} + +// AccessCheck 访问检测 +func (tm *TokenManager) AccessCheck(atoken string) string { + ti, ok := tm.tokens.Load(atoken) + if ok { + token := ti.(*Token) + if token.AToken == atoken { // 访问token + if token.AExp > time.Now().Unix() { + return token.Username + } + tm.tokens.Delete(token.AToken) + } + } + return "" +} + +// ExpCheck 过期检测 +func (tm *TokenManager) ExpCheck() { + tm.tokens.Range(func(k, v interface{}) bool { + token := v.(*Token) + if time.Now().Unix() > token.AExp { + tm.tokens.Delete(token.AToken) + } + if time.Now().Unix() > token.RExp { + tm.tokens.Delete(token.RToken) + } + return true + }) +} diff --git a/provider/auth/user.go b/provider/auth/user.go new file mode 100755 index 0000000..9cbd609 --- /dev/null +++ b/provider/auth/user.go @@ -0,0 +1,141 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package auth + +import ( + "crypto/md5" + "encoding/hex" + "errors" + "strings" + + "github.com/cnotch/tomatox/utils/scan" +) + +// AccessRight 访问权限类型 +type AccessRight int + +// 权限常量 +const ( + PullRight AccessRight = 1 << iota // 拉流权限 + PushRight // 推流权限 +) + +// UserProvider 用户提供者 +type UserProvider interface { + LoadAll() ([]*User, error) + Flush(full []*User, saves []*User, removes []*User) error +} + +// User 用户 +type User struct { + Name string `json:"name"` + Password string `json:"password,omitempty"` + Admin bool `json:"admin,omitempty"` + PushAccess string `json:"push,omitempty"` + PullAccess string `json:"pull,omitempty"` + + pushMatchers []PathMatcher + pullMatchers []PathMatcher +} + +func initMatchers(access string, destMatcher *[]PathMatcher) { + advance := access + pathMask := "" + continueScan := true + for continueScan { + advance, pathMask, continueScan = scan.Semicolon.Scan(advance) + if len(pathMask) == 0 { + continue + } + *destMatcher = append(*destMatcher, NewPathMatcher(pathMask)) + } +} + +func (u *User) init() error { + u.Name = strings.ToLower(u.Name) + if u.Admin { + if len(u.PullAccess) == 0 { + u.PullAccess = "*" + } + if len(u.PushAccess) == 0 { + u.PushAccess = "*" + } + } + + initMatchers(u.PushAccess, &u.pushMatchers) + initMatchers(u.PullAccess, &u.pullMatchers) + return nil +} + +// PasswordMD5 返回口令的MD5字串 +func (u *User) PasswordMD5() string { + if passwordNeedMD5(u.Password) { + pw := md5.Sum([]byte(u.Password)) + return hex.EncodeToString(pw[:]) + } + return u.Password +} + +// ValidatePassword 验证密码 +func (u *User) ValidatePassword(password string) error { + if passwordNeedMD5(password) { + pw := md5.Sum([]byte(password)) + password = hex.EncodeToString(pw[:]) + } + + if strings.EqualFold(u.PasswordMD5(), password) { + return nil + } + return errors.New("password error") +} + +// ValidatePermission 验证权限 +func (u *User) ValidatePermission(path string, right AccessRight) bool { + var matchers []PathMatcher + switch right { + case PushRight: + matchers = u.pushMatchers + case PullRight: + matchers = u.pullMatchers + } + + if matchers == nil { + return false + } + + path = strings.TrimSpace(path) + for _, matcher := range matchers { + if matcher.Match(path) { + return true + } + } + + return false +} + +// CopyFrom 从源属性并初始化 +func (u *User) CopyFrom(src *User, withPassword bool) { + if withPassword { + u.Password = src.Password + } + u.Admin = src.Admin + u.PushAccess = src.PushAccess + u.PullAccess = src.PullAccess + u.init() +} + +// 密码是否需要进行md5处理,如果已经是md5则不处理 +func passwordNeedMD5(password string) bool { + if len(password) != 32 { + return true + } + + _, err := hex.DecodeString(password) + if err != nil { + return true + } + + return false +} diff --git a/provider/auth/user_test.go b/provider/auth/user_test.go new file mode 100755 index 0000000..2377e47 --- /dev/null +++ b/provider/auth/user_test.go @@ -0,0 +1,37 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package auth + +import "testing" + +func TestUser_ValidePermission(t *testing.T) { + u := User{ + Name: "cao", + Password: "ok", + PushAccess: "/a/+/c", + PullAccess: "/a/*", + } + u.init() + + tests := []struct { + name string + path string + right AccessRight + want bool + }{ + {"2", "/a/b/c", PushRight, true}, + {"3", "/a/c", PushRight, false}, + {"4", "/a", PullRight, true}, + {"5", "/a/c", PullRight, true}, + {"6", "/a/c/d", PullRight, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := u.ValidatePermission(tt.path, tt.right); got != tt.want { + t.Errorf("User.ValidePermission() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/provider/route/json.go b/provider/route/json.go new file mode 100755 index 0000000..f6863ea --- /dev/null +++ b/provider/route/json.go @@ -0,0 +1,77 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package route + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/cnotch/tomatox/utils" +) + +// JSON json 提供者 +var JSON = &jsonProvider{} + +type jsonProvider struct { + filePath string +} + +func (p *jsonProvider) Name() string { + return "json" +} + +func (p *jsonProvider) Configure(config map[string]interface{}) error { + path, ok := config["file"] + if ok { + switch v := path.(type) { + case string: + p.filePath = v + default: + return fmt.Errorf("invalid route table config, file attr: %v", path) + } + } else { + p.filePath = "routetable.json" + } + + if !filepath.IsAbs(p.filePath) { + exe, err := os.Executable() + if err != nil { + return err + } + p.filePath = filepath.Join(filepath.Dir(exe), p.filePath) + } + + return nil +} + +func (p *jsonProvider) LoadAll() ([]*Route, error) { + path := p.filePath + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + + // 从文件读 + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + var routes []*Route + if err := json.Unmarshal(b, &routes); err != nil { + return nil, err + } + + return routes, nil +} + +func (p *jsonProvider) Flush(full []*Route, saves []*Route, removes []*Route) error { + return utils.EncodeJSONFile(p.filePath, full) +} diff --git a/provider/route/memory.go b/provider/route/memory.go new file mode 100755 index 0000000..7b974fc --- /dev/null +++ b/provider/route/memory.go @@ -0,0 +1,24 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package route + +type memProvider struct { +} + +func (p *memProvider) Name() string { + return "memory" +} + +func (p *memProvider) Configure(config map[string]interface{}) error { + return nil +} + +func (p *memProvider) LoadAll() ([]*Route, error) { + return nil, nil +} + +func (p *memProvider) Flush(full []*Route, saves []*Route, removes []*Route) error { + return nil +} diff --git a/provider/route/route.go b/provider/route/route.go new file mode 100755 index 0000000..4a0a40c --- /dev/null +++ b/provider/route/route.go @@ -0,0 +1,39 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package route + +import ( + "net/url" + + "github.com/cnotch/tomatox/utils" +) + +// Route 路由 +type Route struct { + Pattern string `json:"pattern"` // 路由模式字串 + URL string `json:"url"` // 目标url + KeepAlive bool `json:"keepalive,omitempty"` // 是否一直保持连接,直到对方断开;默认 false,会在没有人使用时关闭 +} + +func (r *Route) init() error { + r.Pattern = utils.CanonicalPath(r.Pattern) + _, err := url.Parse(r.URL) + if err != nil { + return err + } + return nil +} + +// CopyFrom 从源拷贝 +func (r *Route) CopyFrom(src *Route) { + r.URL = src.URL + r.KeepAlive = src.KeepAlive +} + +// Provider 路由提供者 +type Provider interface { + LoadAll() ([]*Route, error) + Flush(full []*Route, saves []*Route, removes []*Route) error +} diff --git a/provider/route/routetable.go b/provider/route/routetable.go new file mode 100755 index 0000000..f5091e3 --- /dev/null +++ b/provider/route/routetable.go @@ -0,0 +1,261 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package route + +import ( + "sync" + + "github.com/cnotch/tomatox/utils" + "github.com/cnotch/xlog" +) + +var globalT = &routetable{ + m: make(map[string]*Route), +} + +func init() { + // 默认为内存提供者,避免没有初始化全局函数调用问题 + globalT.Reset(&memProvider{}) +} + +// Reset 重置路由表提供者 +func Reset(provider Provider) { + globalT.Reset(provider) +} + +// Match 从路由表中获取和路径匹配的路由实例 +func Match(path string) *Route { + return globalT.Match(path) +} + +// All 获取所有的路由 +func All() []*Route { + return globalT.All() +} + +// Get 获取取指定模式的路由 +func Get(pattern string) *Route { + return globalT.Get(pattern) +} + +// Del 删除指定模式的路由 +func Del(pattern string) error { + return globalT.Del(pattern) +} + +// Save 保存路由 +func Save(src *Route) error { + return globalT.Save(src) +} + +// Flush 刷新路由 +func Flush() error { + return globalT.Flush() +} + +type routetable struct { + lock sync.RWMutex + m map[string]*Route // 路由map + l []*Route // 路由list + + saves []*Route // 自上次Flush后新的保存和删除的路由 + removes []*Route + + provider Provider +} + +func (t *routetable) Reset(provider Provider) { + t.lock.Lock() + defer t.lock.Unlock() + + t.m = make(map[string]*Route) + t.l = t.l[:0] + t.saves = t.saves[:0] + t.removes = t.removes[:0] + t.provider = provider + + routes, err := provider.LoadAll() + if err != nil { + panic("Load route table fail") + } + + if cap(t.l) < len(routes) { + t.l = make([]*Route, 0, len(routes)) + } + + // 加入缓存 + for _, r := range routes { + if err := r.init(); err != nil { + xlog.Warnf("route table init failed: `%v`", err) + continue // 忽略错误的配置 + } + t.m[r.Pattern] = r + t.l = append(t.l, r) + } +} + +func (t *routetable) Match(path string) *Route { + t.lock.RLock() + defer t.lock.RUnlock() + + path = utils.CanonicalPath(path) + if path[len(path)-1] == '/' { // 必须有具体的子路径 + return nil + } + + r, ok := t.m[path] + if ok { // 精确匹配 + ret := *r + return &ret + } + + // 获取最长有效匹配的路由 + var n = 0 + for k, v := range t.m { + if !pathMatch(k, path) { + continue + } + + if r == nil || len(k) > n { + n = len(k) + r = v + } + } + + if r != nil { + ret := *r + r = &ret + if r.URL[len(r.URL)-1] == '/' { + r.URL = r.URL + path[len(r.Pattern):] + } else { + r.URL = r.URL + path[len(r.Pattern)-1:] + } + r.Pattern = path + } + return r +} + +func (t *routetable) Get(pattern string) *Route { + t.lock.RLock() + defer t.lock.RUnlock() + + pattern = utils.CanonicalPath(pattern) + r, _ := t.m[pattern] + return r +} + +func (t *routetable) Del(pattern string) error { + t.lock.Lock() + defer t.lock.Unlock() + + pattern = utils.CanonicalPath(pattern) + r, ok := t.m[pattern] + + if ok { + delete(t.m, pattern) + + // 从完整列表中删除 + for i, r2 := range t.l { + if r.Pattern == r2.Pattern { + t.l = append(t.l[:i], t.l[i+1:]...) + break + } + } + + // 从保存列表中删除 + for i, r2 := range t.saves { + if r.Pattern == r2.Pattern { + t.saves = append(t.saves[:i], t.saves[i+1:]...) + break + } + } + + t.removes = append(t.removes, r) + } + return nil +} + +func (t *routetable) Save(newr *Route) error { + t.lock.Lock() + defer t.lock.Unlock() + + err := newr.init() + if err != nil { + return err + } + + r, ok := t.m[newr.Pattern] + + if ok { // 更新 + r.CopyFrom(newr) + + save := true + // 如果保存列表存在,不新增 + for _, r2 := range t.saves { + if r.Pattern == r2.Pattern { + save = false + break + } + } + + if save { + t.saves = append(t.saves, r) + } + } else { // 新增 + r = newr + t.m[r.Pattern] = r + + t.l = append(t.l, r) + t.saves = append(t.saves, r) + + for i, r2 := range t.removes { + if r.Pattern == r2.Pattern { + t.removes = append(t.removes[:i], t.removes[i+1:]...) + break + } + } + } + return nil +} + +func (t *routetable) Flush() error { + t.lock.Lock() + defer t.lock.Unlock() + + if len(t.saves)+len(t.removes) == 0 { + return nil + } + + err := t.provider.Flush(t.l, t.saves, t.removes) + if err != nil { + return err + } + + t.saves = t.saves[:0] + t.removes = t.removes[:0] + return nil +} + +func (t *routetable) All() []*Route { + t.lock.RLock() + defer t.lock.RUnlock() + + routes := make([]*Route, len(t.l)) + copy(routes, t.l) + return routes +} + +// Does path match pattern? +func pathMatch(pattern, path string) bool { + if len(pattern) == 0 { + // should not happen + return false + } + n := len(pattern) + if pattern[n-1] != '/' { + return pattern == path + } + return len(path) >= n && path[0:n] == pattern +} diff --git a/provider/route/routetable_test.go b/provider/route/routetable_test.go new file mode 100755 index 0000000..5f1e1cd --- /dev/null +++ b/provider/route/routetable_test.go @@ -0,0 +1,36 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package route + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_routetable(t *testing.T) { + + t.Run("routetable", func(t *testing.T) { + Save(&Route{"/test/live1", "rtsp://localhost:5540/live1", false}) + assert.Equal(t, 1, len(globalT.l)) + r := Get("/test/live1") + assert.NotNil(t, r) + Save(&Route{"/easy/", "rtsp://localhost:5540/test", false}) + assert.Equal(t, 2, len(globalT.l)) + r = Match("/easy/live4") + assert.NotNil(t, r) + assert.Equal(t, "rtsp://localhost:5540/test/live4", r.URL) + Del("/test/live1") + Save(&Route{"/test/live1", "rtsp://localhost:5540/live1", false}) + Save(&Route{"/test/live1", "rtsp://localhost:5540/live1", false}) + Del("/test/live1") + Save(&Route{"/test/live1", "rtsp://localhost:5540/live1", false}) + assert.Equal(t, 2, len(globalT.saves)) + assert.Equal(t, 0, len(globalT.removes)) + Flush() + assert.Equal(t, 0, len(globalT.saves)) + }) + +} diff --git a/provider/security/id.go b/provider/security/id.go new file mode 100755 index 0000000..9f14ed1 --- /dev/null +++ b/provider/security/id.go @@ -0,0 +1,85 @@ +/********************************************************************************** +* Copyright (c) 2009-2017 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +************************************************************************************/ +// +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package security + +import ( + "crypto/md5" + "crypto/sha1" + "encoding/base32" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "strconv" + "strings" + "sync/atomic" + "time" + + "golang.org/x/crypto/pbkdf2" +) + +// ID represents a process-wide unique ID. +type ID uint64 + +// next is the next identifier. We seed it with the time in seconds +// to avoid collisions of ids between process restarts. +var next = uint64( + time.Now().Sub(time.Date(2017, 9, 17, 0, 0, 0, 0, time.UTC)).Seconds(), +) + +// NewID generates a new, process-wide unique ID. +func NewID() ID { + return ID(atomic.AddUint64(&next, 1)) +} + +// Unique generates unique id based on the current id with a prefix and salt. +func (id ID) Unique(prefix uint64, salt string) string { + buffer := [16]byte{} + binary.BigEndian.PutUint64(buffer[:8], prefix) + binary.BigEndian.PutUint64(buffer[8:], uint64(id)) + + enc := pbkdf2.Key(buffer[:], []byte(salt), 4096, 16, sha1.New) + return strings.Trim(base32.StdEncoding.EncodeToString(enc), "=") +} + +// String converts the ID to a string representation. +func (id ID) String() string { + return strconv.FormatUint(uint64(id), 10) +} + +// Base64 Base64格式 +func (id ID) Base64() string { + buf := [10]byte{} + l := binary.PutUvarint(buf[:], uint64(id)) + return base64.RawURLEncoding.EncodeToString(buf[:l]) +} + +// Hex 二进制格式 +func (id ID) Hex() string { + buf := [10]byte{} + l := binary.PutUvarint(buf[:], uint64(id)) + return strings.ToUpper(hex.EncodeToString(buf[:l])) +} + +// MD5 获取ID的MD5值 +func (id ID) MD5() string { + buf := [10]byte{} + l := binary.PutUvarint(buf[:], uint64(id)) + md5Digest := md5.Sum(buf[:l]) + return hex.EncodeToString(md5Digest[:]) +} diff --git a/stats/conns.go b/stats/conns.go new file mode 100755 index 0000000..453d5d0 --- /dev/null +++ b/stats/conns.go @@ -0,0 +1,59 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package stats + +import ( + "sync/atomic" +) + +// 全局变量 +var ( + RtspConns = NewConns() // RTSP连接统计 + RtmpConns = NewConns() // RTMP连接统计 + WspConns = NewConns() // WSP连接统计 + FlvConns = NewConns() // flv连接统计 +) + +// ConnsSample 连接计数采样 +type ConnsSample struct { + Total int64 `json:"total"` + Active int64 `json:"active"` +} + +// Conns 连接统计 +type Conns interface { + Add() int64 + Release() int64 + GetSample() ConnsSample +} + +func (s *ConnsSample) clone() ConnsSample { + return ConnsSample{ + Total: atomic.LoadInt64(&s.Total), + Active: atomic.LoadInt64(&s.Active), + } +} + +type conns struct { + sample ConnsSample +} + +// NewConns 新建连接计数 +func NewConns() Conns { + return &conns{} +} + +func (c *conns) Add() int64 { + atomic.AddInt64(&c.sample.Total, 1) + return atomic.AddInt64(&c.sample.Active, 1) +} + +func (c *conns) Release() int64 { + return atomic.AddInt64(&c.sample.Active, -1) +} + +func (c *conns) GetSample() ConnsSample { + return c.sample.clone() +} diff --git a/stats/flow.go b/stats/flow.go new file mode 100755 index 0000000..99e1c66 --- /dev/null +++ b/stats/flow.go @@ -0,0 +1,82 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package stats + +import ( + "sync/atomic" +) + +// FlowSample 流统计采样 +type FlowSample struct { + InBytes int64 `json:"inbytes"` + OutBytes int64 `json:"outbytes"` +} + +// Flow 流统计接口 +type Flow interface { + AddIn(size int64) // 增加输入 + AddOut(size int64) // 增加输出 + GetSample() FlowSample // 获取当前时点采样 +} + +func (fs *FlowSample) clone() FlowSample { + return FlowSample{ + InBytes: atomic.LoadInt64(&fs.InBytes), + OutBytes: atomic.LoadInt64(&fs.OutBytes), + } +} + +// Add 采样累加 +func (fs *FlowSample) Add(f FlowSample) { + fs.InBytes = fs.InBytes + f.InBytes + fs.OutBytes = fs.OutBytes + f.OutBytes +} + +type flow struct { + sample FlowSample +} + +// NewFlow 创建流量统计 +func NewFlow() Flow { + return &flow{} +} + +func (r *flow) AddIn(size int64) { + atomic.AddInt64(&r.sample.InBytes, size) +} + +func (r *flow) AddOut(size int64) { + atomic.AddInt64(&r.sample.OutBytes, size) +} + +func (r *flow) GetSample() FlowSample { + return r.sample.clone() +} + +type childFlow struct { + parent Flow + sample FlowSample +} + +// NewChildFlow 创建子流量计数,它会把自己的计数Add到parent上 +func NewChildFlow(parent Flow) Flow { + return &childFlow{ + parent: parent, + } +} + +func (r *childFlow) AddIn(size int64) { + atomic.AddInt64(&r.sample.InBytes, size) + r.parent.AddIn(size) +} + +func (r *childFlow) AddOut(size int64) { + atomic.AddInt64(&r.sample.OutBytes, size) + r.parent.AddOut(size) +} + +func (r *childFlow) GetSample() FlowSample { + return r.sample.clone() +} diff --git a/stats/flow_test.go b/stats/flow_test.go new file mode 100755 index 0000000..83aa249 --- /dev/null +++ b/stats/flow_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package stats + +import ( + "testing" +) + +func TestFlow(t *testing.T) { + totalFlow := NewFlow() + sub1 := NewChildFlow(totalFlow) + sub2 := NewChildFlow(totalFlow) + + t.Run("", func(t *testing.T) { + sub1.AddIn(100) + sample := sub1.GetSample() + if sample.InBytes != 100 { + t.Error("InBytes not is 100") + } + sub2.AddIn(200) + sample = totalFlow.GetSample() + if sample.InBytes != 300 { + t.Error("InBytes not is 300") + } + }) + +} diff --git a/stats/runtime.go b/stats/runtime.go new file mode 100755 index 0000000..58f1aa5 --- /dev/null +++ b/stats/runtime.go @@ -0,0 +1,131 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package stats + +import ( + "runtime" + "time" + + "github.com/kelindar/process" +) + +// 创建时间 +var ( + StartingTime = time.Now() +) + +// Runtime 运行时统计 +type Runtime struct { + Heap Heap `json:"heap"` + MCache Memory `json:"mcache"` // MemStats.MCacheInuse/MCacheSys + MSpan Memory `json:"mspan"` // MemStats.MSpanInuse/MSpanSys + Stack Memory `json:"stack"` // MemStats.StackInuse/StackSys + GC GC `json:"gc"` + Go Go `json:"go"` +} + +// Proc 进程信息统计 +type Proc struct { + CPU float64 `json:"cpu"` // cpu使用情况 + Priv int32 `json:"priv"` // 私有内存 KB + Virt int32 `json:"virt"` // 虚拟内存 KB + Uptime int32 `json:"uptime"` // 运行时间 S +} + +// Heap 运行是堆信息 +type Heap struct { + Inuse int32 `json:"inuse"` // KB MemStats.HeapInuse + Sys int32 `json:"sys"` // KB MemStats.HeapSys + Alloc int32 `json:"alloc"` // KB MemStats.HeapAlloc + Idle int32 `json:"idle"` // KB MemStats.HeapIdle + Released int32 `json:"released"` // KB MemStats.HeapReleased + Objects int32 `json:"objects"` // = MemStats.HeapObjects +} + +// Memory 通用内存信息 +type Memory struct { + Inuse int32 `json:"inuse"` // KB + Sys int32 `json:"sys"` // KB +} + +// GC 垃圾回收信息 +type GC struct { + CPU float64 `json:"cpu"` // cpu使用情况 + Sys int32 `json:"sys"` // KB MemStats.GCSys +} + +// Go Go运行时 goroutines 、threads 和 total memory +type Go struct { + Count int32 `json:"count"` // runtime.NumGoroutine() + Procs int32 `json:"procs"` //runtime.NumCPU() + Sys int32 `json:"sys"` // KB MemStats.Sys + Alloc int32 `json:"alloc"` // KB MemStats.TotalAlloc +} + +// MeasureRuntime 获取运行时信息。 +func MeasureRuntime() Proc { + defer recover() + var memoryPriv, memoryVirtual int64 + var cpu float64 + process.ProcUsage(&cpu, &memoryPriv, &memoryVirtual) + return Proc{ + CPU: cpu, + Priv: toKB(uint64(memoryPriv)), + Virt: toKB(uint64(memoryVirtual)), + Uptime: int32(time.Now().Sub(StartingTime).Seconds()), + } +} + +// MeasureFullRuntime 获取运行时信息。 +func MeasureFullRuntime() *Runtime { + defer recover() + + // Collect stats + var memory runtime.MemStats + runtime.ReadMemStats(&memory) + + return &Runtime{ + // Measure heap information + Heap: Heap{ + Alloc: toKB(memory.HeapAlloc), + Idle: toKB(memory.HeapIdle), + Inuse: toKB(memory.HeapInuse), + Objects: int32(memory.HeapObjects), + Released: toKB(memory.HeapReleased), + Sys: toKB(memory.HeapSys), + }, + // Measure off heap memory + MCache: Memory{ + Inuse: toKB(memory.MCacheInuse), + Sys: toKB(memory.MCacheSys), + }, + MSpan: Memory{ + Inuse: toKB(memory.MSpanInuse), + Sys: toKB(memory.MSpanSys), + }, + // Measure memory + Stack: Memory{ + Inuse: toKB(memory.StackInuse), + Sys: toKB(memory.StackSys), + }, + // Measure GC + GC: GC{ + CPU: memory.GCCPUFraction, + Sys: toKB(memory.GCSys), + }, + // Measure goroutines and threads and total memory + Go: Go{ + Count: int32(runtime.NumGoroutine()), + Procs: int32(runtime.NumCPU()), + Sys: toKB(memory.Sys), + Alloc: toKB(memory.TotalAlloc), + }, + } +} + +// Converts the memory in bytes to KBs, otherwise it would overflow our int32 +func toKB(v uint64) int32 { + return int32(v / 1024) +} diff --git a/tomatox.conf b/tomatox.conf new file mode 100755 index 0000000..c1bbfd2 --- /dev/null +++ b/tomatox.conf @@ -0,0 +1,15 @@ +{ + "listen": ":1554", + "auth": false, + "cache_gop": true, + "profile": true, + "log": { + "level": "INFO", + "tofile": false, + "filename": "./logs/tomatox.log", + "maxsize": 20, + "maxdays": 7, + "maxbackups": 14, + "compress": false + } +} \ No newline at end of file diff --git a/utils/addr.go b/utils/addr.go new file mode 100755 index 0000000..600e897 --- /dev/null +++ b/utils/addr.go @@ -0,0 +1,54 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package utils + +import ( + "fmt" + "net" + "strings" + "github.com/emitter-io/address" +) + +// GetIP 获取IP信息 +func GetIP(addr net.Addr) string { + s := addr.String() + i := strings.LastIndex(s, ":") + return s[:i] +} + +// IsLocalhostIP 判断是否为本机IP +func IsLocalhostIP(ip net.IP) bool { + for _, localhost := range loopbackBlocks { + if localhost.Contains(ip) { + return true + } + } + privs, err := address.GetPrivate() + if err != nil { + return false + } + + for _, priv := range privs { + if priv.IP.Equal(ip) { + return true + } + } + + return false +} + +var loopbackBlocks = []*net.IPNet{ + parseCIDR("0.0.0.0/8"), // RFC 1918 IPv4 loopback address + parseCIDR("127.0.0.0/8"), // RFC 1122 IPv4 loopback address + parseCIDR("::1/128"), // RFC 1884 IPv6 loopback address +} + +func parseCIDR(s string) *net.IPNet { + _, block, err := net.ParseCIDR(s) + if err != nil { + panic(fmt.Sprintf("Bad CIDR %s: %s", s, err)) + } + return block +} diff --git a/utils/io.go b/utils/io.go new file mode 100755 index 0000000..45ec20f --- /dev/null +++ b/utils/io.go @@ -0,0 +1,40 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package utils + +import ( + "bytes" + "encoding/json" + "os" +) + +// EncodeJSONFile 编码 JSON 文件 +func EncodeJSONFile(path string, obj interface{}) error { + f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.ModePerm) + if err != nil { + return err + } + + defer f.Close() + + var formatted bytes.Buffer + body, err := json.Marshal(obj) + if err != nil { + return err + } + + if err := json.Indent(&formatted, body, "", "\t"); err != nil { + return err + } + + if _, err := f.Write(formatted.Bytes()); err != nil { + return err + } + if err := f.Sync(); err != nil { + return err + } + + return nil +} diff --git a/utils/multicast.go b/utils/multicast.go new file mode 100755 index 0000000..c14debb --- /dev/null +++ b/utils/multicast.go @@ -0,0 +1,55 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package utils + +import ( + "encoding/binary" + "net" + "sync" +) + +// MulticastIPS 全局组播池 +var ( + Multicast = &multicast{ + ipseed: minIP, + portseed: minPort, + } + minIP = binary.BigEndian.Uint32([]byte{235, 0, 0, 0}) + maxIP = binary.BigEndian.Uint32([]byte{235, 255, 255, 255}) + minPort uint16 = 16666 + maxPort uint16 = 39999 +) + +// multicast 组播IP地址池 +type multicast struct { + ipseed uint32 + portseed uint16 + l sync.Mutex +} + +// NextIP 获取组播地址 +func (p *multicast) NextIP() string { + p.l.Lock() + defer p.l.Unlock() + var ipbytes [4]byte + binary.BigEndian.PutUint32(ipbytes[:], p.ipseed) + ip := net.IP(ipbytes[:]).String() + p.ipseed++ + if p.ipseed > maxIP { + p.ipseed = minIP + } + return ip +} + +func (p *multicast) NextPort() int { + p.l.Lock() + defer p.l.Unlock() + port := p.portseed + p.portseed++ + if p.portseed > maxPort { + p.portseed = minPort + } + return int(port) +} diff --git a/utils/murmur/murmur.go b/utils/murmur/murmur.go new file mode 100644 index 0000000..4440cf9 --- /dev/null +++ b/utils/murmur/murmur.go @@ -0,0 +1,102 @@ +/********************************************************************************** +* Copyright (c) 2009-2019 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +************************************************************************************/ + +// Package murmur 是murmur算法的实现,方网站:https://sites.google.com/site/murmurhash/ +// +// MurmurHash算法:高运算性能,低碰撞率,由Austin Appleby创建于2008年, +// 现已应用到Hadoop、libstdc++、nginx、libmemcached等开源系统。 +// 2011年Appleby被Google雇佣,随后Google推出其变种的CityHash算法。 +// +// 当key的长度大于10字节的时候,MurmurHash的运算速度才快于DJB。 +// “从计算速度上来看,MurmurHash只适用于已知长度的、长度比较长的字符”。 +package murmur + +import ( + "reflect" + "unsafe" +) + +const ( + c1_32 uint32 = 0xcc9e2d51 + c2_32 uint32 = 0x1b873593 +) + +// OfString returns a murmur32 hash for the string +func OfString(value string) uint32 { + return Of(stringToBinary(value)) +} + +// Of returns a murmur32 hash for the data slice. +func Of(data []byte) uint32 { + // Seed is set to 37, same as C# version of emitter + var h1 uint32 = 37 + + nblocks := len(data) / 4 + var p uintptr + if len(data) > 0 { + p = uintptr(unsafe.Pointer(&data[0])) + } + + p1 := p + uintptr(4*nblocks) + for ; p < p1; p += 4 { + k1 := *(*uint32)(unsafe.Pointer(p)) + + k1 *= c1_32 + k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15) + k1 *= c2_32 + + h1 ^= k1 + h1 = (h1 << 13) | (h1 >> 19) // rotl32(h1, 13) + h1 = h1*5 + 0xe6546b64 + } + + tail := data[nblocks*4:] + + var k1 uint32 + switch len(tail) & 3 { + case 3: + k1 ^= uint32(tail[2]) << 16 + fallthrough + case 2: + k1 ^= uint32(tail[1]) << 8 + fallthrough + case 1: + k1 ^= uint32(tail[0]) + k1 *= c1_32 + k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15) + k1 *= c2_32 + h1 ^= k1 + } + + h1 ^= uint32(len(data)) + + h1 ^= h1 >> 16 + h1 *= 0x85ebca6b + h1 ^= h1 >> 13 + h1 *= 0xc2b2ae35 + h1 ^= h1 >> 16 + + return (h1 << 24) | (((h1 >> 8) << 16) & 0xFF0000) | (((h1 >> 16) << 8) & 0xFF00) | (h1 >> 24) +} + +func stringToBinary(v string) (b []byte) { + strHeader := (*reflect.StringHeader)(unsafe.Pointer(&v)) + byteHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + byteHeader.Data = strHeader.Data + + l := len(v) + byteHeader.Len = l + byteHeader.Cap = l + return +} diff --git a/utils/murmur/murmur_test.go b/utils/murmur/murmur_test.go new file mode 100644 index 0000000..08f9c97 --- /dev/null +++ b/utils/murmur/murmur_test.go @@ -0,0 +1,72 @@ +/********************************************************************************** +* Copyright (c) 2009-2019 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +************************************************************************************/ + +package murmur + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// BenchmarkOf-8 100000000 14.5 ns/op 0 B/op 0 allocs/op +func BenchmarkOf(b *testing.B) { + v := []byte("a/b/c/d/e/f/g/h/this/is/tomatox") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = Of(v) + } +} + +// BenchmarkOfString-8 100000000 18.4 ns/op 0 B/op 0 allocs/op +func BenchmarkOfString(b *testing.B) { + v := "a/b/c/d/e/f/g/h/this/is/tomatox" + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = OfString(v) + } +} + +func TestMeHash(t *testing.T) { + h := OfString("me") + assert.Equal(t, uint32(2539734036), h) +} + +func TestShareHash(t *testing.T) { + h := Of([]byte("$share")) + assert.Equal(t, uint32(1480642916), h) +} + +func TestLinkHash(t *testing.T) { + h := Of([]byte("link")) + assert.Equal(t, uint32(2667034312), h) +} + +func TestGetHash(t *testing.T) { + h := Of([]byte("+")) + if h != 1815237614 { + t.Errorf("Hash %d is not equal to %d", h, 1815237614) + } +} + +func TestGetHash2(t *testing.T) { + h := Of([]byte("hello world")) + if h != 4008393376 { + t.Errorf("Hash %d is not equal to %d", h, 1815237614) + } +} diff --git a/utils/path.go b/utils/path.go new file mode 100755 index 0000000..22b2a00 --- /dev/null +++ b/utils/path.go @@ -0,0 +1,37 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package utils + +import ( + "path" + "strings" +) + +// CanonicalPath 获取合法的path +func CanonicalPath(p string) string { + p = strings.ToLower(strings.TrimSpace(p)) + + if p == "" { + return "/" + } + + if p[0] != '/' { + p = "/" + p + } + + np := path.Clean(p) + // path.Clean removes trailing slash except for root; + // put the trailing slash back if necessary. + if p[len(p)-1] == '/' && np != "/" { + // Fast path for common case of p being the string we want: + if len(p) == len(np)+1 && strings.HasPrefix(p, np) { + np = p + } else { + np += "/" + } + } + + return np +} diff --git a/utils/scan/pair.go b/utils/scan/pair.go new file mode 100644 index 0000000..2338328 --- /dev/null +++ b/utils/scan/pair.go @@ -0,0 +1,61 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package scan + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +// 预定义Pair扫描对象 +var ( + // EqualPair 扫描 K=V这类形式的Pair字串 + EqualPair = NewPair('=', + func(r rune) bool { + return unicode.IsSpace(r) || r == '"' + }) + + // ColonPair 扫描 K:V 这类形式的Pair字串 + ColonPair = NewPair(':', + func(r rune) bool { + return unicode.IsSpace(r) || r == '"' + }) +) + +// Pair 从字串扫描Key Value 值 +type Pair struct { + delim rune // Key Value 间的分割 + delimLen int // 分割符长度 + trimFunc func(r rune) bool // 返回前 Trim使用的函数 +} + +// NewPair 新建 Pair 扫描器 +func NewPair(delim rune, trimFunc func(r rune) bool) Pair { + pair := Pair{ + delim: delim, + trimFunc: trimFunc, + } + pair.delimLen = utf8.RuneLen(delim) + if trimFunc == nil { + pair.trimFunc = func(r rune) bool { return false } + } + return pair +} + +// Scan 提取 K V +func (p Pair) Scan(s string) (key, value string, found bool) { + if p.delim == 0 { + return s, "", false + } + + i := strings.IndexRune(s, p.delim) + if i < 0 { + return s, "", false + } + + return strings.TrimFunc(s[:i], p.trimFunc), + strings.TrimFunc(s[i+p.delimLen:], p.trimFunc), true +} diff --git a/utils/scan/pair_test.go b/utils/scan/pair_test.go new file mode 100644 index 0000000..6a7c674 --- /dev/null +++ b/utils/scan/pair_test.go @@ -0,0 +1,111 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package scan + +import ( + "testing" + "unicode" +) + +func TestPair_Scan(t *testing.T) { + tests := []struct { + name string + args string + wantKey string + wantValue string + wantOk bool + }{ + { + "不带引号", + "a=chj", + "a", + "chj", + true, + }, + { + "带引号", + "a=\"chj\"", + "a", + "chj", + true, + }, + { + "带空个", + " \ta= \"chj\"\t", + "a", + "chj", + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + gotKey, gotValue, gotOk := EqualPair.Scan(tt.args) + if gotKey != tt.wantKey { + t.Errorf("Pair.Scan() gotKey = %v, want %v", gotKey, tt.wantKey) + } + if gotValue != tt.wantValue { + t.Errorf("Pair.Scan() gotValue = %v, want %v", gotValue, tt.wantValue) + } + if gotOk != tt.wantOk { + t.Errorf("Pair.Scan() gotOk = %v, want %v", gotOk, tt.wantOk) + } + }) + } +} + +func TestPair_ScanMultiRune(t *testing.T) { + chinesePair := NewPair('是', unicode.IsSpace) + tests := []struct { + name string + args string + wantKey string + wantValue string + wantOk bool + }{ + { + "不带空格", + "a是chj", + "a", + "chj", + true, + }, + { + "不带空格", + "a是 chj\t", + "a", + "chj", + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + gotKey, gotValue, gotOk := chinesePair.Scan(tt.args) + if gotKey != tt.wantKey { + t.Errorf("Pair.Scan() gotKey = %v, want %v", gotKey, tt.wantKey) + } + if gotValue != tt.wantValue { + t.Errorf("Pair.Scan() gotValue = %v, want %v", gotValue, tt.wantValue) + } + if gotOk != tt.wantOk { + t.Errorf("Pair.Scan() gotOk = %v, want %v", gotOk, tt.wantOk) + } + }) + } +} + +func Benchmark_Pair_Scan(b *testing.B) { + s := `realm="Another Streaming Media"` + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + key, value, ok := EqualPair.Scan(s) + _ = key + _ = value + _ = ok + } + }) +} diff --git a/utils/scan/scanner.go b/utils/scan/scanner.go new file mode 100644 index 0000000..f2a6319 --- /dev/null +++ b/utils/scan/scanner.go @@ -0,0 +1,53 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package scan + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +// 扫描器 +var ( + // 逗号分割 + Comma = NewScanner(',', unicode.IsSpace) + // 分号分割 + Semicolon = NewScanner(';', unicode.IsSpace) + // 空格分割 + Space = NewScanner(' ', nil) + // 行分割 + Line = NewScanner('\n', unicode.IsSpace) +) + +// Scanner 扫描器 +type Scanner struct { + delim rune + delimLen int + trimFunc func(r rune) bool +} + +// NewScanner 创建扫描器 +func NewScanner(delim rune, trimFunc func(r rune) bool) Scanner { + scanner := Scanner{ + delim: delim, + trimFunc: trimFunc, + } + scanner.delimLen = utf8.RuneLen(delim) + if trimFunc == nil { + scanner.trimFunc = func(r rune) bool { return false } + } + return scanner +} + +// Scan 扫描字串 +func (s Scanner) Scan(str string) (advance, token string, continueScan bool) { + i := strings.IndexRune(str, s.delim) + if i < 0 { + return "", strings.TrimFunc(str, s.trimFunc), false + } + + return strings.TrimFunc(str[i+s.delimLen:], s.trimFunc), strings.TrimFunc(str[:i], s.trimFunc), true +} diff --git a/utils/scan/scanner_test.go b/utils/scan/scanner_test.go new file mode 100644 index 0000000..035d214 --- /dev/null +++ b/utils/scan/scanner_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package scan + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestScanner_Scan(t *testing.T) { + raw := "cao,hong,ju,ok" + t.Run("Scan", func(t *testing.T) { + advance, token, ok := Comma.Scan(raw) + assert.True(t, ok) + assert.Equal(t, "cao", token) + assert.Equal(t, "hong,ju,ok", advance) + i := 0 + for ok { + advance, token, ok = Comma.Scan(advance) + if ok { + i++ + } + } + assert.Equal(t, 2, i) + assert.Equal(t, "ok", token) + }) +} + +func Benchmark_Scanner_Scan(b *testing.B) { + s := `realm="Another Streaming Media", nonce="60a76a995a0cb012f1707abc188f60cb"` + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + realm := "" + nonce := "" + ok := true + advance := s + token := "" + + for ok { + advance, token, ok = Comma.Scan(advance) + k, v, _ := EqualPair.Scan(token) + switch k { + case "realm": + realm = v + case "nonce": + nonce = v + } + } + _ = realm + _ = nonce + } + }) +}