diff --git a/build/javascript/README.md b/build/javascript/README.md index d6acc8ed9..280008ba5 100644 --- a/build/javascript/README.md +++ b/build/javascript/README.md @@ -8,3 +8,9 @@ To add, remove, or update one of these components: 2. Edit the `snowpack` `install` block of `package.json` to specify what files you want to add to the Owncast project. This can be an entire library (such as `preact`) or a single file (such as `video.js/dist/video.min.js`). These paths point to files that live in `node_modules`. 3. Run `npm run build`. This will download the requested module from NPM, package up the assets you specified, and then copy them to the Owncast web app in the `webroot/js/web_modules` directory. 4. Your new web dependency is now available for use in your web code. + +## VideoJS versions + +Currently Videojs version 7.8.3 and http-streaming version 2.2.0 are hardcoded because these are versions that have been found to work properly with our HLS stream. Other versions have had issues with things like discontinuities causing a loading spinner. + +So if you update videojs or vhs make sure you do an end-to-end test of a stream and make sure the "this stream is offline" ending video displays properly. diff --git a/build/javascript/package-lock.json b/build/javascript/package-lock.json index eb7c7f7b7..5768e621f 100644 --- a/build/javascript/package-lock.json +++ b/build/javascript/package-lock.json @@ -362,13 +362,6 @@ "mpd-parser": "0.12.0", "mux.js": "5.6.6", "video.js": "^6 || ^7" - }, - "dependencies": { - "mux.js": { - "version": "5.6.6", - "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.6.6.tgz", - "integrity": "sha512-q5VIpqb28UVs5dKsOQkpHrPxqInMjiZ/f/4qW4gEBKlm2xeBasRjRJIokixFWj+r6PWfVSEygvPffXnG7aK99g==" - } } }, "@videojs/themes": { @@ -380,9 +373,9 @@ } }, "@videojs/vhs-utils": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-2.2.0.tgz", - "integrity": "sha512-Mtq+doRlbNvis9TyI5kfOg+Vg8aHGXkSXiuNwnkcimqyaP3wO/s/iEVKPcmRUySKivjaWktjdEFVXYfaP+/HTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-2.2.1.tgz", + "integrity": "sha512-9Qbwx3LAdkG1jh2HKfninjXDxVZCeaoPcmct/bUcDRmLej68Z9XhLe5d2a9fy1qB+UuQwWg7YySASesWavYNjQ==", "requires": { "@babel/runtime": "^7.5.5", "global": "^4.3.2", @@ -397,17 +390,6 @@ "@babel/runtime": "^7.5.5", "global": "~4.4.0", "is-function": "^1.0.1" - }, - "dependencies": { - "global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "requires": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - } } }, "acorn": { @@ -1207,19 +1189,12 @@ } }, "global": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", - "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", "requires": { "min-document": "^2.19.0", - "process": "~0.5.1" - }, - "dependencies": { - "process": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", - "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" - } + "process": "^0.11.10" } }, "got": { @@ -1725,9 +1700,9 @@ "dev": true }, "mux.js": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.6.1.tgz", - "integrity": "sha512-iIE3EJURbrPZ9Y4i9ADKTIvxGUcAEBOFhwWUOZGCiKlpXDZrqDgcJLDrOa0PenLhw6WYkOyl18kHFEvwm9JSpg==" + "version": "5.6.6", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.6.6.tgz", + "integrity": "sha512-q5VIpqb28UVs5dKsOQkpHrPxqInMjiZ/f/4qW4gEBKlm2xeBasRjRJIokixFWj+r6PWfVSEygvPffXnG7aK99g==" }, "node-emoji": { "version": "1.10.0", @@ -2944,12 +2919,12 @@ } }, "video.js": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.9.6.tgz", - "integrity": "sha512-2Dg0h2IbFCZRJW/1pkNYrTqolZPonR14ajaC30D5gdVwrSLxqR6SgsYDAblXw+mFFJHxleXzoLiM/hu3TfJmEQ==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.8.3.tgz", + "integrity": "sha512-u8/1qEZdBeOm7TgBhJg8ab28vd3x62UMaaSnZ79yOMaxCqACP9CzWJT9c3Isfv2jY9BNLBIIft+BqNLTWudtLw==", "requires": { "@babel/runtime": "^7.9.2", - "@videojs/http-streaming": "1.13.4", + "@videojs/http-streaming": "1.13.2", "@videojs/xhr": "2.5.1", "global": "4.3.2", "keycode": "^2.2.0", @@ -2959,15 +2934,15 @@ }, "dependencies": { "@videojs/http-streaming": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-1.13.4.tgz", - "integrity": "sha512-I/hWi0uiA8aRwB4tfK44FRaWEoOU3uyvjUJW4cNST3TsweuovGGoud7K09WUrlbN4U0EjQvDqNwwXNggNs3niw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-1.13.2.tgz", + "integrity": "sha512-U4Xhh+HxGpRBx9Gm0LlEadq85k9BwckzFgZmyhacauhK/27Mz0goKKFAt+BpxBNp2oHVdAdk8NHfneinsqni3Q==", "requires": { - "aes-decrypter": "3.0.2", + "aes-decrypter": "3.0.0", "global": "^4.3.0", "m3u8-parser": "4.4.0", "mpd-parser": "0.10.0", - "mux.js": "5.6.1", + "mux.js": "5.5.1", "url-toolkit": "^2.1.3", "video.js": "^6.8.0 || ^7.0.0" } @@ -2982,6 +2957,30 @@ "url-toolkit": "^2.1.6" } }, + "aes-decrypter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.0.0.tgz", + "integrity": "sha1-eEihwUW5/b9Xrj4rWxvHzwZEqPs=", + "requires": { + "commander": "^2.9.0", + "global": "^4.3.2", + "pkcs7": "^1.0.2" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "global": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", + "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", + "requires": { + "min-document": "^2.19.0", + "process": "~0.5.1" + } + }, "m3u8-parser": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.4.0.tgz", @@ -3000,6 +2999,16 @@ "global": "^4.3.2", "xmldom": "^0.1.27" } + }, + "mux.js": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.5.1.tgz", + "integrity": "sha512-5VmmjADBqS4++8pTI6poSRJ+chHdaoI4XErcQPM5w4QfwaDl+FQlSI0iOgWbYDn6CBCbDRKaSCcEiN2K5aHNGQ==" + }, + "process": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", + "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" } } }, diff --git a/build/javascript/package.json b/build/javascript/package.json index ad38fa19a..0077316e1 100644 --- a/build/javascript/package.json +++ b/build/javascript/package.json @@ -6,19 +6,19 @@ "dependencies": { "@joeattardi/emoji-button": "^4.2.0", "@justinribeiro/lite-youtube": "^0.9.0", - "@videojs/http-streaming": "^2.2.0", "@videojs/themes": "^1.0.0", "htm": "^3.0.4", "preact": "^10.5.3", "tailwindcss": "^1.8.10", - "video.js": "^7.9.6" + "video.js": "7.8.3", + "@videojs/http-streaming": "2.2.0" }, "devDependencies": { "snowpack": "^2.12.1" }, "snowpack": { "install": [ - "video.js/dist/video.min.js", + "video.js/core.js", "@videojs/themes/fantasy/*", "@videojs/http-streaming/dist/videojs-http-streaming.min.js", "video.js/dist/video-js.min.css", @@ -27,11 +27,14 @@ "htm", "preact", "tailwindcss/dist/tailwind.min.css" - ] + ], + "alias": { + "video.js": "video.js/core.js" + } }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "npm install && npx snowpack install && cp -R web_modules ../../webroot/js" + "build": "npm install && npx snowpack install && cp node_modules/video.js/dist/video-js.min.css web_modules/videojs && cp -R web_modules ../../webroot/js" }, "author": "", "license": "ISC" diff --git a/config/config.go b/config/config.go index de389ea59..63f8aec32 100644 --- a/config/config.go +++ b/config/config.go @@ -178,15 +178,6 @@ func (c *config) GetMaxNumberOfReferencedSegmentsInPlaylist() int { return _default.GetMaxNumberOfReferencedSegmentsInPlaylist() } -func (c *config) GetOfflineContentPath() string { - if c.VideoSettings.OfflineContent != "" { - return c.VideoSettings.OfflineContent - } - - // This is relative to the webroot, not the project root. - return _default.VideoSettings.OfflineContent -} - func (c *config) GetFFMpegPath() string { if c.FFMpegPath != "" { return c.FFMpegPath diff --git a/config/defaults.go b/config/defaults.go index ab2384540..faae2f34e 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -1,9 +1,10 @@ package config import ( - "log" "os/exec" "strings" + + log "github.com/sirupsen/logrus" ) func getDefaults() config { diff --git a/core/core.go b/core/core.go index 918842c5d..e67dc15d5 100644 --- a/core/core.go +++ b/core/core.go @@ -5,7 +5,6 @@ import ( "path" "path/filepath" "strconv" - "time" log "github.com/sirupsen/logrus" @@ -18,13 +17,16 @@ import ( ) var ( - _stats *models.Stats - _storage models.ChunkStorageProvider - _cleanupTimer *time.Timer - _yp *yp.YP - _broadcaster *models.Broadcaster + _stats *models.Stats + _storage models.StorageProvider + _transcoder *ffmpeg.Transcoder + _yp *yp.YP + _broadcaster *models.Broadcaster ) +var handler ffmpeg.HLSHandler +var fileWriter = ffmpeg.FileWriterReceiverService{} + //Start starts up the core processing func Start() error { resetDirectories() @@ -39,6 +41,13 @@ func Start() error { return err } + // The HLS handler takes the written HLS playlists and segments + // and makes storage decisions. It's rather simple right now + // but will play more useful when recordings come into play. + handler = ffmpeg.HLSHandler{} + handler.Storage = _storage + fileWriter.SetupFileWriterReceiverService(&handler) + if err := createInitialOfflineState(); err != nil { log.Error("failed to create the initial offline state") return err @@ -63,31 +72,26 @@ func createInitialOfflineState() error { } } - ffmpeg.ShowStreamOfflineState() + transitionToOfflineVideoStreamContent() return nil } -func startCleanupTimer() { - _cleanupTimer = time.NewTimer(5 * time.Minute) - go func() { - for { - select { - case <-_cleanupTimer.C: - // Reset the session count since the session is over - _stats.SessionMaxViewerCount = 0 - resetDirectories() - ffmpeg.ShowStreamOfflineState() - } - } - }() -} +// transitionToOfflineVideoStreamContent will overwrite the current stream with the +// offline video stream state only. No live stream HLS segments will continue to be +// referenced. +func transitionToOfflineVideoStreamContent() { + log.Traceln("Firing transcoder with offline stream state") -// StopCleanupTimer will stop the previous cleanup timer -func stopCleanupTimer() { - if _cleanupTimer != nil { - _cleanupTimer.Stop() - } + offlineFilename := "offline.ts" + offlineFilePath := "static/" + offlineFilename + _transcoder := ffmpeg.NewTranscoder() + _transcoder.SetSegmentLength(10) + _transcoder.SetInput(offlineFilePath) + _transcoder.Start() + + // Copy the logo to be the thumbnail + utils.Copy(filepath.Join("webroot", config.Config.InstanceDetails.Logo.Large), "webroot/thumbnail.jpg") } func resetDirectories() { @@ -112,4 +116,7 @@ func resetDirectories() { os.MkdirAll(path.Join(config.PrivateHLSStoragePath, strconv.Itoa(0)), 0777) os.MkdirAll(path.Join(config.PublicHLSStoragePath, strconv.Itoa(0)), 0777) } + + // Remove the previous thumbnail + utils.Copy(config.Config.InstanceDetails.Logo.Large, "webroot/thumbnail.jpg") } diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go deleted file mode 100644 index 5671ec6cb..000000000 --- a/core/ffmpeg/ffmpeg.go +++ /dev/null @@ -1,14 +0,0 @@ -package ffmpeg - -import ( - "github.com/owncast/owncast/config" -) - -//ShowStreamOfflineState generates and shows the stream's offline state -func ShowStreamOfflineState() { - transcoder := NewTranscoder() - transcoder.SetSegmentLength(10) - transcoder.SetAppendToStream(true) - transcoder.SetInput(config.Config.GetOfflineContentPath()) - transcoder.Start() -} diff --git a/core/ffmpeg/fileWriterReceiverService.go b/core/ffmpeg/fileWriterReceiverService.go new file mode 100644 index 000000000..ac22de791 --- /dev/null +++ b/core/ffmpeg/fileWriterReceiverService.go @@ -0,0 +1,103 @@ +package ffmpeg + +import ( + "bytes" + "io" + "os" + "path/filepath" + "strconv" + "strings" + + "net/http" + + "github.com/owncast/owncast/config" + "github.com/owncast/owncast/utils" + log "github.com/sirupsen/logrus" +) + +// FileWriterReceiverServiceCallback are to be fired when transcoder responses are written to disk +type FileWriterReceiverServiceCallback interface { + SegmentWritten(localFilePath string) + VariantPlaylistWritten(localFilePath string) + MasterPlaylistWritten(localFilePath string) +} + +// FileWriterReceiverService accepts transcoder responses via HTTP and fires the callbacks +type FileWriterReceiverService struct { + callbacks FileWriterReceiverServiceCallback +} + +// SetupFileWriterReceiverService will start listening for transcoder responses +func (s *FileWriterReceiverService) SetupFileWriterReceiverService(callbacks FileWriterReceiverServiceCallback) { + s.callbacks = callbacks + + httpServer := http.NewServeMux() + httpServer.HandleFunc("/", s.uploadHandler) + + localListenerAddress := "127.0.0.1:" + strconv.Itoa(config.Config.GetPublicWebServerPort()+1) + go http.ListenAndServe(localListenerAddress, httpServer) + log.Traceln("Transcoder response listening on: " + localListenerAddress) +} + +func (s *FileWriterReceiverService) uploadHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "PUT" { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + path := r.URL.Path + writePath := filepath.Join(config.PrivateHLSStoragePath, path) + + var buf bytes.Buffer + io.Copy(&buf, r.Body) + data := buf.Bytes() + f, err := os.Create(writePath) + if err != nil { + returnError(err, w, r) + return + } + + defer f.Close() + _, err = f.Write(data) + if err != nil { + returnError(err, w, r) + return + } + + s.fileWritten(writePath) + w.WriteHeader(http.StatusOK) +} + +var _inWarningState = false + +func (s *FileWriterReceiverService) fileWritten(path string) { + index := utils.GetIndexFromFilePath(path) + + if utils.GetRelativePathFromAbsolutePath(path) == "hls/stream.m3u8" { + s.callbacks.MasterPlaylistWritten(path) + + } else if strings.HasSuffix(path, ".ts") { + performanceMonitorKey := "segmentWritten-" + index + averagePerformance := utils.GetAveragePerformance(performanceMonitorKey) + + utils.StartPerformanceMonitor(performanceMonitorKey) + s.callbacks.SegmentWritten(path) + + if averagePerformance != 0 && averagePerformance > float64(float64(config.Config.GetVideoSegmentSecondsLength())) { + if !_inWarningState { + log.Warnln("slow encoding for variant", index, "if this continues you may see buffering or errors. troubleshoot this issue by visiting https://owncast.online/docs/troubleshooting/") + _inWarningState = true + } + } else { + _inWarningState = false + } + + } else if strings.HasSuffix(path, ".m3u8") { + s.callbacks.VariantPlaylistWritten(path) + } +} + +func returnError(err error, w http.ResponseWriter, r *http.Request) { + log.Errorln(err) + http.Error(w, http.StatusText(http.StatusInternalServerError)+": "+err.Error(), http.StatusInternalServerError) +} diff --git a/core/ffmpeg/hlsFilesystemCleanup.go b/core/ffmpeg/hlsFilesystemCleanup.go new file mode 100644 index 000000000..a8701a02e --- /dev/null +++ b/core/ffmpeg/hlsFilesystemCleanup.go @@ -0,0 +1,63 @@ +package ffmpeg + +import ( + log "github.com/sirupsen/logrus" + + "os" + "path/filepath" + "sort" + + "github.com/owncast/owncast/config" +) + +// Cleanup will delete old files off disk that are no longer being referenced +// in the stream. +func Cleanup(directoryPath string) { + // Determine how many files we should keep on disk + maxNumber := config.Config.GetMaxNumberOfReferencedSegmentsInPlaylist() + buffer := 10 + + files, err := getSegmentFiles(directoryPath) + if err != nil { + log.Fatal(err) + } + + if len(files) < maxNumber+buffer { + return + } + + // Delete old files on disk + filesToDelete := files[maxNumber+buffer:] + for _, file := range filesToDelete { + os.Remove(filepath.Join(directoryPath, file.Name())) + } +} + +func getSegmentFiles(dirname string) ([]os.FileInfo, error) { + f, err := os.Open(dirname) + if err != nil { + return nil, err + } + list, err := f.Readdir(-1) // -1 says to get a list of all files + f.Close() + if err != nil { + return nil, err + } + + filteredList := make([]os.FileInfo, 0) + + // Filter out playlists because we don't want to clean them up + for _, file := range list { + if filepath.Ext(file.Name()) == ".m3u8" { + continue + } + filteredList = append(filteredList, file) + } + + // Sort by date so we can delete old files + sort.Slice(filteredList, func(i, j int) bool { + return filteredList[i].ModTime().UnixNano() > filteredList[j].ModTime().UnixNano() + }) + + return filteredList, nil +} diff --git a/core/ffmpeg/hlsHandler.go b/core/ffmpeg/hlsHandler.go new file mode 100644 index 000000000..65aa7a42d --- /dev/null +++ b/core/ffmpeg/hlsHandler.go @@ -0,0 +1,25 @@ +package ffmpeg + +import ( + "github.com/owncast/owncast/models" +) + +// HLSHandler gets told about available HLS playlists and segments +type HLSHandler struct { + Storage models.StorageProvider +} + +// SegmentWritten is fired when a HLS segment is written to disk +func (h *HLSHandler) SegmentWritten(localFilePath string) { + h.Storage.SegmentWritten(localFilePath) +} + +// VariantPlaylistWritten is fired when a HLS variant playlist is written to disk +func (h *HLSHandler) VariantPlaylistWritten(localFilePath string) { + h.Storage.VariantPlaylistWritten(localFilePath) +} + +// MasterPlaylistWritten is fired when a HLS master playlist is written to disk +func (h *HLSHandler) MasterPlaylistWritten(localFilePath string) { + h.Storage.MasterPlaylistWritten(localFilePath) +} diff --git a/core/ffmpeg/thumbnailGenerator.go b/core/ffmpeg/thumbnailGenerator.go index b98c7f41b..aa58b74e1 100644 --- a/core/ffmpeg/thumbnailGenerator.go +++ b/core/ffmpeg/thumbnailGenerator.go @@ -13,36 +13,44 @@ import ( "github.com/owncast/owncast/config" ) +var _timer *time.Ticker + +func StopThumbnailGenerator() { + if _timer != nil { + _timer.Stop() + } +} + //StartThumbnailGenerator starts generating thumbnails func StartThumbnailGenerator(chunkPath string, variantIndex int) { // Every 20 seconds create a thumbnail from the most // recent video segment. - ticker := time.NewTicker(20 * time.Second) + _timer = time.NewTicker(20 * time.Second) quit := make(chan struct{}) go func() { for { select { - case <-ticker.C: + case <-_timer.C: if err := fireThumbnailGenerator(chunkPath, variantIndex); err != nil { log.Errorln("Unable to generate thumbnail:", err) } case <-quit: //TODO: evaluate if this is ever stopped log.Debug("thumbnail generator has stopped") - ticker.Stop() + _timer.Stop() return } } }() } -func fireThumbnailGenerator(chunkPath string, variantIndex int) error { +func fireThumbnailGenerator(segmentPath string, variantIndex int) error { // JPG takes less time to encode than PNG outputFile := path.Join(config.WebRoot, "thumbnail.jpg") previewGifFile := path.Join(config.WebRoot, "preview.gif") - framePath := path.Join(chunkPath, strconv.Itoa(variantIndex)) + framePath := path.Join(segmentPath, strconv.Itoa(variantIndex)) files, err := ioutil.ReadDir(framePath) if err != nil { return err diff --git a/core/ffmpeg/transcoder.go b/core/ffmpeg/transcoder.go index 98f724887..cde3a37f1 100644 --- a/core/ffmpeg/transcoder.go +++ b/core/ffmpeg/transcoder.go @@ -3,7 +3,6 @@ package ffmpeg import ( "fmt" "os/exec" - "path" "strconv" "strings" @@ -27,6 +26,8 @@ type Transcoder struct { appendToStream bool ffmpegPath string segmentIdentifier string + internalListenerPort int + TranscoderCompleted func(error) } // HLSVariant is a combination of settings that results in a single HLS stream @@ -91,16 +92,27 @@ func (t *Transcoder) Start() { log.Panicln(err, command) } + err = _commandExec.Wait() + if t.TranscoderCompleted != nil { + t.TranscoderCompleted(err) + } return } func (t *Transcoder) getString() string { - hlsOptionFlags := []string{ - "delete_segments", - "program_date_time", - "temp_file", + var port int + if config.Config != nil { + port = config.Config.GetPublicWebServerPort() + 1 + } else if t.internalListenerPort != 0 { + port = t.internalListenerPort + } else { + log.Panicln("A internal port must be set for transcoder callback") } + localListenerAddress := "http://127.0.0.1:" + strconv.Itoa(port) + + hlsOptionFlags := []string{} + if t.appendToStream { hlsOptionFlags = append(hlsOptionFlags, "append_list") } @@ -109,32 +121,43 @@ func (t *Transcoder) getString() string { t.segmentIdentifier = shortid.MustGenerate() } + hlsOptionsString := "" + if len(hlsOptionFlags) > 0 { + hlsOptionsString = "-hls_flags " + strings.Join(hlsOptionFlags, "+") + } ffmpegFlags := []string{ - "cat", t.input, "|", t.ffmpegPath, "-hide_banner", - "-i pipe:", + "-loglevel warning", + "-i ", t.input, + t.getVariantsString(), // HLS Output "-f", "hls", + "-hls_time", strconv.Itoa(t.segmentLengthSeconds), // Length of each segment "-hls_list_size", strconv.Itoa(t.hlsPlaylistLength), // Max # in variant playlist "-hls_delete_threshold", "10", // Start deleting files after hls_list_size + 10 - "-hls_flags", strings.Join(hlsOptionFlags, "+"), // Specific options in HLS generation + hlsOptionsString, // Video settings "-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment) - // "-profile:v", "high", // Main – for standard definition (SD) to 640×480, High – for high definition (HD) to 1920×1080 + "-pix_fmt", "yuv420p", // Force yuv420p color format + "-profile:v", "high", // Main – for standard definition (SD) to 640×480, High – for high definition (HD) to 1920×1080 "-sc_threshold", "0", // Disable scene change detection for creating segments // Filenames "-master_pl_name", "stream.m3u8", - "-strftime 1", // Support the use of strftime in filenames - "-hls_segment_filename", path.Join(t.segmentOutputPath, "/%v/stream-%s-"+t.segmentIdentifier+".ts"), // Each segment's filename + "-strftime 1", // Support the use of strftime in filenames + + "-hls_segment_filename", localListenerAddress + "/%v/stream-" + t.segmentIdentifier + "%s.ts", // Send HLS segments back to us over HTTP "-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0 - path.Join(t.segmentOutputPath, "/%v/stream.m3u8"), // Each variant's playlist - "2> transcoder.log", + + "-method PUT -http_persistent 1", // HLS results sent back to us will be over PUTs + "-fflags +genpts", // Generate presentation time stamp if missing + localListenerAddress + "/%v/stream.m3u8", // Send HLS playlists back to us over HTTP + "2> transcoder.log", // Log to a file for debugging } return strings.Join(ffmpegFlags, " ") @@ -180,7 +203,7 @@ func getVariantFromConfigQuality(quality config.StreamQuality, index int) HLSVar } // NewTranscoder will return a new Transcoder, populated by the config -func NewTranscoder() Transcoder { +func NewTranscoder() *Transcoder { transcoder := new(Transcoder) transcoder.ffmpegPath = config.Config.GetFFMpegPath() transcoder.hlsPlaylistLength = config.Config.GetMaxNumberOfReferencedSegmentsInPlaylist() @@ -207,7 +230,7 @@ func NewTranscoder() Transcoder { transcoder.AddVariant(variant) } - return *transcoder + return transcoder } // Uses `map` https://www.ffmpeg.org/ffmpeg-all.html#Stream-specifiers-1 https://www.ffmpeg.org/ffmpeg-all.html#Advanced-options @@ -364,3 +387,7 @@ func (t *Transcoder) SetAppendToStream(append bool) { func (t *Transcoder) SetIdentifier(output string) { t.segmentIdentifier = output } + +func (t *Transcoder) SetInternalHTTPPort(port int) { + t.internalListenerPort = port +} diff --git a/core/ffmpeg/transcoder_test.go b/core/ffmpeg/transcoder_test.go index 322406683..cb70aae19 100644 --- a/core/ffmpeg/transcoder_test.go +++ b/core/ffmpeg/transcoder_test.go @@ -12,6 +12,7 @@ func TestFFmpegCommand(t *testing.T) { transcoder.SetOutputPath("fakeOutput") transcoder.SetHLSPlaylistLength(10) transcoder.SetIdentifier("jdofFGg") + transcoder.SetInternalHTTPPort(8123) variant := HLSVariant{} variant.videoBitrate = 1200 @@ -23,7 +24,7 @@ func TestFFmpegCommand(t *testing.T) { cmd := transcoder.getString() - expected := `cat fakecontent.flv | /fake/path/ffmpeg -hide_banner -i pipe: -map v:0 -c:v:0 libx264 -b:v:0 1200k -maxrate:v:0 1272k -bufsize:v:0 1440k -g:v:0 119 -x264-params:v:0 "scenecut=0:open_gop=0:min-keyint=119:keyint=119" -map a:0 -c:a:0 copy -r 30 -preset veryfast -var_stream_map "v:0,a:0 " -f hls -hls_time 4 -hls_list_size 10 -hls_delete_threshold 10 -hls_flags delete_segments+program_date_time+temp_file -tune zerolatency -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename fakeOutput/%v/stream-%s-jdofFGg.ts -max_muxing_queue_size 400 fakeOutput/%v/stream.m3u8 2> transcoder.log` + expected := `/fake/path/ffmpeg -hide_banner -loglevel warning -i fakecontent.flv -map v:0 -c:v:0 libx264 -b:v:0 1200k -maxrate:v:0 1272k -bufsize:v:0 1440k -g:v:0 119 -x264-params:v:0 "scenecut=0:open_gop=0:min-keyint=119:keyint=119" -map a:0 -c:a:0 copy -r 30 -preset veryfast -var_stream_map "v:0,a:0 " -f hls -hls_time 4 -hls_list_size 10 -hls_delete_threshold 10 -tune zerolatency -pix_fmt yuv420p -profile:v high -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg%s.ts -max_muxing_queue_size 400 -method PUT -http_persistent 1 -fflags +genpts http://127.0.0.1:8123/%v/stream.m3u8 2> transcoder.log` if cmd != expected { t.Errorf("ffmpeg command does not match expected. Got %s, want: %s", cmd, expected) diff --git a/core/playlist/monitor.go b/core/playlist/monitor.go deleted file mode 100644 index 264edc1c3..000000000 --- a/core/playlist/monitor.go +++ /dev/null @@ -1,157 +0,0 @@ -package playlist - -import ( - "io/ioutil" - "path" - "path/filepath" - "strconv" - "strings" - "time" - - log "github.com/sirupsen/logrus" - - "github.com/radovskyb/watcher" - - "github.com/owncast/owncast/config" - "github.com/owncast/owncast/models" - "github.com/owncast/owncast/utils" -) - -var ( - _storage models.ChunkStorageProvider - variants []models.Variant -) - -//StartVideoContentMonitor starts the video content monitor -func StartVideoContentMonitor(storage models.ChunkStorageProvider) error { - _storage = storage - - pathToMonitor := config.PrivateHLSStoragePath - - // Create at least one structure to store the segments for the different stream variants - variants = make([]models.Variant, len(config.Config.VideoSettings.StreamQualities)) - if len(config.Config.VideoSettings.StreamQualities) > 0 { - for index := range variants { - variants[index] = models.Variant{ - VariantIndex: index, - Segments: make(map[string]*models.Segment), - } - } - } else { - variants[0] = models.Variant{ - VariantIndex: 0, - Segments: make(map[string]*models.Segment), - } - } - - // log.Printf("Using directory %s for storing files with %d variants...\n", pathToMonitor, len(variants)) - - w := watcher.New() - - go func() { - for { - select { - case event := <-w.Event: - - relativePath := utils.GetRelativePathFromAbsolutePath(event.Path) - if path.Ext(relativePath) == ".tmp" { - continue - } - - // Ignore removals - if event.Op == watcher.Remove { - continue - } - - // Handle updates to the master playlist by copying it to webroot - if relativePath == path.Join(config.PrivateHLSStoragePath, "stream.m3u8") { - utils.Copy(event.Path, path.Join(config.PublicHLSStoragePath, "stream.m3u8")) - - } else if filepath.Ext(event.Path) == ".m3u8" { - // Handle updates to playlists, but not the master playlist - updateVariantPlaylist(event.Path) - - } else if filepath.Ext(event.Path) == ".ts" { - segment, err := getSegmentFromPath(event.Path) - if err != nil { - log.Error("failed to get the segment from path") - panic(err) - } - - newObjectPathChannel := make(chan string, 1) - go func() { - newObjectPath, err := storage.Save(path.Join(config.PrivateHLSStoragePath, segment.RelativeUploadPath), 0) - if err != nil { - log.Errorln("failed to save the file to the chunk storage.", err) - } - - newObjectPathChannel <- newObjectPath - }() - - newObjectPath := <-newObjectPathChannel - segment.RemoteID = newObjectPath - // fmt.Println("Uploaded", segment.RelativeUploadPath, "as", newObjectPath) - - variants[segment.VariantIndex].Segments[filepath.Base(segment.RelativeUploadPath)] = &segment - - // Force a variant's playlist to be updated after a file is uploaded. - associatedVariantPlaylist := strings.ReplaceAll(event.Path, path.Base(event.Path), "stream.m3u8") - updateVariantPlaylist(associatedVariantPlaylist) - } - case err := <-w.Error: - panic(err) - case <-w.Closed: - return - } - } - }() - - // Watch the hls segment storage folder recursively for changes. - w.FilterOps(watcher.Write, watcher.Rename, watcher.Create) - - if err := w.AddRecursive(pathToMonitor); err != nil { - return err - } - - return w.Start(time.Millisecond * 200) -} - -func getSegmentFromPath(fullDiskPath string) (models.Segment, error) { - segment := models.Segment{ - FullDiskPath: fullDiskPath, - RelativeUploadPath: utils.GetRelativePathFromAbsolutePath(fullDiskPath), - } - - index, err := strconv.Atoi(segment.RelativeUploadPath[0:1]) - if err != nil { - return segment, err - } - - segment.VariantIndex = index - - return segment, nil -} - -func getVariantIndexFromPath(fullDiskPath string) (int, error) { - return strconv.Atoi(fullDiskPath[0:1]) -} - -func updateVariantPlaylist(fullPath string) error { - relativePath := utils.GetRelativePathFromAbsolutePath(fullPath) - variantIndex, err := getVariantIndexFromPath(relativePath) - if err != nil { - return err - } - - variant := variants[variantIndex] - - playlistBytes, err := ioutil.ReadFile(fullPath) - if err != nil { - return err - } - - playlistString := string(playlistBytes) - playlistString = _storage.GenerateRemotePlaylist(playlistString, variant) - - return WritePlaylist(playlistString, path.Join(config.PublicHLSStoragePath, relativePath)) -} diff --git a/core/rtmp/rtmp.go b/core/rtmp/rtmp.go index fefaee13d..3ba44be82 100644 --- a/core/rtmp/rtmp.go +++ b/core/rtmp/rtmp.go @@ -17,7 +17,6 @@ import ( "github.com/nareix/joy5/format/rtmp" "github.com/owncast/owncast/config" "github.com/owncast/owncast/core" - "github.com/owncast/owncast/core/ffmpeg" "github.com/owncast/owncast/models" "github.com/owncast/owncast/utils" ) @@ -27,7 +26,6 @@ var ( _isConnected = false ) -var _transcoder ffmpeg.Transcoder var _pipe *os.File var _rtmpConnection net.Conn @@ -115,9 +113,6 @@ func HandleConn(c *rtmp.Conn, nc net.Conn) { pipePath := utils.GetTemporaryPipePath() syscall.Mkfifo(pipePath, 0666) - _transcoder = ffmpeg.NewTranscoder() - go _transcoder.Start() - _isConnected = true core.SetStreamAsConnected() _rtmpConnection = nc @@ -153,9 +148,6 @@ func handleDisconnect(conn net.Conn) { conn.Close() _pipe.Close() _isConnected = false - _transcoder.Stop() - _rtmpConnection = nil - core.SetStreamAsDisconnected() } // Disconnect will force disconnect the current inbound RTMP connection. diff --git a/core/stats.go b/core/stats.go index 5799afbab..171a30acb 100644 --- a/core/stats.go +++ b/core/stats.go @@ -115,7 +115,9 @@ func SetClientActive(client models.Client) { func RemoveClient(clientID string) { log.Trace("Removing the client:", clientID) + l.Lock() delete(_stats.Clients, clientID) + l.Unlock() } func GetClients() []models.Client { diff --git a/core/status.go b/core/status.go index 986ad61b9..6c0854f41 100644 --- a/core/status.go +++ b/core/status.go @@ -1,12 +1,7 @@ package core import ( - "time" - - "github.com/owncast/owncast/config" - "github.com/owncast/owncast/core/ffmpeg" "github.com/owncast/owncast/models" - "github.com/owncast/owncast/utils" ) //GetStatus gets the status of the system @@ -25,40 +20,6 @@ func GetStatus() models.Status { } } -//SetStreamAsConnected sets the stream as connected -func SetStreamAsConnected() { - stopCleanupTimer() - - _stats.StreamConnected = true - _stats.LastConnectTime = utils.NullTime{time.Now(), true} - _stats.LastDisconnectTime = utils.NullTime{time.Now(), false} - - chunkPath := config.PublicHLSStoragePath - if usingExternalStorage { - chunkPath = config.PrivateHLSStoragePath - } - - if _yp != nil { - _yp.Start() - } - - ffmpeg.StartThumbnailGenerator(chunkPath, config.Config.VideoSettings.HighestQualityStreamIndex) -} - -//SetStreamAsDisconnected sets the stream as disconnected -func SetStreamAsDisconnected() { - _stats.StreamConnected = false - _stats.LastDisconnectTime = utils.NullTime{time.Now(), true} - _broadcaster = nil - - if _yp != nil { - _yp.Stop() - } - - ffmpeg.ShowStreamOfflineState() - startCleanupTimer() -} - // SetBroadcaster will store the current inbound broadcasting details func SetBroadcaster(broadcaster models.Broadcaster) { _broadcaster = &broadcaster diff --git a/core/storage.go b/core/storage.go index 8b94afa73..f4155ecac 100644 --- a/core/storage.go +++ b/core/storage.go @@ -2,7 +2,6 @@ package core import ( "github.com/owncast/owncast/config" - "github.com/owncast/owncast/core/playlist" "github.com/owncast/owncast/core/storageproviders" ) @@ -11,17 +10,16 @@ var ( ) func setupStorage() error { + handler.Storage = _storage + if config.Config.S3.Enabled { _storage = &storageproviders.S3Storage{} - usingExternalStorage = true + } else { + _storage = &storageproviders.LocalStorage{} } - if usingExternalStorage { - if err := _storage.Setup(); err != nil { - return err - } - - go playlist.StartVideoContentMonitor(_storage) + if err := _storage.Setup(); err != nil { + return err } return nil diff --git a/core/storageproviders/local.go b/core/storageproviders/local.go new file mode 100644 index 000000000..842ef95eb --- /dev/null +++ b/core/storageproviders/local.go @@ -0,0 +1,63 @@ +package storageproviders + +import ( + "path/filepath" + + log "github.com/sirupsen/logrus" + + "github.com/owncast/owncast/config" + "github.com/owncast/owncast/core/ffmpeg" + "github.com/owncast/owncast/utils" +) + +type LocalStorage struct { +} + +// Setup configures this storage provider +func (s *LocalStorage) Setup() error { + // no-op + return nil +} + +// SegmentWritten is called when a single segment of video is written +func (s *LocalStorage) SegmentWritten(localFilePath string) { + s.Save(localFilePath, 0) +} + +// VariantPlaylistWritten is called when a variant hls playlist is written +func (s *LocalStorage) VariantPlaylistWritten(localFilePath string) { + _, error := s.Save(localFilePath, 0) + if error != nil { + log.Errorln(error) + return + } +} + +// MasterPlaylistWritten is called when the master hls playlist is written +func (s *LocalStorage) MasterPlaylistWritten(localFilePath string) { + s.Save(localFilePath, 0) +} + +// Save will save a local filepath using the storage provider +func (s *LocalStorage) Save(filePath string, retryCount int) (string, error) { + newPath := "" + + // This is a hack + if filePath == "hls/stream.m3u8" { + newPath = filepath.Join(config.PublicHLSStoragePath, filepath.Base(filePath)) + } else { + newPath = filepath.Join(config.WebRoot, filePath) + } + + // Move video segments to the destination directory. + // Copy playlists to the destination directory so they can still be referenced in + // the private hls working directory. + if filepath.Ext(filePath) == ".m3u8" { + utils.Copy(filePath, newPath) + } else { + utils.Move(filePath, newPath) + ffmpeg.Cleanup(filepath.Dir(newPath)) + } + + return newPath, nil +} diff --git a/core/storageproviders/s3Storage.go b/core/storageproviders/s3Storage.go index 92a33876c..b31caafcd 100644 --- a/core/storageproviders/s3Storage.go +++ b/core/storageproviders/s3Storage.go @@ -4,8 +4,10 @@ import ( "bufio" "fmt" "os" - "strings" + "path/filepath" + "github.com/owncast/owncast/core/playlist" + "github.com/owncast/owncast/utils" log "github.com/sirupsen/logrus" "github.com/aws/aws-sdk-go/aws" @@ -14,9 +16,14 @@ import ( "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/owncast/owncast/config" - "github.com/owncast/owncast/models" + + "github.com/grafov/m3u8" ) +// If we try to upload a playlist but it is not yet on disk +// then keep a reference to it here. +var _queuedPlaylistUpdates = make(map[string]string, 0) + //S3Storage is the s3 implementation of the ChunkStorageProvider type S3Storage struct { sess *session.Session @@ -31,10 +38,18 @@ type S3Storage struct { s3ACL string } +var _uploader *s3manager.Uploader + //Setup sets up the s3 storage for saving the video to s3 func (s *S3Storage) Setup() error { log.Trace("Setting up S3 for external storage of video...") + if config.Config.S3.ServingEndpoint != "" { + s.host = config.Config.S3.ServingEndpoint + } else { + s.host = fmt.Sprintf("%s/%s", config.Config.S3.Endpoint, config.Config.S3.Bucket) + } + s.s3Endpoint = config.Config.S3.Endpoint s.s3ServingEndpoint = config.Config.S3.ServingEndpoint s.s3Region = config.Config.S3.Region @@ -45,68 +60,113 @@ func (s *S3Storage) Setup() error { s.sess = s.connectAWS() + _uploader = s3manager.NewUploader(s.sess) + return nil } -//Save saves the file to the s3 bucket -func (s *S3Storage) Save(filePath string, retryCount int) (string, error) { - // fmt.Println("Saving", filePath) +// SegmentWritten is called when a single segment of video is written +func (s *S3Storage) SegmentWritten(localFilePath string) { + index := utils.GetIndexFromFilePath(localFilePath) + performanceMonitorKey := "s3upload-" + index + utils.StartPerformanceMonitor(performanceMonitorKey) + // Upload the segment + _, error := s.Save(localFilePath, 0) + if error != nil { + log.Errorln(error) + return + } + averagePerformance := utils.GetAveragePerformance(performanceMonitorKey) + + // Warn the user about long-running save operations + if averagePerformance != 0 { + if averagePerformance > float64(config.Config.GetVideoSegmentSecondsLength())*0.9 { + log.Warnln("Possible slow uploads: average upload S3 save duration", averagePerformance, "ms. troubleshoot this issue by visiting https://owncast.online/docs/troubleshooting/") + } + log.Traceln(localFilePath, "uploaded to S3") + } + + // Upload the variant playlist for this segment + // so the segments and the HLS playlist referencing + // them are in sync. + playlist := filepath.Join(filepath.Dir(localFilePath), "stream.m3u8") + _, error = s.Save(playlist, 0) + if error != nil { + _queuedPlaylistUpdates[playlist] = playlist + if pErr, ok := error.(*os.PathError); ok { + log.Debugln(pErr.Path, "does not yet exist locally when trying to upload to S3 storage.") + return + } + } + + // If a segment file was successfully uploaded then we can delete + // it from the local filesystem. + os.Remove(localFilePath) +} + +// VariantPlaylistWritten is called when a variant hls playlist is written +func (s *S3Storage) VariantPlaylistWritten(localFilePath string) { + // We are uploading the variant playlist after uploading the segment + // to make sure we're not refering to files in a playlist that don't + // yet exist. See SegmentWritten. + if _, ok := _queuedPlaylistUpdates[localFilePath]; ok { + _, error := s.Save(localFilePath, 0) + if error != nil { + log.Errorln(error) + _queuedPlaylistUpdates[localFilePath] = localFilePath + } + delete(_queuedPlaylistUpdates, localFilePath) + } +} + +// MasterPlaylistWritten is called when the master hls playlist is written +func (s *S3Storage) MasterPlaylistWritten(localFilePath string) { + // Rewrite the playlist to use absolute remote S3 URLs + s.rewriteRemotePlaylist(localFilePath) +} + +// Save saves the file to the s3 bucket +func (s *S3Storage) Save(filePath string, retryCount int) (string, error) { file, err := os.Open(filePath) if err != nil { return "", err } defer file.Close() - uploader := s3manager.NewUploader(s.sess) + maxAgeSeconds := utils.GetCacheDurationSecondsForPath(filePath) + cacheControlHeader := fmt.Sprintf("Cache-Control: max-age=%d", maxAgeSeconds) uploadInput := &s3manager.UploadInput{ - Bucket: aws.String(s.s3Bucket), // Bucket to be used - Key: aws.String(filePath), // Name of the file to be saved - Body: file, // File + Bucket: aws.String(s.s3Bucket), // Bucket to be used + Key: aws.String(filePath), // Name of the file to be saved + Body: file, // File + CacheControl: &cacheControlHeader, } + if s.s3ACL != "" { uploadInput.ACL = aws.String(s.s3ACL) + } else { + // Default ACL + uploadInput.ACL = aws.String("public-read") } - response, err := uploader.Upload(uploadInput) + + response, err := _uploader.Upload(uploadInput) if err != nil { - log.Trace("error uploading:", err.Error()) + log.Traceln("error uploading:", filePath, err.Error()) if retryCount < 4 { - log.Trace("Retrying...") + log.Traceln("Retrying...") return s.Save(filePath, retryCount+1) + } else { + log.Warnln("Giving up on", filePath, err) + return "", fmt.Errorf("Giving up on %s", filePath) } } - // fmt.Println("Uploaded", filePath, "to", response.Location) - return response.Location, nil } -//GenerateRemotePlaylist implements the 'GenerateRemotePlaylist' method -func (s *S3Storage) GenerateRemotePlaylist(playlist string, variant models.Variant) string { - var newPlaylist = "" - - scanner := bufio.NewScanner(strings.NewReader(playlist)) - for scanner.Scan() { - line := scanner.Text() - if line[0:1] != "#" { - fullRemotePath := variant.GetSegmentForFilename(line) - if fullRemotePath == nil { - line = "" - } else if s.s3ServingEndpoint != "" { - line = fmt.Sprintf("%s/%s/%s", s.s3ServingEndpoint, config.PrivateHLSStoragePath, fullRemotePath.RelativeUploadPath) - } else { - line = fullRemotePath.RemoteID - } - } - - newPlaylist = newPlaylist + line + "\n" - } - - return newPlaylist -} - -func (s S3Storage) connectAWS() *session.Session { +func (s *S3Storage) connectAWS() *session.Session { creds := credentials.NewStaticCredentials(s.s3AccessKey, s.s3Secret, "") _, err := creds.Get() if err != nil { @@ -127,3 +187,24 @@ func (s S3Storage) connectAWS() *session.Session { } return sess } + +// rewriteRemotePlaylist will take a local playlist and rewrite it to have absolute URLs to remote locations. +func (s *S3Storage) rewriteRemotePlaylist(filePath string) error { + f, err := os.Open(filePath) + if err != nil { + panic(err) + } + + p := m3u8.NewMasterPlaylist() + err = p.DecodeFrom(bufio.NewReader(f), false) + + for _, item := range p.Variants { + item.URI = s.host + filepath.Join("/hls", item.URI) + } + + publicPath := filepath.Join(config.PublicHLSStoragePath, filepath.Base(filePath)) + + newPlaylist := p.String() + + return playlist.WritePlaylist(newPlaylist, publicPath) +} diff --git a/core/streamState.go b/core/streamState.go new file mode 100644 index 000000000..e16d0ac8f --- /dev/null +++ b/core/streamState.go @@ -0,0 +1,132 @@ +package core + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/owncast/owncast/config" + "github.com/owncast/owncast/core/ffmpeg" + "github.com/owncast/owncast/utils" + + "github.com/grafov/m3u8" +) + +var _cleanupTimer *time.Timer + +//SetStreamAsConnected sets the stream as connected +func SetStreamAsConnected() { + _stats.StreamConnected = true + _stats.LastConnectTime = utils.NullTime{time.Now(), true} + _stats.LastDisconnectTime = utils.NullTime{time.Now(), false} + + StopCleanupTimer() + + segmentPath := config.PublicHLSStoragePath + if config.Config.S3.Enabled { + segmentPath = config.PrivateHLSStoragePath + } + + go func() { + _transcoder = ffmpeg.NewTranscoder() + _transcoder.TranscoderCompleted = func(error) { + + SetStreamAsDisconnected() + } + _transcoder.Start() + }() + + ffmpeg.StartThumbnailGenerator(segmentPath, config.Config.VideoSettings.HighestQualityStreamIndex) +} + +//SetStreamAsDisconnected sets the stream as disconnected. +func SetStreamAsDisconnected() { + _stats.StreamConnected = false + _stats.LastDisconnectTime = utils.NullTime{time.Now(), true} + + offlineFilename := "offline.ts" + offlineFilePath := "static/" + offlineFilename + + ffmpeg.StopThumbnailGenerator() + + for index := range config.Config.GetVideoStreamQualities() { + playlistFilePath := fmt.Sprintf(filepath.Join(config.PrivateHLSStoragePath, "%d/stream.m3u8"), index) + segmentFilePath := fmt.Sprintf(filepath.Join(config.PrivateHLSStoragePath, "%d/%s"), index, offlineFilename) + + utils.Copy(offlineFilePath, segmentFilePath) + _storage.Save(segmentFilePath, 0) + + if utils.DoesFileExists(playlistFilePath) { + f, err := os.OpenFile(playlistFilePath, os.O_CREATE|os.O_RDWR, os.ModePerm) + defer f.Close() + if err != nil { + log.Errorln(err) + } + + playlist, _, err := m3u8.DecodeFrom(bufio.NewReader(f), true) + variantPlaylist := playlist.(*m3u8.MediaPlaylist) + if len(variantPlaylist.Segments) > config.Config.GetMaxNumberOfReferencedSegmentsInPlaylist() { + variantPlaylist.Segments = variantPlaylist.Segments[:len(variantPlaylist.Segments)] + } + + err = variantPlaylist.Append(offlineFilename, 8.0, "") + variantPlaylist.SetDiscontinuity() + _, err = f.WriteAt(variantPlaylist.Encode().Bytes(), 0) + if err != nil { + log.Errorln(err) + } + } else { + p, err := m3u8.NewMediaPlaylist(1, 1) + if err != nil { + log.Errorln(err) + } + + // If "offline" content gets changed then change the duration below + err = p.Append(offlineFilename, 8.0, "") + if err != nil { + log.Errorln(err) + } + + p.Close() + f, err := os.Create(playlistFilePath) + if err != nil { + log.Errorln(err) + } + defer f.Close() + _, err = f.Write(p.Encode().Bytes()) + if err != nil { + log.Errorln(err) + } + } + _storage.Save(playlistFilePath, 0) + } + + StartCleanupTimer() +} + +// StartCleanupTimer will fire a cleanup after n minutes being disconnected +func StartCleanupTimer() { + _cleanupTimer = time.NewTimer(5 * time.Minute) + go func() { + for { + select { + case <-_cleanupTimer.C: + // Reset the session count since the session is over + _stats.SessionMaxViewerCount = 0 + resetDirectories() + transitionToOfflineVideoStreamContent() + } + } + }() +} + +// StopCleanupTimer will stop the previous cleanup timer +func StopCleanupTimer() { + if _cleanupTimer != nil { + _cleanupTimer.Stop() + } +} diff --git a/go.mod b/go.mod index 8555d6832..b83a99ee3 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/amalfra/etag v0.0.0-20190921100247-cafc8de96bc5 github.com/aws/aws-sdk-go v1.34.0 github.com/go-ole/go-ole v1.2.4 // indirect + github.com/grafov/m3u8 v0.11.1 + github.com/kr/pretty v0.2.0 // indirect github.com/mattn/go-sqlite3 v1.14.0 github.com/microcosm-cc/bluemonday v1.0.4 github.com/mssola/user_agent v0.5.2 diff --git a/go.sum b/go.sum index 5664cebd8..832820c55 100644 --- a/go.sum +++ b/go.sum @@ -18,10 +18,13 @@ github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNI github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA= +github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= diff --git a/models/playlist.go b/models/playlist.go index c738fa6de..e49c85262 100644 --- a/models/playlist.go +++ b/models/playlist.go @@ -5,10 +5,10 @@ type Segment struct { VariantIndex int // The bitrate variant FullDiskPath string // Where it lives on disk RelativeUploadPath string // Path it should have remotely - RemoteID string + RemoteURL string } -//Variant represents a single bitrate variant and the segments that make it up +//Variant represents a single video variant and the segments that make it up type Variant struct { VariantIndex int Segments map[string]*Segment diff --git a/models/storageProvider.go b/models/storageProvider.go index 4f1737271..0d4ee39db 100644 --- a/models/storageProvider.go +++ b/models/storageProvider.go @@ -1,8 +1,11 @@ package models -//ChunkStorageProvider is how a chunk storage provider should be implemented -type ChunkStorageProvider interface { +//StorageProvider is how a chunk storage provider should be implemented +type StorageProvider interface { Setup() error Save(filePath string, retryCount int) (string, error) - GenerateRemotePlaylist(playlist string, variant Variant) string + + SegmentWritten(localFilePath string) + VariantPlaylistWritten(localFilePath string) + MasterPlaylistWritten(localFilePath string) } diff --git a/router/middleware/caching.go b/router/middleware/caching.go index bcc79ff1d..350197e6f 100644 --- a/router/middleware/caching.go +++ b/router/middleware/caching.go @@ -3,11 +3,11 @@ package middleware import ( "net/http" "os" - "path" "path/filepath" "strconv" "github.com/amalfra/etag" + "github.com/owncast/owncast/utils" ) //DisableCache writes the disable cache header on the responses @@ -42,25 +42,5 @@ func ProcessEtags(w http.ResponseWriter, r *http.Request) int { // SetCachingHeaders will set the cache control header of a response func SetCachingHeaders(w http.ResponseWriter, r *http.Request) { - setCacheSeconds(getCacheDurationSecondsForPath(r.URL.Path), w) -} - -func getCacheDurationSecondsForPath(filePath string) int { - if path.Base(filePath) == "thumbnail.jpg" { - // Thumbnails re-generate during live - return 20 - } else if path.Ext(filePath) == ".js" || path.Ext(filePath) == ".css" { - // Cache javascript & CSS - return 60 - } else if path.Ext(filePath) == ".ts" { - // Cache video segments as long as you want. They can't change. - // This matters most for local hosting of segments for recordings - // and not for live or 3rd party storage. - return 31557600 - } else if path.Ext(filePath) == ".m3u8" { - return 0 - } - - // Default cache length in seconds - return 30 + setCacheSeconds(utils.GetCacheDurationSecondsForPath(r.URL.Path), w) } diff --git a/static/offline.m4v b/static/offline.m4v deleted file mode 100644 index 3c733394b..000000000 Binary files a/static/offline.m4v and /dev/null differ diff --git a/static/offline.ts b/static/offline.ts new file mode 100644 index 000000000..1c67d0963 Binary files /dev/null and b/static/offline.ts differ diff --git a/test/durationTimerTest.go b/test/durationTimerTest.go deleted file mode 100644 index 43b5ce863..000000000 --- a/test/durationTimerTest.go +++ /dev/null @@ -1,19 +0,0 @@ -package test - -import ( - "time" - - log "github.com/sirupsen/logrus" -) - -var timestamp time.Time - -func Mark() { - now := time.Now() - if !timestamp.IsZero() { - delta := now.Sub(timestamp) - log.Println(delta.Milliseconds(), "ms") - } - - timestamp = now -} diff --git a/utils/performanceTimer.go b/utils/performanceTimer.go new file mode 100644 index 000000000..e0abfdae9 --- /dev/null +++ b/utils/performanceTimer.go @@ -0,0 +1,50 @@ +package utils + +import ( + "sort" + "time" +) + +// The "start" timestamp of a timing event +var _pointsInTime = make(map[string]time.Time) + +// A collection of timestamp durations for returning the average of +var _durationStorage = make(map[string][]float64) + +// StartPerformanceMonitor will keep track of the start time of this event +func StartPerformanceMonitor(key string) { + if len(_durationStorage[key]) > 30 { + _durationStorage[key] = removeHighAndLow(_durationStorage[key]) + } + _pointsInTime[key] = time.Now() +} + +// GetAveragePerformance will return the average durations for the event +func GetAveragePerformance(key string) float64 { + timestamp := _pointsInTime[key] + if timestamp.IsZero() { + return 0 + } + + delta := time.Since(timestamp).Seconds() + _durationStorage[key] = append(_durationStorage[key], delta) + if len(_durationStorage[key]) < 10 { + return 0 + } + _durationStorage[key] = removeHighAndLow(_durationStorage[key]) + return avg(_durationStorage[key]) +} + +func removeHighAndLow(values []float64) []float64 { + sort.Float64s(values) + return values[1 : len(values)-1] +} + +func avg(values []float64) float64 { + total := 0.0 + for _, number := range values { + total = total + number + } + average := total / float64(len(values)) + return average +} diff --git a/utils/utils.go b/utils/utils.go index 019d7280e..935e23f3e 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -4,6 +4,7 @@ import ( "bytes" "io/ioutil" "os" + "path" "path/filepath" "strings" @@ -39,7 +40,14 @@ func GetRelativePathFromAbsolutePath(path string) string { return filepath.Join(variant, file) } -//Copy copies the +func GetIndexFromFilePath(path string) string { + pathComponents := strings.Split(path, "/") + variant := pathComponents[len(pathComponents)-2] + + return variant +} + +//Copy copies the file to destination func Copy(source, destination string) error { input, err := ioutil.ReadFile(source) if err != nil { @@ -49,6 +57,11 @@ func Copy(source, destination string) error { return ioutil.WriteFile(destination, input, 0644) } +//Move moves the file to destination +func Move(source, destination string) error { + return os.Rename(source, destination) +} + // IsUserAgentABot returns if a web client user-agent is seen as a bot func IsUserAgentABot(userAgent string) bool { if userAgent == "" { @@ -97,3 +110,24 @@ func RenderSimpleMarkdown(raw string) string { return buf.String() } + +// GetCacheDurationSecondsForPath will return the number of seconds to cache an item +func GetCacheDurationSecondsForPath(filePath string) int { + if path.Base(filePath) == "thumbnail.jpg" { + // Thumbnails re-generate during live + return 20 + } else if path.Ext(filePath) == ".js" || path.Ext(filePath) == ".css" { + // Cache javascript & CSS + return 60 + } else if path.Ext(filePath) == ".ts" { + // Cache video segments as long as you want. They can't change. + // This matters most for local hosting of segments for recordings + // and not for live or 3rd party storage. + return 31557600 + } else if path.Ext(filePath) == ".m3u8" { + return 0 + } + + // Default cache length in seconds + return 30 +} diff --git a/webroot/index-video-only.html b/webroot/index-video-only.html index f0b2643ca..975291289 100644 --- a/webroot/index-video-only.html +++ b/webroot/index-video-only.html @@ -5,7 +5,7 @@ - + diff --git a/webroot/index.html b/webroot/index.html index 60d7a55ea..c60506ad0 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -26,7 +26,7 @@ - + diff --git a/webroot/js/components/player.js b/webroot/js/components/player.js index 22c4d2c77..8c60ac119 100644 --- a/webroot/js/components/player.js +++ b/webroot/js/components/player.js @@ -1,11 +1,11 @@ // https://docs.videojs.com/player -import videojs from '/js/web_modules/videojs/dist/video.min.js'; +import videojs from '/js/web_modules/videojs/core.js'; +import '/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js'; import { getLocalStorage, setLocalStorage } from '../utils/helpers.js'; import { PLAYER_VOLUME } from '../utils/constants.js'; const VIDEO_ID = 'video'; -// TODO: This directory is customizable in the config. So we should expose this via the config API. const URL_STREAM = `/hls/stream.m3u8`; // Video setup diff --git a/webroot/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js b/webroot/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js index a221d101f..7b077c24a 100644 --- a/webroot/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js +++ b/webroot/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js @@ -1,3999 +1,4 @@ -import { c as createCommonjsModule, a as commonjsGlobal, d as document_1, w as window_1$1, g as getDefaultExportFromCjs } from '../../../common/window-1e586371.js'; - -var _extends_1 = createCommonjsModule(function (module) { -function _extends() { - module.exports = _extends = Object.assign || function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; - }; - - return _extends.apply(this, arguments); -} - -module.exports = _extends; -}); - -function _assertThisInitialized(self) { - if (self === void 0) { - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - } - - return self; -} - -var assertThisInitialized = _assertThisInitialized; - -var _typeof_1 = createCommonjsModule(function (module) { -function _typeof(obj) { - "@babel/helpers - typeof"; - - if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { - module.exports = _typeof = function _typeof(obj) { - return typeof obj; - }; - } else { - module.exports = _typeof = function _typeof(obj) { - return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; - }; - } - - return _typeof(obj); -} - -module.exports = _typeof; -}); - -var getPrototypeOf = createCommonjsModule(function (module) { -function _getPrototypeOf(o) { - module.exports = _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { - return o.__proto__ || Object.getPrototypeOf(o); - }; - return _getPrototypeOf(o); -} - -module.exports = _getPrototypeOf; -}); - -function _inheritsLoose(subClass, superClass) { - subClass.prototype = Object.create(superClass.prototype); - subClass.prototype.constructor = subClass; - subClass.__proto__ = superClass; -} - -var inheritsLoose = _inheritsLoose; - -var tuple = SafeParseTuple; - -function SafeParseTuple(obj, reviver) { - var json; - var error = null; - - try { - json = JSON.parse(obj, reviver); - } catch (err) { - error = err; - } - - return [error, json] -} - -var keycode = createCommonjsModule(function (module, exports) { -// Source: http://jsfiddle.net/vWx8V/ -// http://stackoverflow.com/questions/5603195/full-list-of-javascript-keycodes - -/** - * Conenience method returns corresponding value for given keyName or keyCode. - * - * @param {Mixed} keyCode {Number} or keyName {String} - * @return {Mixed} - * @api public - */ - -function keyCode(searchInput) { - // Keyboard Events - if (searchInput && 'object' === typeof searchInput) { - var hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode; - if (hasKeyCode) searchInput = hasKeyCode; - } - - // Numbers - if ('number' === typeof searchInput) return names[searchInput] - - // Everything else (cast to string) - var search = String(searchInput); - - // check codes - var foundNamedKey = codes[search.toLowerCase()]; - if (foundNamedKey) return foundNamedKey - - // check aliases - var foundNamedKey = aliases[search.toLowerCase()]; - if (foundNamedKey) return foundNamedKey - - // weird character? - if (search.length === 1) return search.charCodeAt(0) - - return undefined -} - -/** - * Compares a keyboard event with a given keyCode or keyName. - * - * @param {Event} event Keyboard event that should be tested - * @param {Mixed} keyCode {Number} or keyName {String} - * @return {Boolean} - * @api public - */ -keyCode.isEventKey = function isEventKey(event, nameOrCode) { - if (event && 'object' === typeof event) { - var keyCode = event.which || event.keyCode || event.charCode; - if (keyCode === null || keyCode === undefined) { return false; } - if (typeof nameOrCode === 'string') { - // check codes - var foundNamedKey = codes[nameOrCode.toLowerCase()]; - if (foundNamedKey) { return foundNamedKey === keyCode; } - - // check aliases - var foundNamedKey = aliases[nameOrCode.toLowerCase()]; - if (foundNamedKey) { return foundNamedKey === keyCode; } - } else if (typeof nameOrCode === 'number') { - return nameOrCode === keyCode; - } - return false; - } -}; - -exports = module.exports = keyCode; - -/** - * Get by name - * - * exports.code['enter'] // => 13 - */ - -var codes = exports.code = exports.codes = { - 'backspace': 8, - 'tab': 9, - 'enter': 13, - 'shift': 16, - 'ctrl': 17, - 'alt': 18, - 'pause/break': 19, - 'caps lock': 20, - 'esc': 27, - 'space': 32, - 'page up': 33, - 'page down': 34, - 'end': 35, - 'home': 36, - 'left': 37, - 'up': 38, - 'right': 39, - 'down': 40, - 'insert': 45, - 'delete': 46, - 'command': 91, - 'left command': 91, - 'right command': 93, - 'numpad *': 106, - 'numpad +': 107, - 'numpad -': 109, - 'numpad .': 110, - 'numpad /': 111, - 'num lock': 144, - 'scroll lock': 145, - 'my computer': 182, - 'my calculator': 183, - ';': 186, - '=': 187, - ',': 188, - '-': 189, - '.': 190, - '/': 191, - '`': 192, - '[': 219, - '\\': 220, - ']': 221, - "'": 222 -}; - -// Helper aliases - -var aliases = exports.aliases = { - 'windows': 91, - '⇧': 16, - '⌥': 18, - '⌃': 17, - '⌘': 91, - 'ctl': 17, - 'control': 17, - 'option': 18, - 'pause': 19, - 'break': 19, - 'caps': 20, - 'return': 13, - 'escape': 27, - 'spc': 32, - 'spacebar': 32, - 'pgup': 33, - 'pgdn': 34, - 'ins': 45, - 'del': 46, - 'cmd': 91 -}; - -/*! - * Programatically add the following - */ - -// lower case chars -for (i = 97; i < 123; i++) codes[String.fromCharCode(i)] = i - 32; - -// numbers -for (var i = 48; i < 58; i++) codes[i - 48] = i; - -// function keys -for (i = 1; i < 13; i++) codes['f'+i] = i + 111; - -// numpad keys -for (i = 0; i < 10; i++) codes['numpad '+i] = i + 96; - -/** - * Get by code - * - * exports.name[13] // => 'Enter' - */ - -var names = exports.names = exports.title = {}; // title for backward compat - -// Create reverse mapping -for (i in codes) names[codes[i]] = i; - -// Add aliases -for (var alias in aliases) { - codes[alias] = aliases[alias]; -} -}); - -var win; - -if (typeof window !== "undefined") { - win = window; -} else if (typeof commonjsGlobal !== "undefined") { - win = commonjsGlobal; -} else if (typeof self !== "undefined"){ - win = self; -} else { - win = {}; -} - -var window_1 = win; - -var isFunction_1 = isFunction; - -var toString = Object.prototype.toString; - -function isFunction (fn) { - if (!fn) { - return false - } - var string = toString.call(fn); - return string === '[object Function]' || - (typeof fn === 'function' && string !== '[object RegExp]') || - (typeof window !== 'undefined' && - // IE8 and below - (fn === window.setTimeout || - fn === window.alert || - fn === window.confirm || - fn === window.prompt)) -} - -/** - * @license - * slighly modified parse-headers 2.0.2 - * Copyright (c) 2014 David Björklund - * Available under the MIT license - * - */ - -var parseHeaders = function(headers) { - var result = {}; - - if (!headers) { - return result; - } - - headers.trim().split('\n').forEach(function(row) { - var index = row.indexOf(':'); - var key = row.slice(0, index).trim().toLowerCase(); - var value = row.slice(index + 1).trim(); - - if (typeof(result[key]) === 'undefined') { - result[key] = value; - } else if (Array.isArray(result[key])) { - result[key].push(value); - } else { - result[key] = [ result[key], value ]; - } - }); - - return result; -}; - -var xhr = createXHR; -// Allow use of default import syntax in TypeScript -var _default = createXHR; -createXHR.XMLHttpRequest = window_1.XMLHttpRequest || noop; -createXHR.XDomainRequest = "withCredentials" in (new createXHR.XMLHttpRequest()) ? createXHR.XMLHttpRequest : window_1.XDomainRequest; - -forEachArray(["get", "put", "post", "patch", "head", "delete"], function(method) { - createXHR[method === "delete" ? "del" : method] = function(uri, options, callback) { - options = initParams(uri, options, callback); - options.method = method.toUpperCase(); - return _createXHR(options) - }; -}); - -function forEachArray(array, iterator) { - for (var i = 0; i < array.length; i++) { - iterator(array[i]); - } -} - -function isEmpty(obj){ - for(var i in obj){ - if(obj.hasOwnProperty(i)) return false - } - return true -} - -function initParams(uri, options, callback) { - var params = uri; - - if (isFunction_1(options)) { - callback = options; - if (typeof uri === "string") { - params = {uri:uri}; - } - } else { - params = _extends_1({}, options, {uri: uri}); - } - - params.callback = callback; - return params -} - -function createXHR(uri, options, callback) { - options = initParams(uri, options, callback); - return _createXHR(options) -} - -function _createXHR(options) { - if(typeof options.callback === "undefined"){ - throw new Error("callback argument missing") - } - - var called = false; - var callback = function cbOnce(err, response, body){ - if(!called){ - called = true; - options.callback(err, response, body); - } - }; - - function readystatechange() { - if (xhr.readyState === 4) { - setTimeout(loadFunc, 0); - } - } - - function getBody() { - // Chrome with requestType=blob throws errors arround when even testing access to responseText - var body = undefined; - - if (xhr.response) { - body = xhr.response; - } else { - body = xhr.responseText || getXml(xhr); - } - - if (isJson) { - try { - body = JSON.parse(body); - } catch (e) {} - } - - return body - } - - function errorFunc(evt) { - clearTimeout(timeoutTimer); - if(!(evt instanceof Error)){ - evt = new Error("" + (evt || "Unknown XMLHttpRequest Error") ); - } - evt.statusCode = 0; - return callback(evt, failureResponse) - } - - // will load the data & process the response in a special response object - function loadFunc() { - if (aborted) return - var status; - clearTimeout(timeoutTimer); - if(options.useXDR && xhr.status===undefined) { - //IE8 CORS GET successful response doesn't have a status field, but body is fine - status = 200; - } else { - status = (xhr.status === 1223 ? 204 : xhr.status); - } - var response = failureResponse; - var err = null; - - if (status !== 0){ - response = { - body: getBody(), - statusCode: status, - method: method, - headers: {}, - url: uri, - rawRequest: xhr - }; - if(xhr.getAllResponseHeaders){ //remember xhr can in fact be XDR for CORS in IE - response.headers = parseHeaders(xhr.getAllResponseHeaders()); - } - } else { - err = new Error("Internal XMLHttpRequest Error"); - } - return callback(err, response, response.body) - } - - var xhr = options.xhr || null; - - if (!xhr) { - if (options.cors || options.useXDR) { - xhr = new createXHR.XDomainRequest(); - }else { - xhr = new createXHR.XMLHttpRequest(); - } - } - - var key; - var aborted; - var uri = xhr.url = options.uri || options.url; - var method = xhr.method = options.method || "GET"; - var body = options.body || options.data; - var headers = xhr.headers = options.headers || {}; - var sync = !!options.sync; - var isJson = false; - var timeoutTimer; - var failureResponse = { - body: undefined, - headers: {}, - statusCode: 0, - method: method, - url: uri, - rawRequest: xhr - }; - - if ("json" in options && options.json !== false) { - isJson = true; - headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user - if (method !== "GET" && method !== "HEAD") { - headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user - body = JSON.stringify(options.json === true ? body : options.json); - } - } - - xhr.onreadystatechange = readystatechange; - xhr.onload = loadFunc; - xhr.onerror = errorFunc; - // IE9 must have onprogress be set to a unique function. - xhr.onprogress = function () { - // IE must die - }; - xhr.onabort = function(){ - aborted = true; - }; - xhr.ontimeout = errorFunc; - xhr.open(method, uri, !sync, options.username, options.password); - //has to be after open - if(!sync) { - xhr.withCredentials = !!options.withCredentials; - } - // Cannot set timeout with sync request - // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly - // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent - if (!sync && options.timeout > 0 ) { - timeoutTimer = setTimeout(function(){ - if (aborted) return - aborted = true;//IE9 may still call readystatechange - xhr.abort("timeout"); - var e = new Error("XMLHttpRequest timeout"); - e.code = "ETIMEDOUT"; - errorFunc(e); - }, options.timeout ); - } - - if (xhr.setRequestHeader) { - for(key in headers){ - if(headers.hasOwnProperty(key)){ - xhr.setRequestHeader(key, headers[key]); - } - } - } else if (options.headers && !isEmpty(options.headers)) { - throw new Error("Headers cannot be set on an XDomainRequest object") - } - - if ("responseType" in options) { - xhr.responseType = options.responseType; - } - - if ("beforeSend" in options && - typeof options.beforeSend === "function" - ) { - options.beforeSend(xhr); - } - - // Microsoft Edge browser sends "undefined" when send is called with undefined value. - // XMLHttpRequest spec says to pass null as body to indicate no body - // See https://github.com/naugtur/xhr/issues/100. - xhr.send(body || null); - - return xhr - - -} - -function getXml(xhr) { - // xhr.responseXML will throw Exception "InvalidStateError" or "DOMException" - // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseXML. - try { - if (xhr.responseType === "document") { - return xhr.responseXML - } - var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror"; - if (xhr.responseType === "" && !firefoxBugTakenEffect) { - return xhr.responseXML - } - } catch (e) {} - - return null -} - -function noop() {} -xhr.default = _default; - -/** - * Copyright 2013 vtt.js Contributors - * - * 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. - */ - -/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ - - -var _objCreate = Object.create || (function() { - function F() {} - return function(o) { - if (arguments.length !== 1) { - throw new Error('Object.create shim only accepts one parameter.'); - } - F.prototype = o; - return new F(); - }; -})(); - -// Creates a new ParserError object from an errorData object. The errorData -// object should have default code and message properties. The default message -// property can be overriden by passing in a message parameter. -// See ParsingError.Errors below for acceptable errors. -function ParsingError(errorData, message) { - this.name = "ParsingError"; - this.code = errorData.code; - this.message = message || errorData.message; -} -ParsingError.prototype = _objCreate(Error.prototype); -ParsingError.prototype.constructor = ParsingError; - -// ParsingError metadata for acceptable ParsingErrors. -ParsingError.Errors = { - BadSignature: { - code: 0, - message: "Malformed WebVTT signature." - }, - BadTimeStamp: { - code: 1, - message: "Malformed time stamp." - } -}; - -// Try to parse input as a time stamp. -function parseTimeStamp(input) { - - function computeSeconds(h, m, s, f) { - return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000; - } - - var m = input.match(/^(\d+):(\d{1,2})(:\d{1,2})?\.(\d{3})/); - if (!m) { - return null; - } - - if (m[3]) { - // Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds] - return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]); - } else if (m[1] > 59) { - // Timestamp takes the form of [hours]:[minutes].[milliseconds] - // First position is hours as it's over 59. - return computeSeconds(m[1], m[2], 0, m[4]); - } else { - // Timestamp takes the form of [minutes]:[seconds].[milliseconds] - return computeSeconds(0, m[1], m[2], m[4]); - } -} - -// A settings object holds key/value pairs and will ignore anything but the first -// assignment to a specific key. -function Settings() { - this.values = _objCreate(null); -} - -Settings.prototype = { - // Only accept the first assignment to any key. - set: function(k, v) { - if (!this.get(k) && v !== "") { - this.values[k] = v; - } - }, - // Return the value for a key, or a default value. - // If 'defaultKey' is passed then 'dflt' is assumed to be an object with - // a number of possible default values as properties where 'defaultKey' is - // the key of the property that will be chosen; otherwise it's assumed to be - // a single value. - get: function(k, dflt, defaultKey) { - if (defaultKey) { - return this.has(k) ? this.values[k] : dflt[defaultKey]; - } - return this.has(k) ? this.values[k] : dflt; - }, - // Check whether we have a value for a key. - has: function(k) { - return k in this.values; - }, - // Accept a setting if its one of the given alternatives. - alt: function(k, v, a) { - for (var n = 0; n < a.length; ++n) { - if (v === a[n]) { - this.set(k, v); - break; - } - } - }, - // Accept a setting if its a valid (signed) integer. - integer: function(k, v) { - if (/^-?\d+$/.test(v)) { // integer - this.set(k, parseInt(v, 10)); - } - }, - // Accept a setting if its a valid percentage. - percent: function(k, v) { - var m; - if ((m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/))) { - v = parseFloat(v); - if (v >= 0 && v <= 100) { - this.set(k, v); - return true; - } - } - return false; - } -}; - -// Helper function to parse input into groups separated by 'groupDelim', and -// interprete each group as a key/value pair separated by 'keyValueDelim'. -function parseOptions(input, callback, keyValueDelim, groupDelim) { - var groups = groupDelim ? input.split(groupDelim) : [input]; - for (var i in groups) { - if (typeof groups[i] !== "string") { - continue; - } - var kv = groups[i].split(keyValueDelim); - if (kv.length !== 2) { - continue; - } - var k = kv[0]; - var v = kv[1]; - callback(k, v); - } -} - -function parseCue(input, cue, regionList) { - // Remember the original input if we need to throw an error. - var oInput = input; - // 4.1 WebVTT timestamp - function consumeTimeStamp() { - var ts = parseTimeStamp(input); - if (ts === null) { - throw new ParsingError(ParsingError.Errors.BadTimeStamp, - "Malformed timestamp: " + oInput); - } - // Remove time stamp from input. - input = input.replace(/^[^\sa-zA-Z-]+/, ""); - return ts; - } - - // 4.4.2 WebVTT cue settings - function consumeCueSettings(input, cue) { - var settings = new Settings(); - - parseOptions(input, function (k, v) { - switch (k) { - case "region": - // Find the last region we parsed with the same region id. - for (var i = regionList.length - 1; i >= 0; i--) { - if (regionList[i].id === v) { - settings.set(k, regionList[i].region); - break; - } - } - break; - case "vertical": - settings.alt(k, v, ["rl", "lr"]); - break; - case "line": - var vals = v.split(","), - vals0 = vals[0]; - settings.integer(k, vals0); - settings.percent(k, vals0) ? settings.set("snapToLines", false) : null; - settings.alt(k, vals0, ["auto"]); - if (vals.length === 2) { - settings.alt("lineAlign", vals[1], ["start", "center", "end"]); - } - break; - case "position": - vals = v.split(","); - settings.percent(k, vals[0]); - if (vals.length === 2) { - settings.alt("positionAlign", vals[1], ["start", "center", "end"]); - } - break; - case "size": - settings.percent(k, v); - break; - case "align": - settings.alt(k, v, ["start", "center", "end", "left", "right"]); - break; - } - }, /:/, /\s/); - - // Apply default values for any missing fields. - cue.region = settings.get("region", null); - cue.vertical = settings.get("vertical", ""); - try { - cue.line = settings.get("line", "auto"); - } catch (e) {} - cue.lineAlign = settings.get("lineAlign", "start"); - cue.snapToLines = settings.get("snapToLines", true); - cue.size = settings.get("size", 100); - // Safari still uses the old middle value and won't accept center - try { - cue.align = settings.get("align", "center"); - } catch (e) { - cue.align = settings.get("align", "middle"); - } - try { - cue.position = settings.get("position", "auto"); - } catch (e) { - cue.position = settings.get("position", { - start: 0, - left: 0, - center: 50, - middle: 50, - end: 100, - right: 100 - }, cue.align); - } - - - cue.positionAlign = settings.get("positionAlign", { - start: "start", - left: "start", - center: "center", - middle: "center", - end: "end", - right: "end" - }, cue.align); - } - - function skipWhitespace() { - input = input.replace(/^\s+/, ""); - } - - // 4.1 WebVTT cue timings. - skipWhitespace(); - cue.startTime = consumeTimeStamp(); // (1) collect cue start time - skipWhitespace(); - if (input.substr(0, 3) !== "-->") { // (3) next characters must match "-->" - throw new ParsingError(ParsingError.Errors.BadTimeStamp, - "Malformed time stamp (time stamps must be separated by '-->'): " + - oInput); - } - input = input.substr(3); - skipWhitespace(); - cue.endTime = consumeTimeStamp(); // (5) collect cue end time - - // 4.1 WebVTT cue settings list. - skipWhitespace(); - consumeCueSettings(input, cue); -} - -var TEXTAREA_ELEMENT = document_1.createElement("textarea"); - -var TAG_NAME = { - c: "span", - i: "i", - b: "b", - u: "u", - ruby: "ruby", - rt: "rt", - v: "span", - lang: "span" -}; - -// 5.1 default text color -// 5.2 default text background color is equivalent to text color with bg_ prefix -var DEFAULT_COLOR_CLASS = { - white: 'rgba(255,255,255,1)', - lime: 'rgba(0,255,0,1)', - cyan: 'rgba(0,255,255,1)', - red: 'rgba(255,0,0,1)', - yellow: 'rgba(255,255,0,1)', - magenta: 'rgba(255,0,255,1)', - blue: 'rgba(0,0,255,1)', - black: 'rgba(0,0,0,1)' -}; - -var TAG_ANNOTATION = { - v: "title", - lang: "lang" -}; - -var NEEDS_PARENT = { - rt: "ruby" -}; - -// Parse content into a document fragment. -function parseContent(window, input) { - function nextToken() { - // Check for end-of-string. - if (!input) { - return null; - } - - // Consume 'n' characters from the input. - function consume(result) { - input = input.substr(result.length); - return result; - } - - var m = input.match(/^([^<]*)(<[^>]*>?)?/); - // If there is some text before the next tag, return it, otherwise return - // the tag. - return consume(m[1] ? m[1] : m[2]); - } - - function unescape(s) { - TEXTAREA_ELEMENT.innerHTML = s; - s = TEXTAREA_ELEMENT.textContent; - TEXTAREA_ELEMENT.textContent = ""; - return s; - } - - function shouldAdd(current, element) { - return !NEEDS_PARENT[element.localName] || - NEEDS_PARENT[element.localName] === current.localName; - } - - // Create an element for this tag. - function createElement(type, annotation) { - var tagName = TAG_NAME[type]; - if (!tagName) { - return null; - } - var element = window.document.createElement(tagName); - var name = TAG_ANNOTATION[type]; - if (name && annotation) { - element[name] = annotation.trim(); - } - return element; - } - - var rootDiv = window.document.createElement("div"), - current = rootDiv, - t, - tagStack = []; - - while ((t = nextToken()) !== null) { - if (t[0] === '<') { - if (t[1] === "/") { - // If the closing tag matches, move back up to the parent node. - if (tagStack.length && - tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) { - tagStack.pop(); - current = current.parentNode; - } - // Otherwise just ignore the end tag. - continue; - } - var ts = parseTimeStamp(t.substr(1, t.length - 2)); - var node; - if (ts) { - // Timestamps are lead nodes as well. - node = window.document.createProcessingInstruction("timestamp", ts); - current.appendChild(node); - continue; - } - var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/); - // If we can't parse the tag, skip to the next tag. - if (!m) { - continue; - } - // Try to construct an element, and ignore the tag if we couldn't. - node = createElement(m[1], m[3]); - if (!node) { - continue; - } - // Determine if the tag should be added based on the context of where it - // is placed in the cuetext. - if (!shouldAdd(current, node)) { - continue; - } - // Set the class list (as a list of classes, separated by space). - if (m[2]) { - var classes = m[2].split('.'); - - classes.forEach(function(cl) { - var bgColor = /^bg_/.test(cl); - // slice out `bg_` if it's a background color - var colorName = bgColor ? cl.slice(3) : cl; - - if (DEFAULT_COLOR_CLASS.hasOwnProperty(colorName)) { - var propName = bgColor ? 'background-color' : 'color'; - var propValue = DEFAULT_COLOR_CLASS[colorName]; - - node.style[propName] = propValue; - } - }); - - node.className = classes.join(' '); - } - // Append the node to the current node, and enter the scope of the new - // node. - tagStack.push(m[1]); - current.appendChild(node); - current = node; - continue; - } - - // Text nodes are leaf nodes. - current.appendChild(window.document.createTextNode(unescape(t))); - } - - return rootDiv; -} - -// This is a list of all the Unicode characters that have a strong -// right-to-left category. What this means is that these characters are -// written right-to-left for sure. It was generated by pulling all the strong -// right-to-left characters out of the Unicode data table. That table can -// found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt -var strongRTLRanges = [[0x5be, 0x5be], [0x5c0, 0x5c0], [0x5c3, 0x5c3], [0x5c6, 0x5c6], - [0x5d0, 0x5ea], [0x5f0, 0x5f4], [0x608, 0x608], [0x60b, 0x60b], [0x60d, 0x60d], - [0x61b, 0x61b], [0x61e, 0x64a], [0x66d, 0x66f], [0x671, 0x6d5], [0x6e5, 0x6e6], - [0x6ee, 0x6ef], [0x6fa, 0x70d], [0x70f, 0x710], [0x712, 0x72f], [0x74d, 0x7a5], - [0x7b1, 0x7b1], [0x7c0, 0x7ea], [0x7f4, 0x7f5], [0x7fa, 0x7fa], [0x800, 0x815], - [0x81a, 0x81a], [0x824, 0x824], [0x828, 0x828], [0x830, 0x83e], [0x840, 0x858], - [0x85e, 0x85e], [0x8a0, 0x8a0], [0x8a2, 0x8ac], [0x200f, 0x200f], - [0xfb1d, 0xfb1d], [0xfb1f, 0xfb28], [0xfb2a, 0xfb36], [0xfb38, 0xfb3c], - [0xfb3e, 0xfb3e], [0xfb40, 0xfb41], [0xfb43, 0xfb44], [0xfb46, 0xfbc1], - [0xfbd3, 0xfd3d], [0xfd50, 0xfd8f], [0xfd92, 0xfdc7], [0xfdf0, 0xfdfc], - [0xfe70, 0xfe74], [0xfe76, 0xfefc], [0x10800, 0x10805], [0x10808, 0x10808], - [0x1080a, 0x10835], [0x10837, 0x10838], [0x1083c, 0x1083c], [0x1083f, 0x10855], - [0x10857, 0x1085f], [0x10900, 0x1091b], [0x10920, 0x10939], [0x1093f, 0x1093f], - [0x10980, 0x109b7], [0x109be, 0x109bf], [0x10a00, 0x10a00], [0x10a10, 0x10a13], - [0x10a15, 0x10a17], [0x10a19, 0x10a33], [0x10a40, 0x10a47], [0x10a50, 0x10a58], - [0x10a60, 0x10a7f], [0x10b00, 0x10b35], [0x10b40, 0x10b55], [0x10b58, 0x10b72], - [0x10b78, 0x10b7f], [0x10c00, 0x10c48], [0x1ee00, 0x1ee03], [0x1ee05, 0x1ee1f], - [0x1ee21, 0x1ee22], [0x1ee24, 0x1ee24], [0x1ee27, 0x1ee27], [0x1ee29, 0x1ee32], - [0x1ee34, 0x1ee37], [0x1ee39, 0x1ee39], [0x1ee3b, 0x1ee3b], [0x1ee42, 0x1ee42], - [0x1ee47, 0x1ee47], [0x1ee49, 0x1ee49], [0x1ee4b, 0x1ee4b], [0x1ee4d, 0x1ee4f], - [0x1ee51, 0x1ee52], [0x1ee54, 0x1ee54], [0x1ee57, 0x1ee57], [0x1ee59, 0x1ee59], - [0x1ee5b, 0x1ee5b], [0x1ee5d, 0x1ee5d], [0x1ee5f, 0x1ee5f], [0x1ee61, 0x1ee62], - [0x1ee64, 0x1ee64], [0x1ee67, 0x1ee6a], [0x1ee6c, 0x1ee72], [0x1ee74, 0x1ee77], - [0x1ee79, 0x1ee7c], [0x1ee7e, 0x1ee7e], [0x1ee80, 0x1ee89], [0x1ee8b, 0x1ee9b], - [0x1eea1, 0x1eea3], [0x1eea5, 0x1eea9], [0x1eeab, 0x1eebb], [0x10fffd, 0x10fffd]]; - -function isStrongRTLChar(charCode) { - for (var i = 0; i < strongRTLRanges.length; i++) { - var currentRange = strongRTLRanges[i]; - if (charCode >= currentRange[0] && charCode <= currentRange[1]) { - return true; - } - } - - return false; -} - -function determineBidi(cueDiv) { - var nodeStack = [], - text = "", - charCode; - - if (!cueDiv || !cueDiv.childNodes) { - return "ltr"; - } - - function pushNodes(nodeStack, node) { - for (var i = node.childNodes.length - 1; i >= 0; i--) { - nodeStack.push(node.childNodes[i]); - } - } - - function nextTextNode(nodeStack) { - if (!nodeStack || !nodeStack.length) { - return null; - } - - var node = nodeStack.pop(), - text = node.textContent || node.innerText; - if (text) { - // TODO: This should match all unicode type B characters (paragraph - // separator characters). See issue #115. - var m = text.match(/^.*(\n|\r)/); - if (m) { - nodeStack.length = 0; - return m[0]; - } - return text; - } - if (node.tagName === "ruby") { - return nextTextNode(nodeStack); - } - if (node.childNodes) { - pushNodes(nodeStack, node); - return nextTextNode(nodeStack); - } - } - - pushNodes(nodeStack, cueDiv); - while ((text = nextTextNode(nodeStack))) { - for (var i = 0; i < text.length; i++) { - charCode = text.charCodeAt(i); - if (isStrongRTLChar(charCode)) { - return "rtl"; - } - } - } - return "ltr"; -} - -function computeLinePos(cue) { - if (typeof cue.line === "number" && - (cue.snapToLines || (cue.line >= 0 && cue.line <= 100))) { - return cue.line; - } - if (!cue.track || !cue.track.textTrackList || - !cue.track.textTrackList.mediaElement) { - return -1; - } - var track = cue.track, - trackList = track.textTrackList, - count = 0; - for (var i = 0; i < trackList.length && trackList[i] !== track; i++) { - if (trackList[i].mode === "showing") { - count++; - } - } - return ++count * -1; -} - -function StyleBox() { -} - -// Apply styles to a div. If there is no div passed then it defaults to the -// div on 'this'. -StyleBox.prototype.applyStyles = function(styles, div) { - div = div || this.div; - for (var prop in styles) { - if (styles.hasOwnProperty(prop)) { - div.style[prop] = styles[prop]; - } - } -}; - -StyleBox.prototype.formatStyle = function(val, unit) { - return val === 0 ? 0 : val + unit; -}; - -// Constructs the computed display state of the cue (a div). Places the div -// into the overlay which should be a block level element (usually a div). -function CueStyleBox(window, cue, styleOptions) { - StyleBox.call(this); - this.cue = cue; - - // Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will - // have inline positioning and will function as the cue background box. - this.cueDiv = parseContent(window, cue.text); - var styles = { - color: "rgba(255, 255, 255, 1)", - backgroundColor: "rgba(0, 0, 0, 0.8)", - position: "relative", - left: 0, - right: 0, - top: 0, - bottom: 0, - display: "inline", - writingMode: cue.vertical === "" ? "horizontal-tb" - : cue.vertical === "lr" ? "vertical-lr" - : "vertical-rl", - unicodeBidi: "plaintext" - }; - - this.applyStyles(styles, this.cueDiv); - - // Create an absolutely positioned div that will be used to position the cue - // div. Note, all WebVTT cue-setting alignments are equivalent to the CSS - // mirrors of them except middle instead of center on Safari. - this.div = window.document.createElement("div"); - styles = { - direction: determineBidi(this.cueDiv), - writingMode: cue.vertical === "" ? "horizontal-tb" - : cue.vertical === "lr" ? "vertical-lr" - : "vertical-rl", - unicodeBidi: "plaintext", - textAlign: cue.align === "middle" ? "center" : cue.align, - font: styleOptions.font, - whiteSpace: "pre-line", - position: "absolute" - }; - - this.applyStyles(styles); - this.div.appendChild(this.cueDiv); - - // Calculate the distance from the reference edge of the viewport to the text - // position of the cue box. The reference edge will be resolved later when - // the box orientation styles are applied. - var textPos = 0; - switch (cue.positionAlign) { - case "start": - textPos = cue.position; - break; - case "center": - textPos = cue.position - (cue.size / 2); - break; - case "end": - textPos = cue.position - cue.size; - break; - } - - // Horizontal box orientation; textPos is the distance from the left edge of the - // area to the left edge of the box and cue.size is the distance extending to - // the right from there. - if (cue.vertical === "") { - this.applyStyles({ - left: this.formatStyle(textPos, "%"), - width: this.formatStyle(cue.size, "%") - }); - // Vertical box orientation; textPos is the distance from the top edge of the - // area to the top edge of the box and cue.size is the height extending - // downwards from there. - } else { - this.applyStyles({ - top: this.formatStyle(textPos, "%"), - height: this.formatStyle(cue.size, "%") - }); - } - - this.move = function(box) { - this.applyStyles({ - top: this.formatStyle(box.top, "px"), - bottom: this.formatStyle(box.bottom, "px"), - left: this.formatStyle(box.left, "px"), - right: this.formatStyle(box.right, "px"), - height: this.formatStyle(box.height, "px"), - width: this.formatStyle(box.width, "px") - }); - }; -} -CueStyleBox.prototype = _objCreate(StyleBox.prototype); -CueStyleBox.prototype.constructor = CueStyleBox; - -// Represents the co-ordinates of an Element in a way that we can easily -// compute things with such as if it overlaps or intersects with another Element. -// Can initialize it with either a StyleBox or another BoxPosition. -function BoxPosition(obj) { - // Either a BoxPosition was passed in and we need to copy it, or a StyleBox - // was passed in and we need to copy the results of 'getBoundingClientRect' - // as the object returned is readonly. All co-ordinate values are in reference - // to the viewport origin (top left). - var lh, height, width, top; - if (obj.div) { - height = obj.div.offsetHeight; - width = obj.div.offsetWidth; - top = obj.div.offsetTop; - - var rects = (rects = obj.div.childNodes) && (rects = rects[0]) && - rects.getClientRects && rects.getClientRects(); - obj = obj.div.getBoundingClientRect(); - // In certain cases the outter div will be slightly larger then the sum of - // the inner div's lines. This could be due to bold text, etc, on some platforms. - // In this case we should get the average line height and use that. This will - // result in the desired behaviour. - lh = rects ? Math.max((rects[0] && rects[0].height) || 0, obj.height / rects.length) - : 0; - - } - this.left = obj.left; - this.right = obj.right; - this.top = obj.top || top; - this.height = obj.height || height; - this.bottom = obj.bottom || (top + (obj.height || height)); - this.width = obj.width || width; - this.lineHeight = lh !== undefined ? lh : obj.lineHeight; -} - -// Move the box along a particular axis. Optionally pass in an amount to move -// the box. If no amount is passed then the default is the line height of the -// box. -BoxPosition.prototype.move = function(axis, toMove) { - toMove = toMove !== undefined ? toMove : this.lineHeight; - switch (axis) { - case "+x": - this.left += toMove; - this.right += toMove; - break; - case "-x": - this.left -= toMove; - this.right -= toMove; - break; - case "+y": - this.top += toMove; - this.bottom += toMove; - break; - case "-y": - this.top -= toMove; - this.bottom -= toMove; - break; - } -}; - -// Check if this box overlaps another box, b2. -BoxPosition.prototype.overlaps = function(b2) { - return this.left < b2.right && - this.right > b2.left && - this.top < b2.bottom && - this.bottom > b2.top; -}; - -// Check if this box overlaps any other boxes in boxes. -BoxPosition.prototype.overlapsAny = function(boxes) { - for (var i = 0; i < boxes.length; i++) { - if (this.overlaps(boxes[i])) { - return true; - } - } - return false; -}; - -// Check if this box is within another box. -BoxPosition.prototype.within = function(container) { - return this.top >= container.top && - this.bottom <= container.bottom && - this.left >= container.left && - this.right <= container.right; -}; - -// Check if this box is entirely within the container or it is overlapping -// on the edge opposite of the axis direction passed. For example, if "+x" is -// passed and the box is overlapping on the left edge of the container, then -// return true. -BoxPosition.prototype.overlapsOppositeAxis = function(container, axis) { - switch (axis) { - case "+x": - return this.left < container.left; - case "-x": - return this.right > container.right; - case "+y": - return this.top < container.top; - case "-y": - return this.bottom > container.bottom; - } -}; - -// Find the percentage of the area that this box is overlapping with another -// box. -BoxPosition.prototype.intersectPercentage = function(b2) { - var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)), - y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)), - intersectArea = x * y; - return intersectArea / (this.height * this.width); -}; - -// Convert the positions from this box to CSS compatible positions using -// the reference container's positions. This has to be done because this -// box's positions are in reference to the viewport origin, whereas, CSS -// values are in referecne to their respective edges. -BoxPosition.prototype.toCSSCompatValues = function(reference) { - return { - top: this.top - reference.top, - bottom: reference.bottom - this.bottom, - left: this.left - reference.left, - right: reference.right - this.right, - height: this.height, - width: this.width - }; -}; - -// Get an object that represents the box's position without anything extra. -// Can pass a StyleBox, HTMLElement, or another BoxPositon. -BoxPosition.getSimpleBoxPosition = function(obj) { - var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0; - var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0; - var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0; - - obj = obj.div ? obj.div.getBoundingClientRect() : - obj.tagName ? obj.getBoundingClientRect() : obj; - var ret = { - left: obj.left, - right: obj.right, - top: obj.top || top, - height: obj.height || height, - bottom: obj.bottom || (top + (obj.height || height)), - width: obj.width || width - }; - return ret; -}; - -// Move a StyleBox to its specified, or next best, position. The containerBox -// is the box that contains the StyleBox, such as a div. boxPositions are -// a list of other boxes that the styleBox can't overlap with. -function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) { - - // Find the best position for a cue box, b, on the video. The axis parameter - // is a list of axis, the order of which, it will move the box along. For example: - // Passing ["+x", "-x"] will move the box first along the x axis in the positive - // direction. If it doesn't find a good position for it there it will then move - // it along the x axis in the negative direction. - function findBestPosition(b, axis) { - var bestPosition, - specifiedPosition = new BoxPosition(b), - percentage = 1; // Highest possible so the first thing we get is better. - - for (var i = 0; i < axis.length; i++) { - while (b.overlapsOppositeAxis(containerBox, axis[i]) || - (b.within(containerBox) && b.overlapsAny(boxPositions))) { - b.move(axis[i]); - } - // We found a spot where we aren't overlapping anything. This is our - // best position. - if (b.within(containerBox)) { - return b; - } - var p = b.intersectPercentage(containerBox); - // If we're outside the container box less then we were on our last try - // then remember this position as the best position. - if (percentage > p) { - bestPosition = new BoxPosition(b); - percentage = p; - } - // Reset the box position to the specified position. - b = new BoxPosition(specifiedPosition); - } - return bestPosition || specifiedPosition; - } - - var boxPosition = new BoxPosition(styleBox), - cue = styleBox.cue, - linePos = computeLinePos(cue), - axis = []; - - // If we have a line number to align the cue to. - if (cue.snapToLines) { - var size; - switch (cue.vertical) { - case "": - axis = [ "+y", "-y" ]; - size = "height"; - break; - case "rl": - axis = [ "+x", "-x" ]; - size = "width"; - break; - case "lr": - axis = [ "-x", "+x" ]; - size = "width"; - break; - } - - var step = boxPosition.lineHeight, - position = step * Math.round(linePos), - maxPosition = containerBox[size] + step, - initialAxis = axis[0]; - - // If the specified intial position is greater then the max position then - // clamp the box to the amount of steps it would take for the box to - // reach the max position. - if (Math.abs(position) > maxPosition) { - position = position < 0 ? -1 : 1; - position *= Math.ceil(maxPosition / step) * step; - } - - // If computed line position returns negative then line numbers are - // relative to the bottom of the video instead of the top. Therefore, we - // need to increase our initial position by the length or width of the - // video, depending on the writing direction, and reverse our axis directions. - if (linePos < 0) { - position += cue.vertical === "" ? containerBox.height : containerBox.width; - axis = axis.reverse(); - } - - // Move the box to the specified position. This may not be its best - // position. - boxPosition.move(initialAxis, position); - - } else { - // If we have a percentage line value for the cue. - var calculatedPercentage = (boxPosition.lineHeight / containerBox.height) * 100; - - switch (cue.lineAlign) { - case "center": - linePos -= (calculatedPercentage / 2); - break; - case "end": - linePos -= calculatedPercentage; - break; - } - - // Apply initial line position to the cue box. - switch (cue.vertical) { - case "": - styleBox.applyStyles({ - top: styleBox.formatStyle(linePos, "%") - }); - break; - case "rl": - styleBox.applyStyles({ - left: styleBox.formatStyle(linePos, "%") - }); - break; - case "lr": - styleBox.applyStyles({ - right: styleBox.formatStyle(linePos, "%") - }); - break; - } - - axis = [ "+y", "-x", "+x", "-y" ]; - - // Get the box position again after we've applied the specified positioning - // to it. - boxPosition = new BoxPosition(styleBox); - } - - var bestPosition = findBestPosition(boxPosition, axis); - styleBox.move(bestPosition.toCSSCompatValues(containerBox)); -} - -function WebVTT$1() { - // Nothing -} - -// Helper to allow strings to be decoded instead of the default binary utf8 data. -WebVTT$1.StringDecoder = function() { - return { - decode: function(data) { - if (!data) { - return ""; - } - if (typeof data !== "string") { - throw new Error("Error - expected string data."); - } - return decodeURIComponent(encodeURIComponent(data)); - } - }; -}; - -WebVTT$1.convertCueToDOMTree = function(window, cuetext) { - if (!window || !cuetext) { - return null; - } - return parseContent(window, cuetext); -}; - -var FONT_SIZE_PERCENT = 0.05; -var FONT_STYLE = "sans-serif"; -var CUE_BACKGROUND_PADDING = "1.5%"; - -// Runs the processing model over the cues and regions passed to it. -// @param overlay A block level element (usually a div) that the computed cues -// and regions will be placed into. -WebVTT$1.processCues = function(window, cues, overlay) { - if (!window || !cues || !overlay) { - return null; - } - - // Remove all previous children. - while (overlay.firstChild) { - overlay.removeChild(overlay.firstChild); - } - - var paddedOverlay = window.document.createElement("div"); - paddedOverlay.style.position = "absolute"; - paddedOverlay.style.left = "0"; - paddedOverlay.style.right = "0"; - paddedOverlay.style.top = "0"; - paddedOverlay.style.bottom = "0"; - paddedOverlay.style.margin = CUE_BACKGROUND_PADDING; - overlay.appendChild(paddedOverlay); - - // Determine if we need to compute the display states of the cues. This could - // be the case if a cue's state has been changed since the last computation or - // if it has not been computed yet. - function shouldCompute(cues) { - for (var i = 0; i < cues.length; i++) { - if (cues[i].hasBeenReset || !cues[i].displayState) { - return true; - } - } - return false; - } - - // We don't need to recompute the cues' display states. Just reuse them. - if (!shouldCompute(cues)) { - for (var i = 0; i < cues.length; i++) { - paddedOverlay.appendChild(cues[i].displayState); - } - return; - } - - var boxPositions = [], - containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay), - fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100; - var styleOptions = { - font: fontSize + "px " + FONT_STYLE - }; - - (function() { - var styleBox, cue; - - for (var i = 0; i < cues.length; i++) { - cue = cues[i]; - - // Compute the intial position and styles of the cue div. - styleBox = new CueStyleBox(window, cue, styleOptions); - paddedOverlay.appendChild(styleBox.div); - - // Move the cue div to it's correct line position. - moveBoxToLinePosition(window, styleBox, containerBox, boxPositions); - - // Remember the computed div so that we don't have to recompute it later - // if we don't have too. - cue.displayState = styleBox.div; - - boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox)); - } - })(); -}; - -WebVTT$1.Parser = function(window, vttjs, decoder) { - if (!decoder) { - decoder = vttjs; - vttjs = {}; - } - if (!vttjs) { - vttjs = {}; - } - - this.window = window; - this.vttjs = vttjs; - this.state = "INITIAL"; - this.buffer = ""; - this.decoder = decoder || new TextDecoder("utf8"); - this.regionList = []; -}; - -WebVTT$1.Parser.prototype = { - // If the error is a ParsingError then report it to the consumer if - // possible. If it's not a ParsingError then throw it like normal. - reportOrThrowError: function(e) { - if (e instanceof ParsingError) { - this.onparsingerror && this.onparsingerror(e); - } else { - throw e; - } - }, - parse: function (data) { - var self = this; - - // If there is no data then we won't decode it, but will just try to parse - // whatever is in buffer already. This may occur in circumstances, for - // example when flush() is called. - if (data) { - // Try to decode the data that we received. - self.buffer += self.decoder.decode(data, {stream: true}); - } - - function collectNextLine() { - var buffer = self.buffer; - var pos = 0; - while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') { - ++pos; - } - var line = buffer.substr(0, pos); - // Advance the buffer early in case we fail below. - if (buffer[pos] === '\r') { - ++pos; - } - if (buffer[pos] === '\n') { - ++pos; - } - self.buffer = buffer.substr(pos); - return line; - } - - // 3.4 WebVTT region and WebVTT region settings syntax - function parseRegion(input) { - var settings = new Settings(); - - parseOptions(input, function (k, v) { - switch (k) { - case "id": - settings.set(k, v); - break; - case "width": - settings.percent(k, v); - break; - case "lines": - settings.integer(k, v); - break; - case "regionanchor": - case "viewportanchor": - var xy = v.split(','); - if (xy.length !== 2) { - break; - } - // We have to make sure both x and y parse, so use a temporary - // settings object here. - var anchor = new Settings(); - anchor.percent("x", xy[0]); - anchor.percent("y", xy[1]); - if (!anchor.has("x") || !anchor.has("y")) { - break; - } - settings.set(k + "X", anchor.get("x")); - settings.set(k + "Y", anchor.get("y")); - break; - case "scroll": - settings.alt(k, v, ["up"]); - break; - } - }, /=/, /\s/); - - // Create the region, using default values for any values that were not - // specified. - if (settings.has("id")) { - var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)(); - region.width = settings.get("width", 100); - region.lines = settings.get("lines", 3); - region.regionAnchorX = settings.get("regionanchorX", 0); - region.regionAnchorY = settings.get("regionanchorY", 100); - region.viewportAnchorX = settings.get("viewportanchorX", 0); - region.viewportAnchorY = settings.get("viewportanchorY", 100); - region.scroll = settings.get("scroll", ""); - // Register the region. - self.onregion && self.onregion(region); - // Remember the VTTRegion for later in case we parse any VTTCues that - // reference it. - self.regionList.push({ - id: settings.get("id"), - region: region - }); - } - } - - // draft-pantos-http-live-streaming-20 - // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5 - // 3.5 WebVTT - function parseTimestampMap(input) { - var settings = new Settings(); - - parseOptions(input, function(k, v) { - switch(k) { - case "MPEGT": - settings.integer(k + 'S', v); - break; - case "LOCA": - settings.set(k + 'L', parseTimeStamp(v)); - break; - } - }, /[^\d]:/, /,/); - - self.ontimestampmap && self.ontimestampmap({ - "MPEGTS": settings.get("MPEGTS"), - "LOCAL": settings.get("LOCAL") - }); - } - - // 3.2 WebVTT metadata header syntax - function parseHeader(input) { - if (input.match(/X-TIMESTAMP-MAP/)) { - // This line contains HLS X-TIMESTAMP-MAP metadata - parseOptions(input, function(k, v) { - switch(k) { - case "X-TIMESTAMP-MAP": - parseTimestampMap(v); - break; - } - }, /=/); - } else { - parseOptions(input, function (k, v) { - switch (k) { - case "Region": - // 3.3 WebVTT region metadata header syntax - parseRegion(v); - break; - } - }, /:/); - } - - } - - // 5.1 WebVTT file parsing. - try { - var line; - if (self.state === "INITIAL") { - // We can't start parsing until we have the first line. - if (!/\r\n|\n/.test(self.buffer)) { - return this; - } - - line = collectNextLine(); - - var m = line.match(/^WEBVTT([ \t].*)?$/); - if (!m || !m[0]) { - throw new ParsingError(ParsingError.Errors.BadSignature); - } - - self.state = "HEADER"; - } - - var alreadyCollectedLine = false; - while (self.buffer) { - // We can't parse a line until we have the full line. - if (!/\r\n|\n/.test(self.buffer)) { - return this; - } - - if (!alreadyCollectedLine) { - line = collectNextLine(); - } else { - alreadyCollectedLine = false; - } - - switch (self.state) { - case "HEADER": - // 13-18 - Allow a header (metadata) under the WEBVTT line. - if (/:/.test(line)) { - parseHeader(line); - } else if (!line) { - // An empty line terminates the header and starts the body (cues). - self.state = "ID"; - } - continue; - case "NOTE": - // Ignore NOTE blocks. - if (!line) { - self.state = "ID"; - } - continue; - case "ID": - // Check for the start of NOTE blocks. - if (/^NOTE($|[ \t])/.test(line)) { - self.state = "NOTE"; - break; - } - // 19-29 - Allow any number of line terminators, then initialize new cue values. - if (!line) { - continue; - } - self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, ""); - // Safari still uses the old middle value and won't accept center - try { - self.cue.align = "center"; - } catch (e) { - self.cue.align = "middle"; - } - self.state = "CUE"; - // 30-39 - Check if self line contains an optional identifier or timing data. - if (line.indexOf("-->") === -1) { - self.cue.id = line; - continue; - } - // Process line as start of a cue. - /*falls through*/ - case "CUE": - // 40 - Collect cue timings and settings. - try { - parseCue(line, self.cue, self.regionList); - } catch (e) { - self.reportOrThrowError(e); - // In case of an error ignore rest of the cue. - self.cue = null; - self.state = "BADCUE"; - continue; - } - self.state = "CUETEXT"; - continue; - case "CUETEXT": - var hasSubstring = line.indexOf("-->") !== -1; - // 34 - If we have an empty line then report the cue. - // 35 - If we have the special substring '-->' then report the cue, - // but do not collect the line as we need to process the current - // one as a new cue. - if (!line || hasSubstring && (alreadyCollectedLine = true)) { - // We are done parsing self cue. - self.oncue && self.oncue(self.cue); - self.cue = null; - self.state = "ID"; - continue; - } - if (self.cue.text) { - self.cue.text += "\n"; - } - self.cue.text += line.replace(/\u2028/g, '\n').replace(/u2029/g, '\n'); - continue; - case "BADCUE": // BADCUE - // 54-62 - Collect and discard the remaining cue. - if (!line) { - self.state = "ID"; - } - continue; - } - } - } catch (e) { - self.reportOrThrowError(e); - - // If we are currently parsing a cue, report what we have. - if (self.state === "CUETEXT" && self.cue && self.oncue) { - self.oncue(self.cue); - } - self.cue = null; - // Enter BADWEBVTT state if header was not parsed correctly otherwise - // another exception occurred so enter BADCUE state. - self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE"; - } - return this; - }, - flush: function () { - var self = this; - try { - // Finish decoding the stream. - self.buffer += self.decoder.decode(); - // Synthesize the end of the current cue or region. - if (self.cue || self.state === "HEADER") { - self.buffer += "\n\n"; - self.parse(); - } - // If we've flushed, parsed, and we're still on the INITIAL state then - // that means we don't have enough of the stream to parse the first - // line. - if (self.state === "INITIAL") { - throw new ParsingError(ParsingError.Errors.BadSignature); - } - } catch(e) { - self.reportOrThrowError(e); - } - self.onflush && self.onflush(); - return this; - } -}; - -var vtt = WebVTT$1; - -/** - * Copyright 2013 vtt.js Contributors - * - * 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 autoKeyword = "auto"; -var directionSetting = { - "": 1, - "lr": 1, - "rl": 1 -}; -var alignSetting = { - "start": 1, - "center": 1, - "end": 1, - "left": 1, - "right": 1, - "auto": 1, - "line-left": 1, - "line-right": 1 -}; - -function findDirectionSetting(value) { - if (typeof value !== "string") { - return false; - } - var dir = directionSetting[value.toLowerCase()]; - return dir ? value.toLowerCase() : false; -} - -function findAlignSetting(value) { - if (typeof value !== "string") { - return false; - } - var align = alignSetting[value.toLowerCase()]; - return align ? value.toLowerCase() : false; -} - -function VTTCue(startTime, endTime, text) { - /** - * Shim implementation specific properties. These properties are not in - * the spec. - */ - - // Lets us know when the VTTCue's data has changed in such a way that we need - // to recompute its display state. This lets us compute its display state - // lazily. - this.hasBeenReset = false; - - /** - * VTTCue and TextTrackCue properties - * http://dev.w3.org/html5/webvtt/#vttcue-interface - */ - - var _id = ""; - var _pauseOnExit = false; - var _startTime = startTime; - var _endTime = endTime; - var _text = text; - var _region = null; - var _vertical = ""; - var _snapToLines = true; - var _line = "auto"; - var _lineAlign = "start"; - var _position = "auto"; - var _positionAlign = "auto"; - var _size = 100; - var _align = "center"; - - Object.defineProperties(this, { - "id": { - enumerable: true, - get: function() { - return _id; - }, - set: function(value) { - _id = "" + value; - } - }, - - "pauseOnExit": { - enumerable: true, - get: function() { - return _pauseOnExit; - }, - set: function(value) { - _pauseOnExit = !!value; - } - }, - - "startTime": { - enumerable: true, - get: function() { - return _startTime; - }, - set: function(value) { - if (typeof value !== "number") { - throw new TypeError("Start time must be set to a number."); - } - _startTime = value; - this.hasBeenReset = true; - } - }, - - "endTime": { - enumerable: true, - get: function() { - return _endTime; - }, - set: function(value) { - if (typeof value !== "number") { - throw new TypeError("End time must be set to a number."); - } - _endTime = value; - this.hasBeenReset = true; - } - }, - - "text": { - enumerable: true, - get: function() { - return _text; - }, - set: function(value) { - _text = "" + value; - this.hasBeenReset = true; - } - }, - - "region": { - enumerable: true, - get: function() { - return _region; - }, - set: function(value) { - _region = value; - this.hasBeenReset = true; - } - }, - - "vertical": { - enumerable: true, - get: function() { - return _vertical; - }, - set: function(value) { - var setting = findDirectionSetting(value); - // Have to check for false because the setting an be an empty string. - if (setting === false) { - throw new SyntaxError("Vertical: an invalid or illegal direction string was specified."); - } - _vertical = setting; - this.hasBeenReset = true; - } - }, - - "snapToLines": { - enumerable: true, - get: function() { - return _snapToLines; - }, - set: function(value) { - _snapToLines = !!value; - this.hasBeenReset = true; - } - }, - - "line": { - enumerable: true, - get: function() { - return _line; - }, - set: function(value) { - if (typeof value !== "number" && value !== autoKeyword) { - throw new SyntaxError("Line: an invalid number or illegal string was specified."); - } - _line = value; - this.hasBeenReset = true; - } - }, - - "lineAlign": { - enumerable: true, - get: function() { - return _lineAlign; - }, - set: function(value) { - var setting = findAlignSetting(value); - if (!setting) { - console.warn("lineAlign: an invalid or illegal string was specified."); - } else { - _lineAlign = setting; - this.hasBeenReset = true; - } - } - }, - - "position": { - enumerable: true, - get: function() { - return _position; - }, - set: function(value) { - if (value < 0 || value > 100) { - throw new Error("Position must be between 0 and 100."); - } - _position = value; - this.hasBeenReset = true; - } - }, - - "positionAlign": { - enumerable: true, - get: function() { - return _positionAlign; - }, - set: function(value) { - var setting = findAlignSetting(value); - if (!setting) { - console.warn("positionAlign: an invalid or illegal string was specified."); - } else { - _positionAlign = setting; - this.hasBeenReset = true; - } - } - }, - - "size": { - enumerable: true, - get: function() { - return _size; - }, - set: function(value) { - if (value < 0 || value > 100) { - throw new Error("Size must be between 0 and 100."); - } - _size = value; - this.hasBeenReset = true; - } - }, - - "align": { - enumerable: true, - get: function() { - return _align; - }, - set: function(value) { - var setting = findAlignSetting(value); - if (!setting) { - throw new SyntaxError("align: an invalid or illegal alignment string was specified."); - } - _align = setting; - this.hasBeenReset = true; - } - } - }); - - /** - * Other spec defined properties - */ - - // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state - this.displayState = undefined; -} - -/** - * VTTCue methods - */ - -VTTCue.prototype.getCueAsHTML = function() { - // Assume WebVTT.convertCueToDOMTree is on the global. - return WebVTT.convertCueToDOMTree(window, this.text); -}; - -var vttcue = VTTCue; - -/** - * Copyright 2013 vtt.js Contributors - * - * 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 scrollSetting = { - "": true, - "up": true -}; - -function findScrollSetting(value) { - if (typeof value !== "string") { - return false; - } - var scroll = scrollSetting[value.toLowerCase()]; - return scroll ? value.toLowerCase() : false; -} - -function isValidPercentValue(value) { - return typeof value === "number" && (value >= 0 && value <= 100); -} - -// VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface -function VTTRegion() { - var _width = 100; - var _lines = 3; - var _regionAnchorX = 0; - var _regionAnchorY = 100; - var _viewportAnchorX = 0; - var _viewportAnchorY = 100; - var _scroll = ""; - - Object.defineProperties(this, { - "width": { - enumerable: true, - get: function() { - return _width; - }, - set: function(value) { - if (!isValidPercentValue(value)) { - throw new Error("Width must be between 0 and 100."); - } - _width = value; - } - }, - "lines": { - enumerable: true, - get: function() { - return _lines; - }, - set: function(value) { - if (typeof value !== "number") { - throw new TypeError("Lines must be set to a number."); - } - _lines = value; - } - }, - "regionAnchorY": { - enumerable: true, - get: function() { - return _regionAnchorY; - }, - set: function(value) { - if (!isValidPercentValue(value)) { - throw new Error("RegionAnchorX must be between 0 and 100."); - } - _regionAnchorY = value; - } - }, - "regionAnchorX": { - enumerable: true, - get: function() { - return _regionAnchorX; - }, - set: function(value) { - if(!isValidPercentValue(value)) { - throw new Error("RegionAnchorY must be between 0 and 100."); - } - _regionAnchorX = value; - } - }, - "viewportAnchorY": { - enumerable: true, - get: function() { - return _viewportAnchorY; - }, - set: function(value) { - if (!isValidPercentValue(value)) { - throw new Error("ViewportAnchorY must be between 0 and 100."); - } - _viewportAnchorY = value; - } - }, - "viewportAnchorX": { - enumerable: true, - get: function() { - return _viewportAnchorX; - }, - set: function(value) { - if (!isValidPercentValue(value)) { - throw new Error("ViewportAnchorX must be between 0 and 100."); - } - _viewportAnchorX = value; - } - }, - "scroll": { - enumerable: true, - get: function() { - return _scroll; - }, - set: function(value) { - var setting = findScrollSetting(value); - // Have to check for false as an empty string is a legal value. - if (setting === false) { - console.warn("Scroll: an invalid or illegal string was specified."); - } else { - _scroll = setting; - } - } - } - }); -} - -var vttregion = VTTRegion; - -var browserIndex = createCommonjsModule(function (module) { -/** - * Copyright 2013 vtt.js Contributors - * - * 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. - */ - -// Default exports for Node. Export the extended versions of VTTCue and -// VTTRegion in Node since we likely want the capability to convert back and -// forth between JSON. If we don't then it's not that big of a deal since we're -// off browser. - - - -var vttjs = module.exports = { - WebVTT: vtt, - VTTCue: vttcue, - VTTRegion: vttregion -}; - -window_1$1.vttjs = vttjs; -window_1$1.WebVTT = vttjs.WebVTT; - -var cueShim = vttjs.VTTCue; -var regionShim = vttjs.VTTRegion; -var nativeVTTCue = window_1$1.VTTCue; -var nativeVTTRegion = window_1$1.VTTRegion; - -vttjs.shim = function() { - window_1$1.VTTCue = cueShim; - window_1$1.VTTRegion = regionShim; -}; - -vttjs.restore = function() { - window_1$1.VTTCue = nativeVTTCue; - window_1$1.VTTRegion = nativeVTTRegion; -}; - -if (!window_1$1.VTTCue) { - vttjs.shim(); -} -}); - -var setPrototypeOf = createCommonjsModule(function (module) { -function _setPrototypeOf(o, p) { - module.exports = _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { - o.__proto__ = p; - return o; - }; - - return _setPrototypeOf(o, p); -} - -module.exports = _setPrototypeOf; -}); - -function _isNativeReflectConstruct() { - if (typeof Reflect === "undefined" || !Reflect.construct) return false; - if (Reflect.construct.sham) return false; - if (typeof Proxy === "function") return true; - - try { - Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); - return true; - } catch (e) { - return false; - } -} - -var isNativeReflectConstruct = _isNativeReflectConstruct; - -var construct = createCommonjsModule(function (module) { -function _construct(Parent, args, Class) { - if (isNativeReflectConstruct()) { - module.exports = _construct = Reflect.construct; - } else { - module.exports = _construct = function _construct(Parent, args, Class) { - var a = [null]; - a.push.apply(a, args); - var Constructor = Function.bind.apply(Parent, a); - var instance = new Constructor(); - if (Class) setPrototypeOf(instance, Class.prototype); - return instance; - }; - } - - return _construct.apply(null, arguments); -} - -module.exports = _construct; -}); - -function _inherits(subClass, superClass) { - if (typeof superClass !== "function" && superClass !== null) { - throw new TypeError("Super expression must either be null or a function"); - } - - subClass.prototype = Object.create(superClass && superClass.prototype, { - constructor: { - value: subClass, - writable: true, - configurable: true - } - }); - if (superClass) setPrototypeOf(subClass, superClass); -} - -var inherits = _inherits; - -var urlToolkit = createCommonjsModule(function (module, exports) { -// see https://tools.ietf.org/html/rfc1808 - -(function (root) { - var URL_REGEX = /^((?:[a-zA-Z0-9+\-.]+:)?)(\/\/[^\/?#]*)?((?:[^\/?#]*\/)*[^;?#]*)?(;[^?#]*)?(\?[^#]*)?(#.*)?$/; - var FIRST_SEGMENT_REGEX = /^([^\/?#]*)(.*)$/; - var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g; - var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g; - - var URLToolkit = { - // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or // - // E.g - // With opts.alwaysNormalize = false (default, spec compliant) - // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g - // With opts.alwaysNormalize = true (not spec compliant) - // http://a.com/b/cd + /e/f/../g => http://a.com/e/g - buildAbsoluteURL: function (baseURL, relativeURL, opts) { - opts = opts || {}; - // remove any remaining space and CRLF - baseURL = baseURL.trim(); - relativeURL = relativeURL.trim(); - if (!relativeURL) { - // 2a) If the embedded URL is entirely empty, it inherits the - // entire base URL (i.e., is set equal to the base URL) - // and we are done. - if (!opts.alwaysNormalize) { - return baseURL; - } - var basePartsForNormalise = URLToolkit.parseURL(baseURL); - if (!basePartsForNormalise) { - throw new Error('Error trying to parse base URL.'); - } - basePartsForNormalise.path = URLToolkit.normalizePath( - basePartsForNormalise.path - ); - return URLToolkit.buildURLFromParts(basePartsForNormalise); - } - var relativeParts = URLToolkit.parseURL(relativeURL); - if (!relativeParts) { - throw new Error('Error trying to parse relative URL.'); - } - if (relativeParts.scheme) { - // 2b) If the embedded URL starts with a scheme name, it is - // interpreted as an absolute URL and we are done. - if (!opts.alwaysNormalize) { - return relativeURL; - } - relativeParts.path = URLToolkit.normalizePath(relativeParts.path); - return URLToolkit.buildURLFromParts(relativeParts); - } - var baseParts = URLToolkit.parseURL(baseURL); - if (!baseParts) { - throw new Error('Error trying to parse base URL.'); - } - if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') { - // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc - // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a' - var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path); - baseParts.netLoc = pathParts[1]; - baseParts.path = pathParts[2]; - } - if (baseParts.netLoc && !baseParts.path) { - baseParts.path = '/'; - } - var builtParts = { - // 2c) Otherwise, the embedded URL inherits the scheme of - // the base URL. - scheme: baseParts.scheme, - netLoc: relativeParts.netLoc, - path: null, - params: relativeParts.params, - query: relativeParts.query, - fragment: relativeParts.fragment, - }; - if (!relativeParts.netLoc) { - // 3) If the embedded URL's is non-empty, we skip to - // Step 7. Otherwise, the embedded URL inherits the - // (if any) of the base URL. - builtParts.netLoc = baseParts.netLoc; - // 4) If the embedded URL path is preceded by a slash "/", the - // path is not relative and we skip to Step 7. - if (relativeParts.path[0] !== '/') { - if (!relativeParts.path) { - // 5) If the embedded URL path is empty (and not preceded by a - // slash), then the embedded URL inherits the base URL path - builtParts.path = baseParts.path; - // 5a) if the embedded URL's is non-empty, we skip to - // step 7; otherwise, it inherits the of the base - // URL (if any) and - if (!relativeParts.params) { - builtParts.params = baseParts.params; - // 5b) if the embedded URL's is non-empty, we skip to - // step 7; otherwise, it inherits the of the base - // URL (if any) and we skip to step 7. - if (!relativeParts.query) { - builtParts.query = baseParts.query; - } - } - } else { - // 6) The last segment of the base URL's path (anything - // following the rightmost slash "/", or the entire path if no - // slash is present) is removed and the embedded URL's path is - // appended in its place. - var baseURLPath = baseParts.path; - var newPath = - baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + - relativeParts.path; - builtParts.path = URLToolkit.normalizePath(newPath); - } - } - } - if (builtParts.path === null) { - builtParts.path = opts.alwaysNormalize - ? URLToolkit.normalizePath(relativeParts.path) - : relativeParts.path; - } - return URLToolkit.buildURLFromParts(builtParts); - }, - parseURL: function (url) { - var parts = URL_REGEX.exec(url); - if (!parts) { - return null; - } - return { - scheme: parts[1] || '', - netLoc: parts[2] || '', - path: parts[3] || '', - params: parts[4] || '', - query: parts[5] || '', - fragment: parts[6] || '', - }; - }, - normalizePath: function (path) { - // The following operations are - // then applied, in order, to the new path: - // 6a) All occurrences of "./", where "." is a complete path - // segment, are removed. - // 6b) If the path ends with "." as a complete path segment, - // that "." is removed. - path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); - // 6c) All occurrences of "/../", where is a - // complete path segment not equal to "..", are removed. - // Removal of these path segments is performed iteratively, - // removing the leftmost matching pattern on each iteration, - // until no matching pattern remains. - // 6d) If the path ends with "/..", where is a - // complete path segment not equal to "..", that - // "/.." is removed. - while ( - path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length - ) {} - return path.split('').reverse().join(''); - }, - buildURLFromParts: function (parts) { - return ( - parts.scheme + - parts.netLoc + - parts.path + - parts.params + - parts.query + - parts.fragment - ); - }, - }; - - module.exports = URLToolkit; -})(); -}); - -/*! @name m3u8-parser @version 4.4.0 @license Apache-2.0 */ - -function _extends() { - _extends = Object.assign || function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; - }; - - return _extends.apply(this, arguments); -} - -function _inheritsLoose$1(subClass, superClass) { - subClass.prototype = Object.create(superClass.prototype); - subClass.prototype.constructor = subClass; - subClass.__proto__ = superClass; -} - -function _assertThisInitialized$1(self) { - if (self === void 0) { - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - } - - return self; -} - -/** - * @file stream.js - */ - -/** - * A lightweight readable stream implementation that handles event dispatching. - * - * @class Stream - */ -var Stream = -/*#__PURE__*/ -function () { - function Stream() { - this.listeners = {}; - } - /** - * Add a listener for a specified event type. - * - * @param {string} type the event name - * @param {Function} listener the callback to be invoked when an event of - * the specified type occurs - */ - - - var _proto = Stream.prototype; - - _proto.on = function on(type, listener) { - if (!this.listeners[type]) { - this.listeners[type] = []; - } - - this.listeners[type].push(listener); - } - /** - * Remove a listener for a specified event type. - * - * @param {string} type the event name - * @param {Function} listener a function previously registered for this - * type of event through `on` - * @return {boolean} if we could turn it off or not - */ - ; - - _proto.off = function off(type, listener) { - if (!this.listeners[type]) { - return false; - } - - var index = this.listeners[type].indexOf(listener); - this.listeners[type].splice(index, 1); - return index > -1; - } - /** - * Trigger an event of the specified type on this stream. Any additional - * arguments to this function are passed as parameters to event listeners. - * - * @param {string} type the event name - */ - ; - - _proto.trigger = function trigger(type) { - var callbacks = this.listeners[type]; - var i; - var length; - var args; - - if (!callbacks) { - return; - } // Slicing the arguments on every invocation of this method - // can add a significant amount of overhead. Avoid the - // intermediate object creation for the common case of a - // single callback argument - - - if (arguments.length === 2) { - length = callbacks.length; - - for (i = 0; i < length; ++i) { - callbacks[i].call(this, arguments[1]); - } - } else { - args = Array.prototype.slice.call(arguments, 1); - length = callbacks.length; - - for (i = 0; i < length; ++i) { - callbacks[i].apply(this, args); - } - } - } - /** - * Destroys the stream and cleans up. - */ - ; - - _proto.dispose = function dispose() { - this.listeners = {}; - } - /** - * Forwards all `data` events on this stream to the destination stream. The - * destination stream should provide a method `push` to receive the data - * events as they arrive. - * - * @param {Stream} destination the stream that will receive all `data` events - * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options - */ - ; - - _proto.pipe = function pipe(destination) { - this.on('data', function (data) { - destination.push(data); - }); - }; - - return Stream; -}(); - -/** - * A stream that buffers string input and generates a `data` event for each - * line. - * - * @class LineStream - * @extends Stream - */ - -var LineStream = -/*#__PURE__*/ -function (_Stream) { - _inheritsLoose$1(LineStream, _Stream); - - function LineStream() { - var _this; - - _this = _Stream.call(this) || this; - _this.buffer = ''; - return _this; - } - /** - * Add new data to be parsed. - * - * @param {string} data the text to process - */ - - - var _proto = LineStream.prototype; - - _proto.push = function push(data) { - var nextNewline; - this.buffer += data; - nextNewline = this.buffer.indexOf('\n'); - - for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) { - this.trigger('data', this.buffer.substring(0, nextNewline)); - this.buffer = this.buffer.substring(nextNewline + 1); - } - }; - - return LineStream; -}(Stream); - -/** - * "forgiving" attribute list psuedo-grammar: - * attributes -> keyvalue (',' keyvalue)* - * keyvalue -> key '=' value - * key -> [^=]* - * value -> '"' [^"]* '"' | [^,]* - */ - -var attributeSeparator = function attributeSeparator() { - var key = '[^=]*'; - var value = '"[^"]*"|[^,]*'; - var keyvalue = '(?:' + key + ')=(?:' + value + ')'; - return new RegExp('(?:^|,)(' + keyvalue + ')'); -}; -/** - * Parse attributes from a line given the separator - * - * @param {string} attributes the attribute line to parse - */ - - -var parseAttributes = function parseAttributes(attributes) { - // split the string using attributes as the separator - var attrs = attributes.split(attributeSeparator()); - var result = {}; - var i = attrs.length; - var attr; - - while (i--) { - // filter out unmatched portions of the string - if (attrs[i] === '') { - continue; - } // split the key and value - - - attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); // trim whitespace and remove optional quotes around the value - - attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); - attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); - attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1'); - result[attr[0]] = attr[1]; - } - - return result; -}; -/** - * A line-level M3U8 parser event stream. It expects to receive input one - * line at a time and performs a context-free parse of its contents. A stream - * interpretation of a manifest can be useful if the manifest is expected to - * be too large to fit comfortably into memory or the entirety of the input - * is not immediately available. Otherwise, it's probably much easier to work - * with a regular `Parser` object. - * - * Produces `data` events with an object that captures the parser's - * interpretation of the input. That object has a property `tag` that is one - * of `uri`, `comment`, or `tag`. URIs only have a single additional - * property, `line`, which captures the entirety of the input without - * interpretation. Comments similarly have a single additional property - * `text` which is the input without the leading `#`. - * - * Tags always have a property `tagType` which is the lower-cased version of - * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance, - * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized - * tags are given the tag type `unknown` and a single additional property - * `data` with the remainder of the input. - * - * @class ParseStream - * @extends Stream - */ - - -var ParseStream = -/*#__PURE__*/ -function (_Stream) { - _inheritsLoose$1(ParseStream, _Stream); - - function ParseStream() { - var _this; - - _this = _Stream.call(this) || this; - _this.customParsers = []; - _this.tagMappers = []; - return _this; - } - /** - * Parses an additional line of input. - * - * @param {string} line a single line of an M3U8 file to parse - */ - - - var _proto = ParseStream.prototype; - - _proto.push = function push(line) { - var _this2 = this; - - var match; - var event; // strip whitespace - - line = line.trim(); - - if (line.length === 0) { - // ignore empty lines - return; - } // URIs - - - if (line[0] !== '#') { - this.trigger('data', { - type: 'uri', - uri: line - }); - return; - } // map tags - - - var newLines = this.tagMappers.reduce(function (acc, mapper) { - var mappedLine = mapper(line); // skip if unchanged - - if (mappedLine === line) { - return acc; - } - - return acc.concat([mappedLine]); - }, [line]); - newLines.forEach(function (newLine) { - for (var i = 0; i < _this2.customParsers.length; i++) { - if (_this2.customParsers[i].call(_this2, newLine)) { - return; - } - } // Comments - - - if (newLine.indexOf('#EXT') !== 0) { - _this2.trigger('data', { - type: 'comment', - text: newLine.slice(1) - }); - - return; - } // strip off any carriage returns here so the regex matching - // doesn't have to account for them. - - - newLine = newLine.replace('\r', ''); // Tags - - match = /^#EXTM3U/.exec(newLine); - - if (match) { - _this2.trigger('data', { - type: 'tag', - tagType: 'm3u' - }); - - return; - } - - match = /^#EXTINF:?([0-9\.]*)?,?(.*)?$/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'inf' - }; - - if (match[1]) { - event.duration = parseFloat(match[1]); - } - - if (match[2]) { - event.title = match[2]; - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-TARGETDURATION:?([0-9.]*)?/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'targetduration' - }; - - if (match[1]) { - event.duration = parseInt(match[1], 10); - } - - _this2.trigger('data', event); - - return; - } - - match = /^#ZEN-TOTAL-DURATION:?([0-9.]*)?/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'totalduration' - }; - - if (match[1]) { - event.duration = parseInt(match[1], 10); - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-VERSION:?([0-9.]*)?/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'version' - }; - - if (match[1]) { - event.version = parseInt(match[1], 10); - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'media-sequence' - }; - - if (match[1]) { - event.number = parseInt(match[1], 10); - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'discontinuity-sequence' - }; - - if (match[1]) { - event.number = parseInt(match[1], 10); - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-PLAYLIST-TYPE:?(.*)?$/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'playlist-type' - }; - - if (match[1]) { - event.playlistType = match[1]; - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'byterange' - }; - - if (match[1]) { - event.length = parseInt(match[1], 10); - } - - if (match[2]) { - event.offset = parseInt(match[2], 10); - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-ALLOW-CACHE:?(YES|NO)?/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'allow-cache' - }; - - if (match[1]) { - event.allowed = !/NO/.test(match[1]); - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-MAP:?(.*)$/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'map' - }; - - if (match[1]) { - var attributes = parseAttributes(match[1]); - - if (attributes.URI) { - event.uri = attributes.URI; - } - - if (attributes.BYTERANGE) { - var _attributes$BYTERANGE = attributes.BYTERANGE.split('@'), - length = _attributes$BYTERANGE[0], - offset = _attributes$BYTERANGE[1]; - - event.byterange = {}; - - if (length) { - event.byterange.length = parseInt(length, 10); - } - - if (offset) { - event.byterange.offset = parseInt(offset, 10); - } - } - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-STREAM-INF:?(.*)$/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'stream-inf' - }; - - if (match[1]) { - event.attributes = parseAttributes(match[1]); - - if (event.attributes.RESOLUTION) { - var split = event.attributes.RESOLUTION.split('x'); - var resolution = {}; - - if (split[0]) { - resolution.width = parseInt(split[0], 10); - } - - if (split[1]) { - resolution.height = parseInt(split[1], 10); - } - - event.attributes.RESOLUTION = resolution; - } - - if (event.attributes.BANDWIDTH) { - event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); - } - - if (event.attributes['PROGRAM-ID']) { - event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10); - } - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-MEDIA:?(.*)$/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'media' - }; - - if (match[1]) { - event.attributes = parseAttributes(match[1]); - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-ENDLIST/.exec(newLine); - - if (match) { - _this2.trigger('data', { - type: 'tag', - tagType: 'endlist' - }); - - return; - } - - match = /^#EXT-X-DISCONTINUITY/.exec(newLine); - - if (match) { - _this2.trigger('data', { - type: 'tag', - tagType: 'discontinuity' - }); - - return; - } - - match = /^#EXT-X-PROGRAM-DATE-TIME:?(.*)$/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'program-date-time' - }; - - if (match[1]) { - event.dateTimeString = match[1]; - event.dateTimeObject = new Date(match[1]); - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-KEY:?(.*)$/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'key' - }; - - if (match[1]) { - event.attributes = parseAttributes(match[1]); // parse the IV string into a Uint32Array - - if (event.attributes.IV) { - if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') { - event.attributes.IV = event.attributes.IV.substring(2); - } - - event.attributes.IV = event.attributes.IV.match(/.{8}/g); - event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16); - event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16); - event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16); - event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16); - event.attributes.IV = new Uint32Array(event.attributes.IV); - } - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-START:?(.*)$/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'start' - }; - - if (match[1]) { - event.attributes = parseAttributes(match[1]); - event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']); - event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE); - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-CUE-OUT-CONT:?(.*)?$/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'cue-out-cont' - }; - - if (match[1]) { - event.data = match[1]; - } else { - event.data = ''; - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-CUE-OUT:?(.*)?$/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'cue-out' - }; - - if (match[1]) { - event.data = match[1]; - } else { - event.data = ''; - } - - _this2.trigger('data', event); - - return; - } - - match = /^#EXT-X-CUE-IN:?(.*)?$/.exec(newLine); - - if (match) { - event = { - type: 'tag', - tagType: 'cue-in' - }; - - if (match[1]) { - event.data = match[1]; - } else { - event.data = ''; - } - - _this2.trigger('data', event); - - return; - } // unknown tag type - - - _this2.trigger('data', { - type: 'tag', - data: newLine.slice(4) - }); - }); - } - /** - * Add a parser for custom headers - * - * @param {Object} options a map of options for the added parser - * @param {RegExp} options.expression a regular expression to match the custom header - * @param {string} options.customType the custom type to register to the output - * @param {Function} [options.dataParser] function to parse the line into an object - * @param {boolean} [options.segment] should tag data be attached to the segment object - */ - ; - - _proto.addParser = function addParser(_ref) { - var _this3 = this; - - var expression = _ref.expression, - customType = _ref.customType, - dataParser = _ref.dataParser, - segment = _ref.segment; - - if (typeof dataParser !== 'function') { - dataParser = function dataParser(line) { - return line; - }; - } - - this.customParsers.push(function (line) { - var match = expression.exec(line); - - if (match) { - _this3.trigger('data', { - type: 'custom', - data: dataParser(line), - customType: customType, - segment: segment - }); - - return true; - } - }); - } - /** - * Add a custom header mapper - * - * @param {Object} options - * @param {RegExp} options.expression a regular expression to match the custom header - * @param {Function} options.map function to translate tag into a different tag - */ - ; - - _proto.addTagMapper = function addTagMapper(_ref2) { - var expression = _ref2.expression, - map = _ref2.map; - - var mapFn = function mapFn(line) { - if (expression.test(line)) { - return map(line); - } - - return line; - }; - - this.tagMappers.push(mapFn); - }; - - return ParseStream; -}(Stream); - -function decodeB64ToUint8Array(b64Text) { - var decodedString = window_1$1.atob(b64Text || ''); - var array = new Uint8Array(decodedString.length); - - for (var i = 0; i < decodedString.length; i++) { - array[i] = decodedString.charCodeAt(i); - } - - return array; -} - -/** - * A parser for M3U8 files. The current interpretation of the input is - * exposed as a property `manifest` on parser objects. It's just two lines to - * create and parse a manifest once you have the contents available as a string: - * - * ```js - * var parser = new m3u8.Parser(); - * parser.push(xhr.responseText); - * ``` - * - * New input can later be applied to update the manifest object by calling - * `push` again. - * - * The parser attempts to create a usable manifest object even if the - * underlying input is somewhat nonsensical. It emits `info` and `warning` - * events during the parse if it encounters input that seems invalid or - * requires some property of the manifest object to be defaulted. - * - * @class Parser - * @extends Stream - */ - -var Parser = -/*#__PURE__*/ -function (_Stream) { - _inheritsLoose$1(Parser, _Stream); - - function Parser() { - var _this; - - _this = _Stream.call(this) || this; - _this.lineStream = new LineStream(); - _this.parseStream = new ParseStream(); - - _this.lineStream.pipe(_this.parseStream); - /* eslint-disable consistent-this */ - - - var self = _assertThisInitialized$1(_this); - /* eslint-enable consistent-this */ - - - var uris = []; - var currentUri = {}; // if specified, the active EXT-X-MAP definition - - var currentMap; // if specified, the active decryption key - - var _key; - - var noop = function noop() {}; - - var defaultMediaGroups = { - 'AUDIO': {}, - 'VIDEO': {}, - 'CLOSED-CAPTIONS': {}, - 'SUBTITLES': {} - }; // This is the Widevine UUID from DASH IF IOP. The same exact string is - // used in MPDs with Widevine encrypted streams. - - var widevineUuid = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; // group segments into numbered timelines delineated by discontinuities - - var currentTimeline = 0; // the manifest is empty until the parse stream begins delivering data - - _this.manifest = { - allowCache: true, - discontinuityStarts: [], - segments: [] - }; // update the manifest with the m3u8 entry from the parse stream - - _this.parseStream.on('data', function (entry) { - var mediaGroup; - var rendition; - ({ - tag: function tag() { - // switch based on the tag type - (({ - 'allow-cache': function allowCache() { - this.manifest.allowCache = entry.allowed; - - if (!('allowed' in entry)) { - this.trigger('info', { - message: 'defaulting allowCache to YES' - }); - this.manifest.allowCache = true; - } - }, - byterange: function byterange() { - var byterange = {}; - - if ('length' in entry) { - currentUri.byterange = byterange; - byterange.length = entry.length; - - if (!('offset' in entry)) { - this.trigger('info', { - message: 'defaulting offset to zero' - }); - entry.offset = 0; - } - } - - if ('offset' in entry) { - currentUri.byterange = byterange; - byterange.offset = entry.offset; - } - }, - endlist: function endlist() { - this.manifest.endList = true; - }, - inf: function inf() { - if (!('mediaSequence' in this.manifest)) { - this.manifest.mediaSequence = 0; - this.trigger('info', { - message: 'defaulting media sequence to zero' - }); - } - - if (!('discontinuitySequence' in this.manifest)) { - this.manifest.discontinuitySequence = 0; - this.trigger('info', { - message: 'defaulting discontinuity sequence to zero' - }); - } - - if (entry.duration > 0) { - currentUri.duration = entry.duration; - } - - if (entry.duration === 0) { - currentUri.duration = 0.01; - this.trigger('info', { - message: 'updating zero segment duration to a small value' - }); - } - - this.manifest.segments = uris; - }, - key: function key() { - if (!entry.attributes) { - this.trigger('warn', { - message: 'ignoring key declaration without attribute list' - }); - return; - } // clear the active encryption key - - - if (entry.attributes.METHOD === 'NONE') { - _key = null; - return; - } - - if (!entry.attributes.URI) { - this.trigger('warn', { - message: 'ignoring key declaration without URI' - }); - return; - } // check if the content is encrypted for Widevine - // Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf - - - if (entry.attributes.KEYFORMAT === widevineUuid) { - var VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC']; - - if (VALID_METHODS.indexOf(entry.attributes.METHOD) === -1) { - this.trigger('warn', { - message: 'invalid key method provided for Widevine' - }); - return; - } - - if (entry.attributes.METHOD === 'SAMPLE-AES-CENC') { - this.trigger('warn', { - message: 'SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead' - }); - } - - if (entry.attributes.URI.substring(0, 23) !== 'data:text/plain;base64,') { - this.trigger('warn', { - message: 'invalid key URI provided for Widevine' - }); - return; - } - - if (!(entry.attributes.KEYID && entry.attributes.KEYID.substring(0, 2) === '0x')) { - this.trigger('warn', { - message: 'invalid key ID provided for Widevine' - }); - return; - } // if Widevine key attributes are valid, store them as `contentProtection` - // on the manifest to emulate Widevine tag structure in a DASH mpd - - - this.manifest.contentProtection = { - 'com.widevine.alpha': { - attributes: { - schemeIdUri: entry.attributes.KEYFORMAT, - // remove '0x' from the key id string - keyId: entry.attributes.KEYID.substring(2) - }, - // decode the base64-encoded PSSH box - pssh: decodeB64ToUint8Array(entry.attributes.URI.split(',')[1]) - } - }; - return; - } - - if (!entry.attributes.METHOD) { - this.trigger('warn', { - message: 'defaulting key method to AES-128' - }); - } // setup an encryption key for upcoming segments - - - _key = { - method: entry.attributes.METHOD || 'AES-128', - uri: entry.attributes.URI - }; - - if (typeof entry.attributes.IV !== 'undefined') { - _key.iv = entry.attributes.IV; - } - }, - 'media-sequence': function mediaSequence() { - if (!isFinite(entry.number)) { - this.trigger('warn', { - message: 'ignoring invalid media sequence: ' + entry.number - }); - return; - } - - this.manifest.mediaSequence = entry.number; - }, - 'discontinuity-sequence': function discontinuitySequence() { - if (!isFinite(entry.number)) { - this.trigger('warn', { - message: 'ignoring invalid discontinuity sequence: ' + entry.number - }); - return; - } - - this.manifest.discontinuitySequence = entry.number; - currentTimeline = entry.number; - }, - 'playlist-type': function playlistType() { - if (!/VOD|EVENT/.test(entry.playlistType)) { - this.trigger('warn', { - message: 'ignoring unknown playlist type: ' + entry.playlist - }); - return; - } - - this.manifest.playlistType = entry.playlistType; - }, - map: function map() { - currentMap = {}; - - if (entry.uri) { - currentMap.uri = entry.uri; - } - - if (entry.byterange) { - currentMap.byterange = entry.byterange; - } - }, - 'stream-inf': function streamInf() { - this.manifest.playlists = uris; - this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; - - if (!entry.attributes) { - this.trigger('warn', { - message: 'ignoring empty stream-inf attributes' - }); - return; - } - - if (!currentUri.attributes) { - currentUri.attributes = {}; - } - - _extends(currentUri.attributes, entry.attributes); - }, - media: function media() { - this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; - - if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) { - this.trigger('warn', { - message: 'ignoring incomplete or missing media group' - }); - return; - } // find the media group, creating defaults as necessary - - - var mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE]; - mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {}; - mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; // collect the rendition metadata - - rendition = { - default: /yes/i.test(entry.attributes.DEFAULT) - }; - - if (rendition.default) { - rendition.autoselect = true; - } else { - rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT); - } - - if (entry.attributes.LANGUAGE) { - rendition.language = entry.attributes.LANGUAGE; - } - - if (entry.attributes.URI) { - rendition.uri = entry.attributes.URI; - } - - if (entry.attributes['INSTREAM-ID']) { - rendition.instreamId = entry.attributes['INSTREAM-ID']; - } - - if (entry.attributes.CHARACTERISTICS) { - rendition.characteristics = entry.attributes.CHARACTERISTICS; - } - - if (entry.attributes.FORCED) { - rendition.forced = /yes/i.test(entry.attributes.FORCED); - } // insert the new rendition - - - mediaGroup[entry.attributes.NAME] = rendition; - }, - discontinuity: function discontinuity() { - currentTimeline += 1; - currentUri.discontinuity = true; - this.manifest.discontinuityStarts.push(uris.length); - }, - 'program-date-time': function programDateTime() { - if (typeof this.manifest.dateTimeString === 'undefined') { - // PROGRAM-DATE-TIME is a media-segment tag, but for backwards - // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag - // to the manifest object - // TODO: Consider removing this in future major version - this.manifest.dateTimeString = entry.dateTimeString; - this.manifest.dateTimeObject = entry.dateTimeObject; - } - - currentUri.dateTimeString = entry.dateTimeString; - currentUri.dateTimeObject = entry.dateTimeObject; - }, - targetduration: function targetduration() { - if (!isFinite(entry.duration) || entry.duration < 0) { - this.trigger('warn', { - message: 'ignoring invalid target duration: ' + entry.duration - }); - return; - } - - this.manifest.targetDuration = entry.duration; - }, - totalduration: function totalduration() { - if (!isFinite(entry.duration) || entry.duration < 0) { - this.trigger('warn', { - message: 'ignoring invalid total duration: ' + entry.duration - }); - return; - } - - this.manifest.totalDuration = entry.duration; - }, - start: function start() { - if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) { - this.trigger('warn', { - message: 'ignoring start declaration without appropriate attribute list' - }); - return; - } - - this.manifest.start = { - timeOffset: entry.attributes['TIME-OFFSET'], - precise: entry.attributes.PRECISE - }; - }, - 'cue-out': function cueOut() { - currentUri.cueOut = entry.data; - }, - 'cue-out-cont': function cueOutCont() { - currentUri.cueOutCont = entry.data; - }, - 'cue-in': function cueIn() { - currentUri.cueIn = entry.data; - } - })[entry.tagType] || noop).call(self); - }, - uri: function uri() { - currentUri.uri = entry.uri; - uris.push(currentUri); // if no explicit duration was declared, use the target duration - - if (this.manifest.targetDuration && !('duration' in currentUri)) { - this.trigger('warn', { - message: 'defaulting segment duration to the target duration' - }); - currentUri.duration = this.manifest.targetDuration; - } // annotate with encryption information, if necessary - - - if (_key) { - currentUri.key = _key; - } - - currentUri.timeline = currentTimeline; // annotate with initialization segment information, if necessary - - if (currentMap) { - currentUri.map = currentMap; - } // prepare for the next URI - - - currentUri = {}; - }, - comment: function comment() {// comments are not important for playback - }, - custom: function custom() { - // if this is segment-level data attach the output to the segment - if (entry.segment) { - currentUri.custom = currentUri.custom || {}; - currentUri.custom[entry.customType] = entry.data; // if this is manifest-level data attach to the top level manifest object - } else { - this.manifest.custom = this.manifest.custom || {}; - this.manifest.custom[entry.customType] = entry.data; - } - } - })[entry.type].call(self); - }); - - return _this; - } - /** - * Parse the input string and update the manifest object. - * - * @param {string} chunk a potentially incomplete portion of the manifest - */ - - - var _proto = Parser.prototype; - - _proto.push = function push(chunk) { - this.lineStream.push(chunk); - } - /** - * Flush any remaining input. This can be handy if the last line of an M3U8 - * manifest did not contain a trailing newline but the file has been - * completely received. - */ - ; - - _proto.end = function end() { - // flush any buffered input - this.lineStream.push('\n'); - } - /** - * Add an additional parser for non-standard tags - * - * @param {Object} options a map of options for the added parser - * @param {RegExp} options.expression a regular expression to match the custom header - * @param {string} options.type the type to register to the output - * @param {Function} [options.dataParser] function to parse the line into an object - * @param {boolean} [options.segment] should tag data be attached to the segment object - */ - ; - - _proto.addParser = function addParser(options) { - this.parseStream.addParser(options); - } - /** - * Add a custom header mapper - * - * @param {Object} options - * @param {RegExp} options.expression a regular expression to match the custom header - * @param {Function} options.map function to translate tag into a different tag - */ - ; - - _proto.addTagMapper = function addTagMapper(options) { - this.parseStream.addTagMapper(options); - }; - - return Parser; -}(Stream); - -function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } - -var URLToolkit = _interopDefault(urlToolkit); -var window$1 = _interopDefault(window_1$1); - -var resolveUrl = function resolveUrl(baseUrl, relativeUrl) { - // return early if we don't need to resolve - if (/^[a-z]+:/i.test(relativeUrl)) { - return relativeUrl; - } // if the base URL is relative then combine with the current location - - - if (!/\/\//i.test(baseUrl)) { - baseUrl = URLToolkit.buildAbsoluteURL(window$1.location && window$1.location.href || '', baseUrl); - } - - return URLToolkit.buildAbsoluteURL(baseUrl, relativeUrl); -}; - -var resolveUrl_1 = resolveUrl; - -function _interopDefault$1 (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } - -var window$2 = _interopDefault$1(window_1$1); - -var atob = function atob(s) { - return window$2.atob ? window$2.atob(s) : Buffer.from(s, 'base64').toString('binary'); -}; - -function decodeB64ToUint8Array$1(b64Text) { - var decodedString = atob(b64Text); - var array = new Uint8Array(decodedString.length); - - for (var i = 0; i < decodedString.length; i++) { - array[i] = decodedString.charCodeAt(i); - } - - return array; -} - -var decodeB64ToUint8Array_1 = decodeB64ToUint8Array$1; +import { c as createCommonjsModule, g as getDefaultExportFromCjs, d as document_1, w as window_1, a as core, b as commonjsGlobal } from '../../../common/core-02e93804.js'; //[4] NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] //[4a] NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] @@ -4007,7 +12,7 @@ var tagNamePattern = new RegExp('^'+nameStartChar.source+nameChar.source+'*(?:\: //S_TAG, S_ATTR, S_EQ, S_ATTR_NOQUOT_VALUE //S_ATTR_SPACE, S_ATTR_END, S_TAG_SPACE, S_TAG_CLOSE var S_TAG = 0;//tag name offerring -var S_ATTR = 1;//attr name offerring +var S_ATTR = 1;//attr name offerring var S_ATTR_SPACE=2;//attr name end and space offer var S_EQ = 3;//=space? var S_ATTR_NOQUOT_VALUE = 4;//attr value(no quot value only) @@ -4016,7 +21,7 @@ var S_TAG_SPACE = 6;//(attr value end || tag end ) && (space offer) var S_TAG_CLOSE = 7;//closed el function XMLReader(){ - + } XMLReader.prototype = { @@ -4046,7 +51,7 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ function entityReplacer(a){ var k = a.slice(1,-1); if(k in entityMap){ - return entityMap[k]; + return entityMap[k]; }else if(k.charAt(0) === '#'){ return fixedFromCharCode(parseInt(k.substr(1).replace('x','0x'))) }else { @@ -4075,7 +80,7 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ var lineEnd = 0; var linePattern = /.*(?:\r\n?|\n)|.*$/g; var locator = domBuilder.locator; - + var parseStack = [{currentNSMap:defaultNSMapCopy}]; var closeMap = {}; var start = 0; @@ -4085,9 +90,9 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ if(tagStart<0){ if(!source.substr(start).match(/^\s*$/)){ var doc = domBuilder.doc; - var text = doc.createTextNode(source.substr(start)); - doc.appendChild(text); - domBuilder.currentElement = text; + var text = doc.createTextNode(source.substr(start)); + doc.appendChild(text); + domBuilder.currentElement = text; } return; } @@ -4100,15 +105,15 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ var tagName = source.substring(tagStart+2,end); var config = parseStack.pop(); if(end<0){ - - tagName = source.substring(tagStart+2).replace(/[\s<].*/,''); - //console.error('#@@@@@@'+tagName) - errorHandler.error("end tag name: "+tagName+' is not complete:'+config.tagName); - end = tagStart+1+tagName.length; - }else if(tagName.match(/\s',start+9); domBuilder.startCDATA(); domBuilder.characters(source,start+9,end-start-9); - domBuilder.endCDATA(); + domBuilder.endCDATA(); return end+3; } //1 && /!doctype/i.test(matchs[0][0])){ @@ -4534,7 +539,7 @@ function parseDCC(source,start,domBuilder,errorHandler){//sure start with '0; + return this.attributes.length>0; }, lookupPrefix:function(namespaceURI){ - var el = this; - while(el){ - var map = el._nsMap; - //console.dir(map) - if(map){ - for(var n in map){ - if(map[n] == namespaceURI){ - return n; - } - } - } - el = el.nodeType == ATTRIBUTE_NODE?el.ownerDocument : el.parentNode; - } - return null; + var el = this; + while(el){ + var map = el._nsMap; + //console.dir(map) + if(map){ + for(var n in map){ + if(map[n] == namespaceURI){ + return n; + } + } + } + el = el.nodeType == ATTRIBUTE_NODE?el.ownerDocument : el.parentNode; + } + return null; }, // Introduced in DOM Level 3: lookupNamespaceURI:function(prefix){ - var el = this; - while(el){ - var map = el._nsMap; - //console.dir(map) - if(map){ - if(prefix in map){ - return map[prefix] ; - } - } - el = el.nodeType == ATTRIBUTE_NODE?el.ownerDocument : el.parentNode; - } - return null; + var el = this; + while(el){ + var map = el._nsMap; + //console.dir(map) + if(map){ + if(prefix in map){ + return map[prefix] ; + } + } + el = el.nodeType == ATTRIBUTE_NODE?el.ownerDocument : el.parentNode; + } + return null; }, // Introduced in DOM Level 3: isDefaultNamespace:function(namespaceURI){ - var prefix = this.lookupPrefix(namespaceURI); - return prefix == null; + var prefix = this.lookupPrefix(namespaceURI); + return prefix == null; } }; @@ -5106,7 +1111,7 @@ function _onUpdateChild(doc,el,newChild){ /** * attributes; * children; - * + * * writeable properties: * nodeValue,Attr:value,CharacterData:data * prefix @@ -5148,8 +1153,8 @@ function _insertBefore(parentNode,newChild,nextChild){ newFirst.previousSibling = pre; newLast.nextSibling = nextChild; - - + + if(pre){ pre.nextSibling = newFirst; }else { @@ -5198,8 +1203,8 @@ Document.prototype = { doctype : null, documentElement : null, _inc : 1, - - insertBefore : function(newChild, refChild){//raises + + insertBefore : function(newChild, refChild){//raises if(newChild.nodeType == DOCUMENT_FRAGMENT_NODE){ var child = newChild.firstChild; while(child){ @@ -5212,7 +1217,7 @@ Document.prototype = { if(this.documentElement == null && newChild.nodeType == ELEMENT_NODE){ this.documentElement = newChild; } - + return _insertBefore(this,newChild,refChild),(newChild.ownerDocument = this),newChild; }, removeChild : function(oldChild){ @@ -5238,7 +1243,7 @@ Document.prototype = { }); return rtv; }, - + //document factory method: createElement : function(tagName){ var node = new Element(); @@ -5335,7 +1340,7 @@ Document.prototype = { return node; } }; -_extends$1(Document,Node); +_extends(Document,Node); function Element() { @@ -5361,7 +1366,7 @@ function Element() { var attr = this.getAttributeNode(name); attr && this.removeAttributeNode(attr); }, - + //four real opeartion method appendChild:function(newChild){ if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){ @@ -5385,7 +1390,7 @@ function Element() { var old = this.getAttributeNodeNS(namespaceURI, localName); old && this.removeAttributeNode(old); }, - + hasAttributeNS : function(namespaceURI, localName){ return this.getAttributeNodeNS(namespaceURI, localName)!=null; }, @@ -5401,7 +1406,7 @@ function Element() { getAttributeNodeNS : function(namespaceURI, localName){ return this.attributes.getNamedItemNS(namespaceURI, localName); }, - + getElementsByTagName : function(tagName){ return new LiveNodeList(this,function(base){ var ls = []; @@ -5422,7 +1427,7 @@ function Element() { } }); return ls; - + }); } }; @@ -5430,10 +1435,10 @@ Document.prototype.getElementsByTagName = Element.prototype.getElementsByTagName Document.prototype.getElementsByTagNameNS = Element.prototype.getElementsByTagNameNS; -_extends$1(Element,Node); +_extends(Element,Node); function Attr() { }Attr.prototype.nodeType = ATTRIBUTE_NODE; -_extends$1(Attr,Node); +_extends(Attr,Node); function CharacterData() { @@ -5449,7 +1454,7 @@ function CharacterData() { }, insertData: function(offset,text) { this.replaceData(offset,0,text); - + }, appendChild:function(newChild){ throw new Error(ExceptionMessage[HIERARCHY_REQUEST_ERR]) @@ -5465,7 +1470,7 @@ function CharacterData() { this.length = text.length; } }; -_extends$1(CharacterData,Node); +_extends(CharacterData,Node); function Text() { }Text.prototype = { nodeName : "#text", @@ -5483,48 +1488,48 @@ function Text() { return newNode; } }; -_extends$1(Text,CharacterData); +_extends(Text,CharacterData); function Comment() { }Comment.prototype = { nodeName : "#comment", nodeType : COMMENT_NODE }; -_extends$1(Comment,CharacterData); +_extends(Comment,CharacterData); function CDATASection() { }CDATASection.prototype = { nodeName : "#cdata-section", nodeType : CDATA_SECTION_NODE }; -_extends$1(CDATASection,CharacterData); +_extends(CDATASection,CharacterData); function DocumentType() { }DocumentType.prototype.nodeType = DOCUMENT_TYPE_NODE; -_extends$1(DocumentType,Node); +_extends(DocumentType,Node); function Notation() { }Notation.prototype.nodeType = NOTATION_NODE; -_extends$1(Notation,Node); +_extends(Notation,Node); function Entity() { }Entity.prototype.nodeType = ENTITY_NODE; -_extends$1(Entity,Node); +_extends(Entity,Node); function EntityReference() { }EntityReference.prototype.nodeType = ENTITY_REFERENCE_NODE; -_extends$1(EntityReference,Node); +_extends(EntityReference,Node); function DocumentFragment() { }DocumentFragment.prototype.nodeName = "#document-fragment"; DocumentFragment.prototype.nodeType = DOCUMENT_FRAGMENT_NODE; -_extends$1(DocumentFragment,Node); +_extends(DocumentFragment,Node); function ProcessingInstruction() { } ProcessingInstruction.prototype.nodeType = PROCESSING_INSTRUCTION_NODE; -_extends$1(ProcessingInstruction,Node); +_extends(ProcessingInstruction,Node); function XMLSerializer(){} XMLSerializer.prototype.serializeToString = function(node,isHtml,nodeFilter){ return nodeSerializeToString.call(node,isHtml,nodeFilter); @@ -5557,12 +1562,12 @@ function needNamespaceDefine(node,isHTML, visibleNamespaces) { if (!prefix && !uri){ return false; } - if (prefix === "xml" && uri === "http://www.w3.org/XML/1998/namespace" + if (prefix === "xml" && uri === "http://www.w3.org/XML/1998/namespace" || uri == 'http://www.w3.org/2000/xmlns/'){ return false; } - - var i = visibleNamespaces.length; + + var i = visibleNamespaces.length; //console.log('@@@@',node.tagName,prefix,uri,visibleNamespaces) while (i--) { var ns = visibleNamespaces[i]; @@ -5601,12 +1606,12 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){ var len = attrs.length; var child = node.firstChild; var nodeName = node.tagName; - - isHTML = (htmlns === node.namespaceURI) ||isHTML; + + isHTML = (htmlns === node.namespaceURI) ||isHTML; buf.push('<',nodeName); - - - + + + for(var i=0;i'); //if is cdata child node @@ -5827,7 +1832,7 @@ try{ } } }); - + function getTextContent(node){ switch(node.nodeType){ case ELEMENT_NODE: @@ -5866,7 +1871,7 @@ var dom = { var domParser = createCommonjsModule(function (module, exports) { function DOMParser(options){ this.options = options ||{locator:{}}; - + } DOMParser.prototype.parseFromString = function(source,mimeType){ var options = this.options; @@ -5879,7 +1884,7 @@ DOMParser.prototype.parseFromString = function(source,mimeType){ if(locator){ domBuilder.setDocumentLocator(locator); } - + sax.errorHandler = buildErrorHandler(errorHandler,domBuilder,locator); sax.domBuilder = options.domBuilder || domBuilder; if(/\/x?html?$/.test(mimeType)){ @@ -5924,8 +1929,8 @@ function buildErrorHandler(errorImpl,domBuilder,locator){ /** * +ContentHandler+ErrorHandler * +LexicalHandler+EntityResolver2 - * -DeclHandler-DTDHandler - * + * -DeclHandler-DTDHandler + * * DefaultHandler:EntityResolver, DTDHandler, ContentHandler, ErrorHandler * DefaultHandler2:DefaultHandler,LexicalHandler, DeclHandler, EntityResolver2 * @link http://www.saxproject.org/apidoc/org/xml/sax/helpers/DefaultHandler.html @@ -5940,13 +1945,13 @@ function position(locator,node){ /** * @see org.xml.sax.ContentHandler#startDocument * @link http://www.saxproject.org/apidoc/org/xml/sax/ContentHandler.html - */ + */ DOMHandler.prototype = { startDocument : function() { - this.doc = new DOMImplementation().createDocument(null, null, null); - if (this.locator) { - this.doc.documentURI = this.locator.systemId; - } + this.doc = new DOMImplementation().createDocument(null, null, null); + if (this.locator) { + this.doc.documentURI = this.locator.systemId; + } }, startElement:function(namespaceURI, localName, qName, attrs) { var doc = this.doc; @@ -5954,7 +1959,7 @@ DOMHandler.prototype = { var len = attrs.length; appendElement(this, el); this.currentElement = el; - + this.locator && position(this.locator,el); for (var i = 0 ; i < len; i++) { var namespaceURI = attrs.getURI(i); @@ -6007,7 +2012,7 @@ DOMHandler.prototype = { }, setDocumentLocator:function (locator) { if(this.locator = locator){// && !('lineNumber' in locator)){ - locator.lineNumber = 0; + locator.lineNumber = 0; } }, //LexicalHandler @@ -6017,7 +2022,7 @@ DOMHandler.prototype = { this.locator && position(this.locator,comm); appendElement(this, comm); }, - + startCDATA:function() { //used in characters() methods this.cdata = true; @@ -6025,7 +2030,7 @@ DOMHandler.prototype = { endCDATA:function() { this.cdata = false; }, - + startDTD:function(name, publicId, systemId) { var impl = this.doc.implementation; if (impl && impl.createDocumentType) { @@ -6117,51995 +2122,9 @@ function appendElement (hander,node) { //} }); -/*! @name mpd-parser @version 0.10.0 @license Apache-2.0 */ - -var isObject = function isObject(obj) { - return !!obj && typeof obj === 'object'; -}; - -var merge = function merge() { - for (var _len = arguments.length, objects = new Array(_len), _key = 0; _key < _len; _key++) { - objects[_key] = arguments[_key]; - } - - return objects.reduce(function (result, source) { - Object.keys(source).forEach(function (key) { - if (Array.isArray(result[key]) && Array.isArray(source[key])) { - result[key] = result[key].concat(source[key]); - } else if (isObject(result[key]) && isObject(source[key])) { - result[key] = merge(result[key], source[key]); - } else { - result[key] = source[key]; - } - }); - return result; - }, {}); -}; -var values = function values(o) { - return Object.keys(o).map(function (k) { - return o[k]; - }); -}; - -var range = function range(start, end) { - var result = []; - - for (var i = start; i < end; i++) { - result.push(i); - } - - return result; -}; -var flatten = function flatten(lists) { - return lists.reduce(function (x, y) { - return x.concat(y); - }, []); -}; -var from = function from(list) { - if (!list.length) { - return []; - } - - var result = []; - - for (var i = 0; i < list.length; i++) { - result.push(list[i]); - } - - return result; -}; -var findIndexes = function findIndexes(l, key) { - return l.reduce(function (a, e, i) { - if (e[key]) { - a.push(i); - } - - return a; - }, []); -}; - -var errors = { - INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD', - DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST', - DASH_INVALID_XML: 'DASH_INVALID_XML', - NO_BASE_URL: 'NO_BASE_URL', - MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION', - SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED', - UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME' -}; - -/** - * @typedef {Object} SingleUri - * @property {string} uri - relative location of segment - * @property {string} resolvedUri - resolved location of segment - * @property {Object} byterange - Object containing information on how to make byte range - * requests following byte-range-spec per RFC2616. - * @property {String} byterange.length - length of range request - * @property {String} byterange.offset - byte offset of range request - * - * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1 - */ - -/** - * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object - * that conforms to how m3u8-parser is structured - * - * @see https://github.com/videojs/m3u8-parser - * - * @param {string} baseUrl - baseUrl provided by nodes - * @param {string} source - source url for segment - * @param {string} range - optional range used for range calls, - * follows RFC 2616, Clause 14.35.1 - * @return {SingleUri} full segment information transformed into a format similar - * to m3u8-parser - */ - -var urlTypeToSegment = function urlTypeToSegment(_ref) { - var _ref$baseUrl = _ref.baseUrl, - baseUrl = _ref$baseUrl === void 0 ? '' : _ref$baseUrl, - _ref$source = _ref.source, - source = _ref$source === void 0 ? '' : _ref$source, - _ref$range = _ref.range, - range = _ref$range === void 0 ? '' : _ref$range, - _ref$indexRange = _ref.indexRange, - indexRange = _ref$indexRange === void 0 ? '' : _ref$indexRange; - var segment = { - uri: source, - resolvedUri: resolveUrl_1(baseUrl || '', source) - }; - - if (range || indexRange) { - var rangeStr = range ? range : indexRange; - var ranges = rangeStr.split('-'); - var startRange = parseInt(ranges[0], 10); - var endRange = parseInt(ranges[1], 10); // byterange should be inclusive according to - // RFC 2616, Clause 14.35.1 - - segment.byterange = { - length: endRange - startRange + 1, - offset: startRange - }; - } - - return segment; -}; -var byteRangeToString = function byteRangeToString(byterange) { - // `endRange` is one less than `offset + length` because the HTTP range - // header uses inclusive ranges - var endRange = byterange.offset + byterange.length - 1; - return byterange.offset + "-" + endRange; -}; - -/** - * Functions for calculating the range of available segments in static and dynamic - * manifests. - */ - -var segmentRange = { - /** - * Returns the entire range of available segments for a static MPD - * - * @param {Object} attributes - * Inheritied MPD attributes - * @return {{ start: number, end: number }} - * The start and end numbers for available segments - */ - static: function _static(attributes) { - var duration = attributes.duration, - _attributes$timescale = attributes.timescale, - timescale = _attributes$timescale === void 0 ? 1 : _attributes$timescale, - sourceDuration = attributes.sourceDuration; - return { - start: 0, - end: Math.ceil(sourceDuration / (duration / timescale)) - }; - }, - - /** - * Returns the current live window range of available segments for a dynamic MPD - * - * @param {Object} attributes - * Inheritied MPD attributes - * @return {{ start: number, end: number }} - * The start and end numbers for available segments - */ - dynamic: function dynamic(attributes) { - var NOW = attributes.NOW, - clientOffset = attributes.clientOffset, - availabilityStartTime = attributes.availabilityStartTime, - _attributes$timescale2 = attributes.timescale, - timescale = _attributes$timescale2 === void 0 ? 1 : _attributes$timescale2, - duration = attributes.duration, - _attributes$start = attributes.start, - start = _attributes$start === void 0 ? 0 : _attributes$start, - _attributes$minimumUp = attributes.minimumUpdatePeriod, - minimumUpdatePeriod = _attributes$minimumUp === void 0 ? 0 : _attributes$minimumUp, - _attributes$timeShift = attributes.timeShiftBufferDepth, - timeShiftBufferDepth = _attributes$timeShift === void 0 ? Infinity : _attributes$timeShift; - var now = (NOW + clientOffset) / 1000; - var periodStartWC = availabilityStartTime + start; - var periodEndWC = now + minimumUpdatePeriod; - var periodDuration = periodEndWC - periodStartWC; - var segmentCount = Math.ceil(periodDuration * timescale / duration); - var availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration); - var availableEnd = Math.floor((now - periodStartWC) * timescale / duration); - return { - start: Math.max(0, availableStart), - end: Math.min(segmentCount, availableEnd) - }; - } -}; -/** - * Maps a range of numbers to objects with information needed to build the corresponding - * segment list - * - * @name toSegmentsCallback - * @function - * @param {number} number - * Number of the segment - * @param {number} index - * Index of the number in the range list - * @return {{ number: Number, duration: Number, timeline: Number, time: Number }} - * Object with segment timing and duration info - */ - -/** - * Returns a callback for Array.prototype.map for mapping a range of numbers to - * information needed to build the segment list. - * - * @param {Object} attributes - * Inherited MPD attributes - * @return {toSegmentsCallback} - * Callback map function - */ - -var toSegments = function toSegments(attributes) { - return function (number, index) { - var duration = attributes.duration, - _attributes$timescale3 = attributes.timescale, - timescale = _attributes$timescale3 === void 0 ? 1 : _attributes$timescale3, - periodIndex = attributes.periodIndex, - _attributes$startNumb = attributes.startNumber, - startNumber = _attributes$startNumb === void 0 ? 1 : _attributes$startNumb; - return { - number: startNumber + number, - duration: duration / timescale, - timeline: periodIndex, - time: index * duration - }; - }; -}; -/** - * Returns a list of objects containing segment timing and duration info used for - * building the list of segments. This uses the @duration attribute specified - * in the MPD manifest to derive the range of segments. - * - * @param {Object} attributes - * Inherited MPD attributes - * @return {{number: number, duration: number, time: number, timeline: number}[]} - * List of Objects with segment timing and duration info - */ - -var parseByDuration = function parseByDuration(attributes) { - var _attributes$type = attributes.type, - type = _attributes$type === void 0 ? 'static' : _attributes$type, - duration = attributes.duration, - _attributes$timescale4 = attributes.timescale, - timescale = _attributes$timescale4 === void 0 ? 1 : _attributes$timescale4, - sourceDuration = attributes.sourceDuration; - - var _segmentRange$type = segmentRange[type](attributes), - start = _segmentRange$type.start, - end = _segmentRange$type.end; - - var segments = range(start, end).map(toSegments(attributes)); - - if (type === 'static') { - var index = segments.length - 1; // final segment may be less than full segment duration - - segments[index].duration = sourceDuration - duration / timescale * index; - } - - return segments; -}; - -/** - * Translates SegmentBase into a set of segments. - * (DASH SPEC Section 5.3.9.3.2) contains a set of nodes. Each - * node should be translated into segment. - * - * @param {Object} attributes - * Object containing all inherited attributes from parent elements with attribute - * names as keys - * @return {Object.} list of segments - */ - -var segmentsFromBase = function segmentsFromBase(attributes) { - var baseUrl = attributes.baseUrl, - _attributes$initializ = attributes.initialization, - initialization = _attributes$initializ === void 0 ? {} : _attributes$initializ, - sourceDuration = attributes.sourceDuration, - _attributes$timescale = attributes.timescale, - timescale = _attributes$timescale === void 0 ? 1 : _attributes$timescale, - _attributes$indexRang = attributes.indexRange, - indexRange = _attributes$indexRang === void 0 ? '' : _attributes$indexRang, - duration = attributes.duration; // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1) - - if (!baseUrl) { - throw new Error(errors.NO_BASE_URL); - } - - var initSegment = urlTypeToSegment({ - baseUrl: baseUrl, - source: initialization.sourceURL, - range: initialization.range - }); - var segment = urlTypeToSegment({ - baseUrl: baseUrl, - source: baseUrl, - indexRange: indexRange - }); - segment.map = initSegment; // If there is a duration, use it, otherwise use the given duration of the source - // (since SegmentBase is only for one total segment) - - if (duration) { - var segmentTimeInfo = parseByDuration(attributes); - - if (segmentTimeInfo.length) { - segment.duration = segmentTimeInfo[0].duration; - segment.timeline = segmentTimeInfo[0].timeline; - } - } else if (sourceDuration) { - segment.duration = sourceDuration / timescale; - segment.timeline = 0; - } // This is used for mediaSequence - - - segment.number = 0; - return [segment]; -}; -/** - * Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist - * according to the sidx information given. - * - * playlist.sidx has metadadata about the sidx where-as the sidx param - * is the parsed sidx box itself. - * - * @param {Object} playlist the playlist to update the sidx information for - * @param {Object} sidx the parsed sidx box - * @return {Object} the playlist object with the updated sidx information - */ - -var addSegmentsToPlaylist = function addSegmentsToPlaylist(playlist, sidx, baseUrl) { - // Retain init segment information - var initSegment = playlist.sidx.map ? playlist.sidx.map : null; // Retain source duration from initial master manifest parsing - - var sourceDuration = playlist.sidx.duration; // Retain source timeline - - var timeline = playlist.timeline || 0; - var sidxByteRange = playlist.sidx.byterange; - var sidxEnd = sidxByteRange.offset + sidxByteRange.length; // Retain timescale of the parsed sidx - - var timescale = sidx.timescale; // referenceType 1 refers to other sidx boxes - - var mediaReferences = sidx.references.filter(function (r) { - return r.referenceType !== 1; - }); - var segments = []; // firstOffset is the offset from the end of the sidx box - - var startIndex = sidxEnd + sidx.firstOffset; - - for (var i = 0; i < mediaReferences.length; i++) { - var reference = sidx.references[i]; // size of the referenced (sub)segment - - var size = reference.referencedSize; // duration of the referenced (sub)segment, in the timescale - // this will be converted to seconds when generating segments - - var duration = reference.subsegmentDuration; // should be an inclusive range - - var endIndex = startIndex + size - 1; - var indexRange = startIndex + "-" + endIndex; - var attributes = { - baseUrl: baseUrl, - timescale: timescale, - timeline: timeline, - // this is used in parseByDuration - periodIndex: timeline, - duration: duration, - sourceDuration: sourceDuration, - indexRange: indexRange - }; - var segment = segmentsFromBase(attributes)[0]; - - if (initSegment) { - segment.map = initSegment; - } - - segments.push(segment); - startIndex += size; - } - - playlist.segments = segments; - return playlist; -}; - -var mergeDiscontiguousPlaylists = function mergeDiscontiguousPlaylists(playlists) { - var mergedPlaylists = values(playlists.reduce(function (acc, playlist) { - // assuming playlist IDs are the same across periods - // TODO: handle multiperiod where representation sets are not the same - // across periods - var name = playlist.attributes.id + (playlist.attributes.lang || ''); // Periods after first - - if (acc[name]) { - var _acc$name$segments; - - // first segment of subsequent periods signal a discontinuity - if (playlist.segments[0]) { - playlist.segments[0].discontinuity = true; - } - - (_acc$name$segments = acc[name].segments).push.apply(_acc$name$segments, playlist.segments); // bubble up contentProtection, this assumes all DRM content - // has the same contentProtection - - - if (playlist.attributes.contentProtection) { - acc[name].attributes.contentProtection = playlist.attributes.contentProtection; - } - } else { - // first Period - acc[name] = playlist; - } - - return acc; - }, {})); - return mergedPlaylists.map(function (playlist) { - playlist.discontinuityStarts = findIndexes(playlist.segments, 'discontinuity'); - return playlist; - }); -}; - -var addSegmentInfoFromSidx = function addSegmentInfoFromSidx(playlists, sidxMapping) { - if (sidxMapping === void 0) { - sidxMapping = {}; - } - - if (!Object.keys(sidxMapping).length) { - return playlists; - } - - for (var i in playlists) { - var playlist = playlists[i]; - - if (!playlist.sidx) { - continue; - } - - var sidxKey = playlist.sidx.uri + '-' + byteRangeToString(playlist.sidx.byterange); - var sidxMatch = sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx; - - if (playlist.sidx && sidxMatch) { - addSegmentsToPlaylist(playlist, sidxMatch, playlist.sidx.resolvedUri); - } - } - - return playlists; -}; - -var formatAudioPlaylist = function formatAudioPlaylist(_ref) { - var _attributes; - - var attributes = _ref.attributes, - segments = _ref.segments, - sidx = _ref.sidx; - var playlist = { - attributes: (_attributes = { - NAME: attributes.id, - BANDWIDTH: attributes.bandwidth, - CODECS: attributes.codecs - }, _attributes['PROGRAM-ID'] = 1, _attributes), - uri: '', - endList: (attributes.type || 'static') === 'static', - timeline: attributes.periodIndex, - resolvedUri: '', - targetDuration: attributes.duration, - segments: segments, - mediaSequence: segments.length ? segments[0].number : 1 - }; - - if (attributes.contentProtection) { - playlist.contentProtection = attributes.contentProtection; - } - - if (sidx) { - playlist.sidx = sidx; - } - - return playlist; -}; -var formatVttPlaylist = function formatVttPlaylist(_ref2) { - var _attributes2; - - var attributes = _ref2.attributes, - segments = _ref2.segments; - - if (typeof segments === 'undefined') { - // vtt tracks may use single file in BaseURL - segments = [{ - uri: attributes.baseUrl, - timeline: attributes.periodIndex, - resolvedUri: attributes.baseUrl || '', - duration: attributes.sourceDuration, - number: 0 - }]; // targetDuration should be the same duration as the only segment - - attributes.duration = attributes.sourceDuration; - } - - return { - attributes: (_attributes2 = { - NAME: attributes.id, - BANDWIDTH: attributes.bandwidth - }, _attributes2['PROGRAM-ID'] = 1, _attributes2), - uri: '', - endList: (attributes.type || 'static') === 'static', - timeline: attributes.periodIndex, - resolvedUri: attributes.baseUrl || '', - targetDuration: attributes.duration, - segments: segments, - mediaSequence: segments.length ? segments[0].number : 1 - }; -}; -var organizeAudioPlaylists = function organizeAudioPlaylists(playlists, sidxMapping) { - if (sidxMapping === void 0) { - sidxMapping = {}; - } - - var mainPlaylist; - var formattedPlaylists = playlists.reduce(function (a, playlist) { - var role = playlist.attributes.role && playlist.attributes.role.value || ''; - var language = playlist.attributes.lang || ''; - var label = 'main'; - - if (language) { - var roleLabel = role ? " (" + role + ")" : ''; - label = "" + playlist.attributes.lang + roleLabel; - } // skip if we already have the highest quality audio for a language - - - if (a[label] && a[label].playlists[0].attributes.BANDWIDTH > playlist.attributes.bandwidth) { - return a; - } - - a[label] = { - language: language, - autoselect: true, - default: role === 'main', - playlists: addSegmentInfoFromSidx([formatAudioPlaylist(playlist)], sidxMapping), - uri: '' - }; - - if (typeof mainPlaylist === 'undefined' && role === 'main') { - mainPlaylist = playlist; - mainPlaylist.default = true; - } - - return a; - }, {}); // if no playlists have role "main", mark the first as main - - if (!mainPlaylist) { - var firstLabel = Object.keys(formattedPlaylists)[0]; - formattedPlaylists[firstLabel].default = true; - } - - return formattedPlaylists; -}; -var organizeVttPlaylists = function organizeVttPlaylists(playlists, sidxMapping) { - if (sidxMapping === void 0) { - sidxMapping = {}; - } - - return playlists.reduce(function (a, playlist) { - var label = playlist.attributes.lang || 'text'; // skip if we already have subtitles - - if (a[label]) { - return a; - } - - a[label] = { - language: label, - default: false, - autoselect: false, - playlists: addSegmentInfoFromSidx([formatVttPlaylist(playlist)], sidxMapping), - uri: '' - }; - return a; - }, {}); -}; -var formatVideoPlaylist = function formatVideoPlaylist(_ref3) { - var _attributes3; - - var attributes = _ref3.attributes, - segments = _ref3.segments, - sidx = _ref3.sidx; - var playlist = { - attributes: (_attributes3 = { - NAME: attributes.id, - AUDIO: 'audio', - SUBTITLES: 'subs', - RESOLUTION: { - width: attributes.width, - height: attributes.height - }, - CODECS: attributes.codecs, - BANDWIDTH: attributes.bandwidth - }, _attributes3['PROGRAM-ID'] = 1, _attributes3), - uri: '', - endList: (attributes.type || 'static') === 'static', - timeline: attributes.periodIndex, - resolvedUri: '', - targetDuration: attributes.duration, - segments: segments, - mediaSequence: segments.length ? segments[0].number : 1 - }; - - if (attributes.contentProtection) { - playlist.contentProtection = attributes.contentProtection; - } - - if (sidx) { - playlist.sidx = sidx; - } - - return playlist; -}; -var toM3u8 = function toM3u8(dashPlaylists, sidxMapping) { - var _mediaGroups; - - if (sidxMapping === void 0) { - sidxMapping = {}; - } - - if (!dashPlaylists.length) { - return {}; - } // grab all master attributes - - - var _dashPlaylists$0$attr = dashPlaylists[0].attributes, - duration = _dashPlaylists$0$attr.sourceDuration, - _dashPlaylists$0$attr2 = _dashPlaylists$0$attr.type, - type = _dashPlaylists$0$attr2 === void 0 ? 'static' : _dashPlaylists$0$attr2, - suggestedPresentationDelay = _dashPlaylists$0$attr.suggestedPresentationDelay, - _dashPlaylists$0$attr3 = _dashPlaylists$0$attr.minimumUpdatePeriod, - minimumUpdatePeriod = _dashPlaylists$0$attr3 === void 0 ? 0 : _dashPlaylists$0$attr3; - - var videoOnly = function videoOnly(_ref4) { - var attributes = _ref4.attributes; - return attributes.mimeType === 'video/mp4' || attributes.contentType === 'video'; - }; - - var audioOnly = function audioOnly(_ref5) { - var attributes = _ref5.attributes; - return attributes.mimeType === 'audio/mp4' || attributes.contentType === 'audio'; - }; - - var vttOnly = function vttOnly(_ref6) { - var attributes = _ref6.attributes; - return attributes.mimeType === 'text/vtt' || attributes.contentType === 'text'; - }; - - var videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist); - var audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly)); - var vttPlaylists = dashPlaylists.filter(vttOnly); - var master = { - allowCache: true, - discontinuityStarts: [], - segments: [], - endList: true, - mediaGroups: (_mediaGroups = { - AUDIO: {}, - VIDEO: {} - }, _mediaGroups['CLOSED-CAPTIONS'] = {}, _mediaGroups.SUBTITLES = {}, _mediaGroups), - uri: '', - duration: duration, - playlists: addSegmentInfoFromSidx(videoPlaylists, sidxMapping), - minimumUpdatePeriod: minimumUpdatePeriod * 1000 - }; - - if (type === 'dynamic') { - master.suggestedPresentationDelay = suggestedPresentationDelay; - } - - if (audioPlaylists.length) { - master.mediaGroups.AUDIO.audio = organizeAudioPlaylists(audioPlaylists, sidxMapping); - } - - if (vttPlaylists.length) { - master.mediaGroups.SUBTITLES.subs = organizeVttPlaylists(vttPlaylists, sidxMapping); - } - - return master; -}; - -/** - * Calculates the R (repetition) value for a live stream (for the final segment - * in a manifest where the r value is negative 1) - * - * @param {Object} attributes - * Object containing all inherited attributes from parent elements with attribute - * names as keys - * @param {number} time - * current time (typically the total time up until the final segment) - * @param {number} duration - * duration property for the given - * - * @return {number} - * R value to reach the end of the given period - */ -var getLiveRValue = function getLiveRValue(attributes, time, duration) { - var NOW = attributes.NOW, - clientOffset = attributes.clientOffset, - availabilityStartTime = attributes.availabilityStartTime, - _attributes$timescale = attributes.timescale, - timescale = _attributes$timescale === void 0 ? 1 : _attributes$timescale, - _attributes$start = attributes.start, - start = _attributes$start === void 0 ? 0 : _attributes$start, - _attributes$minimumUp = attributes.minimumUpdatePeriod, - minimumUpdatePeriod = _attributes$minimumUp === void 0 ? 0 : _attributes$minimumUp; - var now = (NOW + clientOffset) / 1000; - var periodStartWC = availabilityStartTime + start; - var periodEndWC = now + minimumUpdatePeriod; - var periodDuration = periodEndWC - periodStartWC; - return Math.ceil((periodDuration * timescale - time) / duration); -}; -/** - * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment - * timing and duration - * - * @param {Object} attributes - * Object containing all inherited attributes from parent elements with attribute - * names as keys - * @param {Object[]} segmentTimeline - * List of objects representing the attributes of each S element contained within - * - * @return {{number: number, duration: number, time: number, timeline: number}[]} - * List of Objects with segment timing and duration info - */ - - -var parseByTimeline = function parseByTimeline(attributes, segmentTimeline) { - var _attributes$type = attributes.type, - type = _attributes$type === void 0 ? 'static' : _attributes$type, - _attributes$minimumUp2 = attributes.minimumUpdatePeriod, - minimumUpdatePeriod = _attributes$minimumUp2 === void 0 ? 0 : _attributes$minimumUp2, - _attributes$media = attributes.media, - media = _attributes$media === void 0 ? '' : _attributes$media, - sourceDuration = attributes.sourceDuration, - _attributes$timescale2 = attributes.timescale, - timescale = _attributes$timescale2 === void 0 ? 1 : _attributes$timescale2, - _attributes$startNumb = attributes.startNumber, - startNumber = _attributes$startNumb === void 0 ? 1 : _attributes$startNumb, - timeline = attributes.periodIndex; - var segments = []; - var time = -1; - - for (var sIndex = 0; sIndex < segmentTimeline.length; sIndex++) { - var S = segmentTimeline[sIndex]; - var duration = S.d; - var repeat = S.r || 0; - var segmentTime = S.t || 0; - - if (time < 0) { - // first segment - time = segmentTime; - } - - if (segmentTime && segmentTime > time) { - // discontinuity - // TODO: How to handle this type of discontinuity - // timeline++ here would treat it like HLS discontuity and content would - // get appended without gap - // E.G. - // - // - // - // - // would have $Time$ values of [0, 1, 2, 5] - // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY) - // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP) - // does the value of sourceDuration consider this when calculating arbitrary - // negative @r repeat value? - // E.G. Same elements as above with this added at the end - // - // with a sourceDuration of 10 - // Would the 2 gaps be included in the time duration calculations resulting in - // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments - // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ? - time = segmentTime; - } - - var count = void 0; - - if (repeat < 0) { - var nextS = sIndex + 1; - - if (nextS === segmentTimeline.length) { - // last segment - if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) { - count = getLiveRValue(attributes, time, duration); - } else { - // TODO: This may be incorrect depending on conclusion of TODO above - count = (sourceDuration * timescale - time) / duration; - } - } else { - count = (segmentTimeline[nextS].t - time) / duration; - } - } else { - count = repeat + 1; - } - - var end = startNumber + segments.length + count; - var number = startNumber + segments.length; - - while (number < end) { - segments.push({ - number: number, - duration: duration / timescale, - time: time, - timeline: timeline - }); - time += duration; - number++; - } - } - - return segments; -}; - -var identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g; -/** - * Replaces template identifiers with corresponding values. To be used as the callback - * for String.prototype.replace - * - * @name replaceCallback - * @function - * @param {string} match - * Entire match of identifier - * @param {string} identifier - * Name of matched identifier - * @param {string} format - * Format tag string. Its presence indicates that padding is expected - * @param {string} width - * Desired length of the replaced value. Values less than this width shall be left - * zero padded - * @return {string} - * Replacement for the matched identifier - */ - -/** - * Returns a function to be used as a callback for String.prototype.replace to replace - * template identifiers - * - * @param {Obect} values - * Object containing values that shall be used to replace known identifiers - * @param {number} values.RepresentationID - * Value of the Representation@id attribute - * @param {number} values.Number - * Number of the corresponding segment - * @param {number} values.Bandwidth - * Value of the Representation@bandwidth attribute. - * @param {number} values.Time - * Timestamp value of the corresponding segment - * @return {replaceCallback} - * Callback to be used with String.prototype.replace to replace identifiers - */ - -var identifierReplacement = function identifierReplacement(values) { - return function (match, identifier, format, width) { - if (match === '$$') { - // escape sequence - return '$'; - } - - if (typeof values[identifier] === 'undefined') { - return match; - } - - var value = '' + values[identifier]; - - if (identifier === 'RepresentationID') { - // Format tag shall not be present with RepresentationID - return value; - } - - if (!format) { - width = 1; - } else { - width = parseInt(width, 10); - } - - if (value.length >= width) { - return value; - } - - return "" + new Array(width - value.length + 1).join('0') + value; - }; -}; -/** - * Constructs a segment url from a template string - * - * @param {string} url - * Template string to construct url from - * @param {Obect} values - * Object containing values that shall be used to replace known identifiers - * @param {number} values.RepresentationID - * Value of the Representation@id attribute - * @param {number} values.Number - * Number of the corresponding segment - * @param {number} values.Bandwidth - * Value of the Representation@bandwidth attribute. - * @param {number} values.Time - * Timestamp value of the corresponding segment - * @return {string} - * Segment url with identifiers replaced - */ - -var constructTemplateUrl = function constructTemplateUrl(url, values) { - return url.replace(identifierPattern, identifierReplacement(values)); -}; -/** - * Generates a list of objects containing timing and duration information about each - * segment needed to generate segment uris and the complete segment object - * - * @param {Object} attributes - * Object containing all inherited attributes from parent elements with attribute - * names as keys - * @param {Object[]|undefined} segmentTimeline - * List of objects representing the attributes of each S element contained within - * the SegmentTimeline element - * @return {{number: number, duration: number, time: number, timeline: number}[]} - * List of Objects with segment timing and duration info - */ - -var parseTemplateInfo = function parseTemplateInfo(attributes, segmentTimeline) { - if (!attributes.duration && !segmentTimeline) { - // if neither @duration or SegmentTimeline are present, then there shall be exactly - // one media segment - return [{ - number: attributes.startNumber || 1, - duration: attributes.sourceDuration, - time: 0, - timeline: attributes.periodIndex - }]; - } - - if (attributes.duration) { - return parseByDuration(attributes); - } - - return parseByTimeline(attributes, segmentTimeline); -}; -/** - * Generates a list of segments using information provided by the SegmentTemplate element - * - * @param {Object} attributes - * Object containing all inherited attributes from parent elements with attribute - * names as keys - * @param {Object[]|undefined} segmentTimeline - * List of objects representing the attributes of each S element contained within - * the SegmentTimeline element - * @return {Object[]} - * List of segment objects - */ - -var segmentsFromTemplate = function segmentsFromTemplate(attributes, segmentTimeline) { - var templateValues = { - RepresentationID: attributes.id, - Bandwidth: attributes.bandwidth || 0 - }; - var _attributes$initializ = attributes.initialization, - initialization = _attributes$initializ === void 0 ? { - sourceURL: '', - range: '' - } : _attributes$initializ; - var mapSegment = urlTypeToSegment({ - baseUrl: attributes.baseUrl, - source: constructTemplateUrl(initialization.sourceURL, templateValues), - range: initialization.range - }); - var segments = parseTemplateInfo(attributes, segmentTimeline); - return segments.map(function (segment) { - templateValues.Number = segment.number; - templateValues.Time = segment.time; - var uri = constructTemplateUrl(attributes.media || '', templateValues); - return { - uri: uri, - timeline: segment.timeline, - duration: segment.duration, - resolvedUri: resolveUrl_1(attributes.baseUrl || '', uri), - map: mapSegment, - number: segment.number - }; - }); -}; - -/** - * Converts a (of type URLType from the DASH spec 5.3.9.2 Table 14) - * to an object that matches the output of a segment in videojs/mpd-parser - * - * @param {Object} attributes - * Object containing all inherited attributes from parent elements with attribute - * names as keys - * @param {Object} segmentUrl - * node to translate into a segment object - * @return {Object} translated segment object - */ - -var SegmentURLToSegmentObject = function SegmentURLToSegmentObject(attributes, segmentUrl) { - var baseUrl = attributes.baseUrl, - _attributes$initializ = attributes.initialization, - initialization = _attributes$initializ === void 0 ? {} : _attributes$initializ; - var initSegment = urlTypeToSegment({ - baseUrl: baseUrl, - source: initialization.sourceURL, - range: initialization.range - }); - var segment = urlTypeToSegment({ - baseUrl: baseUrl, - source: segmentUrl.media, - range: segmentUrl.mediaRange - }); - segment.map = initSegment; - return segment; -}; -/** - * Generates a list of segments using information provided by the SegmentList element - * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of nodes. Each - * node should be translated into segment. - * - * @param {Object} attributes - * Object containing all inherited attributes from parent elements with attribute - * names as keys - * @param {Object[]|undefined} segmentTimeline - * List of objects representing the attributes of each S element contained within - * the SegmentTimeline element - * @return {Object.} list of segments - */ - - -var segmentsFromList = function segmentsFromList(attributes, segmentTimeline) { - var duration = attributes.duration, - _attributes$segmentUr = attributes.segmentUrls, - segmentUrls = _attributes$segmentUr === void 0 ? [] : _attributes$segmentUr; // Per spec (5.3.9.2.1) no way to determine segment duration OR - // if both SegmentTimeline and @duration are defined, it is outside of spec. - - if (!duration && !segmentTimeline || duration && segmentTimeline) { - throw new Error(errors.SEGMENT_TIME_UNSPECIFIED); - } - - var segmentUrlMap = segmentUrls.map(function (segmentUrlObject) { - return SegmentURLToSegmentObject(attributes, segmentUrlObject); - }); - var segmentTimeInfo; - - if (duration) { - segmentTimeInfo = parseByDuration(attributes); - } - - if (segmentTimeline) { - segmentTimeInfo = parseByTimeline(attributes, segmentTimeline); - } - - var segments = segmentTimeInfo.map(function (segmentTime, index) { - if (segmentUrlMap[index]) { - var segment = segmentUrlMap[index]; - segment.timeline = segmentTime.timeline; - segment.duration = segmentTime.duration; - segment.number = segmentTime.number; - return segment; - } // Since we're mapping we should get rid of any blank segments (in case - // the given SegmentTimeline is handling for more elements than we have - // SegmentURLs for). - - }).filter(function (segment) { - return segment; - }); - return segments; -}; - -var generateSegments = function generateSegments(_ref) { - var attributes = _ref.attributes, - segmentInfo = _ref.segmentInfo; - var segmentAttributes; - var segmentsFn; - - if (segmentInfo.template) { - segmentsFn = segmentsFromTemplate; - segmentAttributes = merge(attributes, segmentInfo.template); - } else if (segmentInfo.base) { - segmentsFn = segmentsFromBase; - segmentAttributes = merge(attributes, segmentInfo.base); - } else if (segmentInfo.list) { - segmentsFn = segmentsFromList; - segmentAttributes = merge(attributes, segmentInfo.list); - } - - var segmentsInfo = { - attributes: attributes - }; - - if (!segmentsFn) { - return segmentsInfo; - } - - var segments = segmentsFn(segmentAttributes, segmentInfo.timeline); // The @duration attribute will be used to determin the playlist's targetDuration which - // must be in seconds. Since we've generated the segment list, we no longer need - // @duration to be in @timescale units, so we can convert it here. - - if (segmentAttributes.duration) { - var _segmentAttributes = segmentAttributes, - duration = _segmentAttributes.duration, - _segmentAttributes$ti = _segmentAttributes.timescale, - timescale = _segmentAttributes$ti === void 0 ? 1 : _segmentAttributes$ti; - segmentAttributes.duration = duration / timescale; - } else if (segments.length) { - // if there is no @duration attribute, use the largest segment duration as - // as target duration - segmentAttributes.duration = segments.reduce(function (max, segment) { - return Math.max(max, Math.ceil(segment.duration)); - }, 0); - } else { - segmentAttributes.duration = 0; - } - - segmentsInfo.attributes = segmentAttributes; - segmentsInfo.segments = segments; // This is a sidx box without actual segment information - - if (segmentInfo.base && segmentAttributes.indexRange) { - segmentsInfo.sidx = segments[0]; - segmentsInfo.segments = []; - } - - return segmentsInfo; -}; -var toPlaylists = function toPlaylists(representations) { - return representations.map(generateSegments); -}; - -var findChildren = function findChildren(element, name) { - return from(element.childNodes).filter(function (_ref) { - var tagName = _ref.tagName; - return tagName === name; - }); -}; -var getContent = function getContent(element) { - return element.textContent.trim(); -}; - -var parseDuration = function parseDuration(str) { - var SECONDS_IN_YEAR = 365 * 24 * 60 * 60; - var SECONDS_IN_MONTH = 30 * 24 * 60 * 60; - var SECONDS_IN_DAY = 24 * 60 * 60; - var SECONDS_IN_HOUR = 60 * 60; - var SECONDS_IN_MIN = 60; // P10Y10M10DT10H10M10.1S - - var durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/; - var match = durationRegex.exec(str); - - if (!match) { - return 0; - } - - var _match$slice = match.slice(1), - year = _match$slice[0], - month = _match$slice[1], - day = _match$slice[2], - hour = _match$slice[3], - minute = _match$slice[4], - second = _match$slice[5]; - - return parseFloat(year || 0) * SECONDS_IN_YEAR + parseFloat(month || 0) * SECONDS_IN_MONTH + parseFloat(day || 0) * SECONDS_IN_DAY + parseFloat(hour || 0) * SECONDS_IN_HOUR + parseFloat(minute || 0) * SECONDS_IN_MIN + parseFloat(second || 0); -}; -var parseDate = function parseDate(str) { - // Date format without timezone according to ISO 8601 - // YYY-MM-DDThh:mm:ss.ssssss - var dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/; // If the date string does not specifiy a timezone, we must specifiy UTC. This is - // expressed by ending with 'Z' - - if (dateRegex.test(str)) { - str += 'Z'; - } - - return Date.parse(str); -}; - -var parsers = { - /** - * Specifies the duration of the entire Media Presentation. Format is a duration string - * as specified in ISO 8601 - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The duration in seconds - */ - mediaPresentationDuration: function mediaPresentationDuration(value) { - return parseDuration(value); - }, - - /** - * Specifies the Segment availability start time for all Segments referred to in this - * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability - * time. Format is a date string as specified in ISO 8601 - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The date as seconds from unix epoch - */ - availabilityStartTime: function availabilityStartTime(value) { - return parseDate(value) / 1000; - }, - - /** - * Specifies the smallest period between potential changes to the MPD. Format is a - * duration string as specified in ISO 8601 - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The duration in seconds - */ - minimumUpdatePeriod: function minimumUpdatePeriod(value) { - return parseDuration(value); - }, - - /** - * Specifies the suggested presentation delay. Format is a - * duration string as specified in ISO 8601 - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The duration in seconds - */ - suggestedPresentationDelay: function suggestedPresentationDelay(value) { - return parseDuration(value); - }, - - /** - * specifices the type of mpd. Can be either "static" or "dynamic" - * - * @param {string} value - * value of attribute as a string - * - * @return {string} - * The type as a string - */ - type: function type(value) { - return value; - }, - - /** - * Specifies the duration of the smallest time shifting buffer for any Representation - * in the MPD. Format is a duration string as specified in ISO 8601 - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The duration in seconds - */ - timeShiftBufferDepth: function timeShiftBufferDepth(value) { - return parseDuration(value); - }, - - /** - * Specifies the PeriodStart time of the Period relative to the availabilityStarttime. - * Format is a duration string as specified in ISO 8601 - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The duration in seconds - */ - start: function start(value) { - return parseDuration(value); - }, - - /** - * Specifies the width of the visual presentation - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The parsed width - */ - width: function width(value) { - return parseInt(value, 10); - }, - - /** - * Specifies the height of the visual presentation - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The parsed height - */ - height: function height(value) { - return parseInt(value, 10); - }, - - /** - * Specifies the bitrate of the representation - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The parsed bandwidth - */ - bandwidth: function bandwidth(value) { - return parseInt(value, 10); - }, - - /** - * Specifies the number of the first Media Segment in this Representation in the Period - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The parsed number - */ - startNumber: function startNumber(value) { - return parseInt(value, 10); - }, - - /** - * Specifies the timescale in units per seconds - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The aprsed timescale - */ - timescale: function timescale(value) { - return parseInt(value, 10); - }, - - /** - * Specifies the constant approximate Segment duration - * NOTE: The element also contains an @duration attribute. This duration - * specifies the duration of the Period. This attribute is currently not - * supported by the rest of the parser, however we still check for it to prevent - * errors. - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The parsed duration - */ - duration: function duration(value) { - var parsedValue = parseInt(value, 10); - - if (isNaN(parsedValue)) { - return parseDuration(value); - } - - return parsedValue; - }, - - /** - * Specifies the Segment duration, in units of the value of the @timescale. - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The parsed duration - */ - d: function d(value) { - return parseInt(value, 10); - }, - - /** - * Specifies the MPD start time, in @timescale units, the first Segment in the series - * starts relative to the beginning of the Period - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The parsed time - */ - t: function t(value) { - return parseInt(value, 10); - }, - - /** - * Specifies the repeat count of the number of following contiguous Segments with the - * same duration expressed by the value of @d - * - * @param {string} value - * value of attribute as a string - * @return {number} - * The parsed number - */ - r: function r(value) { - return parseInt(value, 10); - }, - - /** - * Default parser for all other attributes. Acts as a no-op and just returns the value - * as a string - * - * @param {string} value - * value of attribute as a string - * @return {string} - * Unparsed value - */ - DEFAULT: function DEFAULT(value) { - return value; - } -}; -/** - * Gets all the attributes and values of the provided node, parses attributes with known - * types, and returns an object with attribute names mapped to values. - * - * @param {Node} el - * The node to parse attributes from - * @return {Object} - * Object with all attributes of el parsed - */ - -var parseAttributes$1 = function parseAttributes(el) { - if (!(el && el.attributes)) { - return {}; - } - - return from(el.attributes).reduce(function (a, e) { - var parseFn = parsers[e.name] || parsers.DEFAULT; - a[e.name] = parseFn(e.value); - return a; - }, {}); -}; - -var keySystemsMap = { - 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey', - 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha', - 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready', - 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime' -}; -/** - * Builds a list of urls that is the product of the reference urls and BaseURL values - * - * @param {string[]} referenceUrls - * List of reference urls to resolve to - * @param {Node[]} baseUrlElements - * List of BaseURL nodes from the mpd - * @return {string[]} - * List of resolved urls - */ - -var buildBaseUrls = function buildBaseUrls(referenceUrls, baseUrlElements) { - if (!baseUrlElements.length) { - return referenceUrls; - } - - return flatten(referenceUrls.map(function (reference) { - return baseUrlElements.map(function (baseUrlElement) { - return resolveUrl_1(reference, getContent(baseUrlElement)); - }); - })); -}; -/** - * Contains all Segment information for its containing AdaptationSet - * - * @typedef {Object} SegmentInformation - * @property {Object|undefined} template - * Contains the attributes for the SegmentTemplate node - * @property {Object[]|undefined} timeline - * Contains a list of atrributes for each S node within the SegmentTimeline node - * @property {Object|undefined} list - * Contains the attributes for the SegmentList node - * @property {Object|undefined} base - * Contains the attributes for the SegmentBase node - */ - -/** - * Returns all available Segment information contained within the AdaptationSet node - * - * @param {Node} adaptationSet - * The AdaptationSet node to get Segment information from - * @return {SegmentInformation} - * The Segment information contained within the provided AdaptationSet - */ - -var getSegmentInformation = function getSegmentInformation(adaptationSet) { - var segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0]; - var segmentList = findChildren(adaptationSet, 'SegmentList')[0]; - var segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(function (s) { - return merge({ - tag: 'SegmentURL' - }, parseAttributes$1(s)); - }); - var segmentBase = findChildren(adaptationSet, 'SegmentBase')[0]; - var segmentTimelineParentNode = segmentList || segmentTemplate; - var segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0]; - var segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate; - var segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; // SegmentTemplate is handled slightly differently, since it can have both - // @initialization and an node. @initialization can be templated, - // while the node can have a url and range specified. If the has - // both @initialization and an subelement we opt to override with - // the node, as this interaction is not defined in the spec. - - var template = segmentTemplate && parseAttributes$1(segmentTemplate); - - if (template && segmentInitialization) { - template.initialization = segmentInitialization && parseAttributes$1(segmentInitialization); - } else if (template && template.initialization) { - // If it is @initialization we convert it to an object since this is the format that - // later functions will rely on for the initialization segment. This is only valid - // for - template.initialization = { - sourceURL: template.initialization - }; - } - - var segmentInfo = { - template: template, - timeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(function (s) { - return parseAttributes$1(s); - }), - list: segmentList && merge(parseAttributes$1(segmentList), { - segmentUrls: segmentUrls, - initialization: parseAttributes$1(segmentInitialization) - }), - base: segmentBase && merge(parseAttributes$1(segmentBase), { - initialization: parseAttributes$1(segmentInitialization) - }) - }; - Object.keys(segmentInfo).forEach(function (key) { - if (!segmentInfo[key]) { - delete segmentInfo[key]; - } - }); - return segmentInfo; -}; -/** - * Contains Segment information and attributes needed to construct a Playlist object - * from a Representation - * - * @typedef {Object} RepresentationInformation - * @property {SegmentInformation} segmentInfo - * Segment information for this Representation - * @property {Object} attributes - * Inherited attributes for this Representation - */ - -/** - * Maps a Representation node to an object containing Segment information and attributes - * - * @name inheritBaseUrlsCallback - * @function - * @param {Node} representation - * Representation node from the mpd - * @return {RepresentationInformation} - * Representation information needed to construct a Playlist object - */ - -/** - * Returns a callback for Array.prototype.map for mapping Representation nodes to - * Segment information and attributes using inherited BaseURL nodes. - * - * @param {Object} adaptationSetAttributes - * Contains attributes inherited by the AdaptationSet - * @param {string[]} adaptationSetBaseUrls - * Contains list of resolved base urls inherited by the AdaptationSet - * @param {SegmentInformation} adaptationSetSegmentInfo - * Contains Segment information for the AdaptationSet - * @return {inheritBaseUrlsCallback} - * Callback map function - */ - -var inheritBaseUrls = function inheritBaseUrls(adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) { - return function (representation) { - var repBaseUrlElements = findChildren(representation, 'BaseURL'); - var repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements); - var attributes = merge(adaptationSetAttributes, parseAttributes$1(representation)); - var representationSegmentInfo = getSegmentInformation(representation); - return repBaseUrls.map(function (baseUrl) { - return { - segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo), - attributes: merge(attributes, { - baseUrl: baseUrl - }) - }; - }); - }; -}; -/** - * Tranforms a series of content protection nodes to - * an object containing pssh data by key system - * - * @param {Node[]} contentProtectionNodes - * Content protection nodes - * @return {Object} - * Object containing pssh data by key system - */ - -var generateKeySystemInformation = function generateKeySystemInformation(contentProtectionNodes) { - return contentProtectionNodes.reduce(function (acc, node) { - var attributes = parseAttributes$1(node); - var keySystem = keySystemsMap[attributes.schemeIdUri]; - - if (keySystem) { - acc[keySystem] = { - attributes: attributes - }; - var psshNode = findChildren(node, 'cenc:pssh')[0]; - - if (psshNode) { - var pssh = getContent(psshNode); - var psshBuffer = pssh && decodeB64ToUint8Array_1(pssh); - acc[keySystem].pssh = psshBuffer; - } - } - - return acc; - }, {}); -}; -/** - * Maps an AdaptationSet node to a list of Representation information objects - * - * @name toRepresentationsCallback - * @function - * @param {Node} adaptationSet - * AdaptationSet node from the mpd - * @return {RepresentationInformation[]} - * List of objects containing Representaion information - */ - -/** - * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of - * Representation information objects - * - * @param {Object} periodAttributes - * Contains attributes inherited by the Period - * @param {string[]} periodBaseUrls - * Contains list of resolved base urls inherited by the Period - * @param {string[]} periodSegmentInfo - * Contains Segment Information at the period level - * @return {toRepresentationsCallback} - * Callback map function - */ - - -var toRepresentations = function toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo) { - return function (adaptationSet) { - var adaptationSetAttributes = parseAttributes$1(adaptationSet); - var adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL')); - var role = findChildren(adaptationSet, 'Role')[0]; - var roleAttributes = { - role: parseAttributes$1(role) - }; - var attrs = merge(periodAttributes, adaptationSetAttributes, roleAttributes); - var contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection')); - - if (Object.keys(contentProtection).length) { - attrs = merge(attrs, { - contentProtection: contentProtection - }); - } - - var segmentInfo = getSegmentInformation(adaptationSet); - var representations = findChildren(adaptationSet, 'Representation'); - var adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo); - return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo))); - }; -}; -/** - * Maps an Period node to a list of Representation inforamtion objects for all - * AdaptationSet nodes contained within the Period - * - * @name toAdaptationSetsCallback - * @function - * @param {Node} period - * Period node from the mpd - * @param {number} periodIndex - * Index of the Period within the mpd - * @return {RepresentationInformation[]} - * List of objects containing Representaion information - */ - -/** - * Returns a callback for Array.prototype.map for mapping Period nodes to a list of - * Representation information objects - * - * @param {Object} mpdAttributes - * Contains attributes inherited by the mpd - * @param {string[]} mpdBaseUrls - * Contains list of resolved base urls inherited by the mpd - * @return {toAdaptationSetsCallback} - * Callback map function - */ - -var toAdaptationSets = function toAdaptationSets(mpdAttributes, mpdBaseUrls) { - return function (period, index) { - var periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period, 'BaseURL')); - var periodAtt = parseAttributes$1(period); - var parsedPeriodId = parseInt(periodAtt.id, 10); // fallback to mapping index if Period@id is not a number - - var periodIndex = window_1$1.isNaN(parsedPeriodId) ? index : parsedPeriodId; - var periodAttributes = merge(mpdAttributes, { - periodIndex: periodIndex - }); - var adaptationSets = findChildren(period, 'AdaptationSet'); - var periodSegmentInfo = getSegmentInformation(period); - return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo))); - }; -}; -/** - * Traverses the mpd xml tree to generate a list of Representation information objects - * that have inherited attributes from parent nodes - * - * @param {Node} mpd - * The root node of the mpd - * @param {Object} options - * Available options for inheritAttributes - * @param {string} options.manifestUri - * The uri source of the mpd - * @param {number} options.NOW - * Current time per DASH IOP. Default is current time in ms since epoch - * @param {number} options.clientOffset - * Client time difference from NOW (in milliseconds) - * @return {RepresentationInformation[]} - * List of objects containing Representation information - */ - -var inheritAttributes = function inheritAttributes(mpd, options) { - if (options === void 0) { - options = {}; - } - - var _options = options, - _options$manifestUri = _options.manifestUri, - manifestUri = _options$manifestUri === void 0 ? '' : _options$manifestUri, - _options$NOW = _options.NOW, - NOW = _options$NOW === void 0 ? Date.now() : _options$NOW, - _options$clientOffset = _options.clientOffset, - clientOffset = _options$clientOffset === void 0 ? 0 : _options$clientOffset; - var periods = findChildren(mpd, 'Period'); - - if (!periods.length) { - throw new Error(errors.INVALID_NUMBER_OF_PERIOD); - } - - var mpdAttributes = parseAttributes$1(mpd); - var mpdBaseUrls = buildBaseUrls([manifestUri], findChildren(mpd, 'BaseURL')); - mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0; - mpdAttributes.NOW = NOW; - mpdAttributes.clientOffset = clientOffset; - return flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))); -}; - -var stringToMpdXml = function stringToMpdXml(manifestString) { - if (manifestString === '') { - throw new Error(errors.DASH_EMPTY_MANIFEST); - } - - var parser = new domParser.DOMParser(); - var xml = parser.parseFromString(manifestString, 'application/xml'); - var mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null; - - if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) { - throw new Error(errors.DASH_INVALID_XML); - } - - return mpd; -}; - -/** - * Parses the manifest for a UTCTiming node, returning the nodes attributes if found - * - * @param {string} mpd - * XML string of the MPD manifest - * @return {Object|null} - * Attributes of UTCTiming node specified in the manifest. Null if none found - */ - -var parseUTCTimingScheme = function parseUTCTimingScheme(mpd) { - var UTCTimingNode = findChildren(mpd, 'UTCTiming')[0]; - - if (!UTCTimingNode) { - return null; - } - - var attributes = parseAttributes$1(UTCTimingNode); - - switch (attributes.schemeIdUri) { - case 'urn:mpeg:dash:utc:http-head:2014': - case 'urn:mpeg:dash:utc:http-head:2012': - attributes.method = 'HEAD'; - break; - - case 'urn:mpeg:dash:utc:http-xsdate:2014': - case 'urn:mpeg:dash:utc:http-iso:2014': - case 'urn:mpeg:dash:utc:http-xsdate:2012': - case 'urn:mpeg:dash:utc:http-iso:2012': - attributes.method = 'GET'; - break; - - case 'urn:mpeg:dash:utc:direct:2014': - case 'urn:mpeg:dash:utc:direct:2012': - attributes.method = 'DIRECT'; - attributes.value = Date.parse(attributes.value); - break; - - case 'urn:mpeg:dash:utc:http-ntp:2014': - case 'urn:mpeg:dash:utc:ntp:2014': - case 'urn:mpeg:dash:utc:sntp:2014': - default: - throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME); - } - - return attributes; -}; - -var parse$1 = function parse(manifestString, options) { - if (options === void 0) { - options = {}; - } - - return toM3u8(toPlaylists(inheritAttributes(stringToMpdXml(manifestString), options)), options.sidxMapping); -}; -/** - * Parses the manifest for a UTCTiming node, returning the nodes attributes if found - * - * @param {string} manifestString - * XML string of the MPD manifest - * @return {Object|null} - * Attributes of UTCTiming node specified in the manifest. Null if none found - */ - - -var parseUTCTiming = function parseUTCTiming(manifestString) { - return parseUTCTimingScheme(stringToMpdXml(manifestString)); -}; - -/** - * mux.js - * - * Copyright (c) Brightcove - * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE - */ -var toUnsigned = function(value) { - return value >>> 0; -}; - -var toHexString = function(value) { - return ('00' + value.toString(16)).slice(-2); -}; - -var bin = { - toUnsigned: toUnsigned, - toHexString: toHexString -}; - -var - inspectMp4, - textifyMp4, - toUnsigned$1 = bin.toUnsigned, - parseMp4Date = function(seconds) { - return new Date(seconds * 1000 - 2082844800000); - }, - parseSampleFlags = function(flags) { - return { - isLeading: (flags[0] & 0x0c) >>> 2, - dependsOn: flags[0] & 0x03, - isDependedOn: (flags[1] & 0xc0) >>> 6, - hasRedundancy: (flags[1] & 0x30) >>> 4, - paddingValue: (flags[1] & 0x0e) >>> 1, - isNonSyncSample: flags[1] & 0x01, - degradationPriority: (flags[2] << 8) | flags[3] - }; - }, - /** - * Returns the string representation of an ASCII encoded four byte buffer. - * @param buffer {Uint8Array} a four-byte buffer to translate - * @return {string} the corresponding string - */ - parseType = function(buffer) { - var result = ''; - result += String.fromCharCode(buffer[0]); - result += String.fromCharCode(buffer[1]); - result += String.fromCharCode(buffer[2]); - result += String.fromCharCode(buffer[3]); - return result; - }, - // Find the data for a box specified by its path - findBox = function(data, path) { - var results = [], - i, size, type, end, subresults; - - if (!path.length) { - // short-circuit the search for empty paths - return null; - } - - for (i = 0; i < data.byteLength;) { - size = toUnsigned$1(data[i] << 24 | - data[i + 1] << 16 | - data[i + 2] << 8 | - data[i + 3]); - - type = parseType(data.subarray(i + 4, i + 8)); - - end = size > 1 ? i + size : data.byteLength; - - if (type === path[0]) { - if (path.length === 1) { - // this is the end of the path and we've found the box we were - // looking for - results.push(data.subarray(i + 8, end)); - } else { - // recursively search for the next box along the path - subresults = findBox(data.subarray(i + 8, end), path.slice(1)); - if (subresults.length) { - results = results.concat(subresults); - } - } - } - i = end; - } - - // we've finished searching all of data - return results; - }, - nalParse = function(avcStream) { - var - avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength), - result = [], - i, - length; - for (i = 0; i + 4 < avcStream.length; i += length) { - length = avcView.getUint32(i); - i += 4; - - // bail if this doesn't appear to be an H264 stream - if (length <= 0) { - result.push('MALFORMED DATA'); - continue; - } - - switch (avcStream[i] & 0x1F) { - case 0x01: - result.push('slice_layer_without_partitioning_rbsp'); - break; - case 0x05: - result.push('slice_layer_without_partitioning_rbsp_idr'); - break; - case 0x06: - result.push('sei_rbsp'); - break; - case 0x07: - result.push('seq_parameter_set_rbsp'); - break; - case 0x08: - result.push('pic_parameter_set_rbsp'); - break; - case 0x09: - result.push('access_unit_delimiter_rbsp'); - break; - default: - result.push('UNKNOWN NAL - ' + avcStream[i] & 0x1F); - break; - } - } - return result; - }, - - // registry of handlers for individual mp4 box types - parse$2 = { - // codingname, not a first-class box type. stsd entries share the - // same format as real boxes so the parsing infrastructure can be - // shared - avc1: function(data) { - var view = new DataView(data.buffer, data.byteOffset, data.byteLength); - return { - dataReferenceIndex: view.getUint16(6), - width: view.getUint16(24), - height: view.getUint16(26), - horizresolution: view.getUint16(28) + (view.getUint16(30) / 16), - vertresolution: view.getUint16(32) + (view.getUint16(34) / 16), - frameCount: view.getUint16(40), - depth: view.getUint16(74), - config: inspectMp4(data.subarray(78, data.byteLength)) - }; - }, - avcC: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - result = { - configurationVersion: data[0], - avcProfileIndication: data[1], - profileCompatibility: data[2], - avcLevelIndication: data[3], - lengthSizeMinusOne: data[4] & 0x03, - sps: [], - pps: [] - }, - numOfSequenceParameterSets = data[5] & 0x1f, - numOfPictureParameterSets, - nalSize, - offset, - i; - - // iterate past any SPSs - offset = 6; - for (i = 0; i < numOfSequenceParameterSets; i++) { - nalSize = view.getUint16(offset); - offset += 2; - result.sps.push(new Uint8Array(data.subarray(offset, offset + nalSize))); - offset += nalSize; - } - // iterate past any PPSs - numOfPictureParameterSets = data[offset]; - offset++; - for (i = 0; i < numOfPictureParameterSets; i++) { - nalSize = view.getUint16(offset); - offset += 2; - result.pps.push(new Uint8Array(data.subarray(offset, offset + nalSize))); - offset += nalSize; - } - return result; - }, - btrt: function(data) { - var view = new DataView(data.buffer, data.byteOffset, data.byteLength); - return { - bufferSizeDB: view.getUint32(0), - maxBitrate: view.getUint32(4), - avgBitrate: view.getUint32(8) - }; - }, - esds: function(data) { - return { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - esId: (data[6] << 8) | data[7], - streamPriority: data[8] & 0x1f, - decoderConfig: { - objectProfileIndication: data[11], - streamType: (data[12] >>> 2) & 0x3f, - bufferSize: (data[13] << 16) | (data[14] << 8) | data[15], - maxBitrate: (data[16] << 24) | - (data[17] << 16) | - (data[18] << 8) | - data[19], - avgBitrate: (data[20] << 24) | - (data[21] << 16) | - (data[22] << 8) | - data[23], - decoderConfigDescriptor: { - tag: data[24], - length: data[25], - audioObjectType: (data[26] >>> 3) & 0x1f, - samplingFrequencyIndex: ((data[26] & 0x07) << 1) | - ((data[27] >>> 7) & 0x01), - channelConfiguration: (data[27] >>> 3) & 0x0f - } - } - }; - }, - ftyp: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - result = { - majorBrand: parseType(data.subarray(0, 4)), - minorVersion: view.getUint32(4), - compatibleBrands: [] - }, - i = 8; - while (i < data.byteLength) { - result.compatibleBrands.push(parseType(data.subarray(i, i + 4))); - i += 4; - } - return result; - }, - dinf: function(data) { - return { - boxes: inspectMp4(data) - }; - }, - dref: function(data) { - return { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - dataReferences: inspectMp4(data.subarray(8)) - }; - }, - hdlr: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - result = { - version: view.getUint8(0), - flags: new Uint8Array(data.subarray(1, 4)), - handlerType: parseType(data.subarray(8, 12)), - name: '' - }, - i = 8; - - // parse out the name field - for (i = 24; i < data.byteLength; i++) { - if (data[i] === 0x00) { - // the name field is null-terminated - i++; - break; - } - result.name += String.fromCharCode(data[i]); - } - // decode UTF-8 to javascript's internal representation - // see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html - result.name = decodeURIComponent(escape(result.name)); - - return result; - }, - mdat: function(data) { - return { - byteLength: data.byteLength, - nals: nalParse(data) - }; - }, - mdhd: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - i = 4, - language, - result = { - version: view.getUint8(0), - flags: new Uint8Array(data.subarray(1, 4)), - language: '' - }; - if (result.version === 1) { - i += 4; - result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes - i += 8; - result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes - i += 4; - result.timescale = view.getUint32(i); - i += 8; - result.duration = view.getUint32(i); // truncating top 4 bytes - } else { - result.creationTime = parseMp4Date(view.getUint32(i)); - i += 4; - result.modificationTime = parseMp4Date(view.getUint32(i)); - i += 4; - result.timescale = view.getUint32(i); - i += 4; - result.duration = view.getUint32(i); - } - i += 4; - // language is stored as an ISO-639-2/T code in an array of three 5-bit fields - // each field is the packed difference between its ASCII value and 0x60 - language = view.getUint16(i); - result.language += String.fromCharCode((language >> 10) + 0x60); - result.language += String.fromCharCode(((language & 0x03e0) >> 5) + 0x60); - result.language += String.fromCharCode((language & 0x1f) + 0x60); - - return result; - }, - mdia: function(data) { - return { - boxes: inspectMp4(data) - }; - }, - mfhd: function(data) { - return { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - sequenceNumber: (data[4] << 24) | - (data[5] << 16) | - (data[6] << 8) | - (data[7]) - }; - }, - minf: function(data) { - return { - boxes: inspectMp4(data) - }; - }, - // codingname, not a first-class box type. stsd entries share the - // same format as real boxes so the parsing infrastructure can be - // shared - mp4a: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - result = { - // 6 bytes reserved - dataReferenceIndex: view.getUint16(6), - // 4 + 4 bytes reserved - channelcount: view.getUint16(16), - samplesize: view.getUint16(18), - // 2 bytes pre_defined - // 2 bytes reserved - samplerate: view.getUint16(24) + (view.getUint16(26) / 65536) - }; - - // if there are more bytes to process, assume this is an ISO/IEC - // 14496-14 MP4AudioSampleEntry and parse the ESDBox - if (data.byteLength > 28) { - result.streamDescriptor = inspectMp4(data.subarray(28))[0]; - } - return result; - }, - moof: function(data) { - return { - boxes: inspectMp4(data) - }; - }, - moov: function(data) { - return { - boxes: inspectMp4(data) - }; - }, - mvex: function(data) { - return { - boxes: inspectMp4(data) - }; - }, - mvhd: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - i = 4, - result = { - version: view.getUint8(0), - flags: new Uint8Array(data.subarray(1, 4)) - }; - - if (result.version === 1) { - i += 4; - result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes - i += 8; - result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes - i += 4; - result.timescale = view.getUint32(i); - i += 8; - result.duration = view.getUint32(i); // truncating top 4 bytes - } else { - result.creationTime = parseMp4Date(view.getUint32(i)); - i += 4; - result.modificationTime = parseMp4Date(view.getUint32(i)); - i += 4; - result.timescale = view.getUint32(i); - i += 4; - result.duration = view.getUint32(i); - } - i += 4; - - // convert fixed-point, base 16 back to a number - result.rate = view.getUint16(i) + (view.getUint16(i + 2) / 16); - i += 4; - result.volume = view.getUint8(i) + (view.getUint8(i + 1) / 8); - i += 2; - i += 2; - i += 2 * 4; - result.matrix = new Uint32Array(data.subarray(i, i + (9 * 4))); - i += 9 * 4; - i += 6 * 4; - result.nextTrackId = view.getUint32(i); - return result; - }, - pdin: function(data) { - var view = new DataView(data.buffer, data.byteOffset, data.byteLength); - return { - version: view.getUint8(0), - flags: new Uint8Array(data.subarray(1, 4)), - rate: view.getUint32(4), - initialDelay: view.getUint32(8) - }; - }, - sdtp: function(data) { - var - result = { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - samples: [] - }, i; - - for (i = 4; i < data.byteLength; i++) { - result.samples.push({ - dependsOn: (data[i] & 0x30) >> 4, - isDependedOn: (data[i] & 0x0c) >> 2, - hasRedundancy: data[i] & 0x03 - }); - } - return result; - }, - sidx: function(data) { - var view = new DataView(data.buffer, data.byteOffset, data.byteLength), - result = { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - references: [], - referenceId: view.getUint32(4), - timescale: view.getUint32(8), - earliestPresentationTime: view.getUint32(12), - firstOffset: view.getUint32(16) - }, - referenceCount = view.getUint16(22), - i; - - for (i = 24; referenceCount; i += 12, referenceCount--) { - result.references.push({ - referenceType: (data[i] & 0x80) >>> 7, - referencedSize: view.getUint32(i) & 0x7FFFFFFF, - subsegmentDuration: view.getUint32(i + 4), - startsWithSap: !!(data[i + 8] & 0x80), - sapType: (data[i + 8] & 0x70) >>> 4, - sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF - }); - } - - return result; - }, - smhd: function(data) { - return { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - balance: data[4] + (data[5] / 256) - }; - }, - stbl: function(data) { - return { - boxes: inspectMp4(data) - }; - }, - stco: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - result = { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - chunkOffsets: [] - }, - entryCount = view.getUint32(4), - i; - for (i = 8; entryCount; i += 4, entryCount--) { - result.chunkOffsets.push(view.getUint32(i)); - } - return result; - }, - stsc: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - entryCount = view.getUint32(4), - result = { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - sampleToChunks: [] - }, - i; - for (i = 8; entryCount; i += 12, entryCount--) { - result.sampleToChunks.push({ - firstChunk: view.getUint32(i), - samplesPerChunk: view.getUint32(i + 4), - sampleDescriptionIndex: view.getUint32(i + 8) - }); - } - return result; - }, - stsd: function(data) { - return { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - sampleDescriptions: inspectMp4(data.subarray(8)) - }; - }, - stsz: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - result = { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - sampleSize: view.getUint32(4), - entries: [] - }, - i; - for (i = 12; i < data.byteLength; i += 4) { - result.entries.push(view.getUint32(i)); - } - return result; - }, - stts: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - result = { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - timeToSamples: [] - }, - entryCount = view.getUint32(4), - i; - - for (i = 8; entryCount; i += 8, entryCount--) { - result.timeToSamples.push({ - sampleCount: view.getUint32(i), - sampleDelta: view.getUint32(i + 4) - }); - } - return result; - }, - styp: function(data) { - return parse$2.ftyp(data); - }, - tfdt: function(data) { - var result = { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - baseMediaDecodeTime: toUnsigned$1(data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]) - }; - if (result.version === 1) { - result.baseMediaDecodeTime *= Math.pow(2, 32); - result.baseMediaDecodeTime += toUnsigned$1(data[8] << 24 | data[9] << 16 | data[10] << 8 | data[11]); - } - return result; - }, - tfhd: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - result = { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - trackId: view.getUint32(4) - }, - baseDataOffsetPresent = result.flags[2] & 0x01, - sampleDescriptionIndexPresent = result.flags[2] & 0x02, - defaultSampleDurationPresent = result.flags[2] & 0x08, - defaultSampleSizePresent = result.flags[2] & 0x10, - defaultSampleFlagsPresent = result.flags[2] & 0x20, - durationIsEmpty = result.flags[0] & 0x010000, - defaultBaseIsMoof = result.flags[0] & 0x020000, - i; - - i = 8; - if (baseDataOffsetPresent) { - i += 4; // truncate top 4 bytes - // FIXME: should we read the full 64 bits? - result.baseDataOffset = view.getUint32(12); - i += 4; - } - if (sampleDescriptionIndexPresent) { - result.sampleDescriptionIndex = view.getUint32(i); - i += 4; - } - if (defaultSampleDurationPresent) { - result.defaultSampleDuration = view.getUint32(i); - i += 4; - } - if (defaultSampleSizePresent) { - result.defaultSampleSize = view.getUint32(i); - i += 4; - } - if (defaultSampleFlagsPresent) { - result.defaultSampleFlags = view.getUint32(i); - } - if (durationIsEmpty) { - result.durationIsEmpty = true; - } - if (!baseDataOffsetPresent && defaultBaseIsMoof) { - result.baseDataOffsetIsMoof = true; - } - return result; - }, - tkhd: function(data) { - var - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - i = 4, - result = { - version: view.getUint8(0), - flags: new Uint8Array(data.subarray(1, 4)) - }; - if (result.version === 1) { - i += 4; - result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes - i += 8; - result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes - i += 4; - result.trackId = view.getUint32(i); - i += 4; - i += 8; - result.duration = view.getUint32(i); // truncating top 4 bytes - } else { - result.creationTime = parseMp4Date(view.getUint32(i)); - i += 4; - result.modificationTime = parseMp4Date(view.getUint32(i)); - i += 4; - result.trackId = view.getUint32(i); - i += 4; - i += 4; - result.duration = view.getUint32(i); - } - i += 4; - i += 2 * 4; - result.layer = view.getUint16(i); - i += 2; - result.alternateGroup = view.getUint16(i); - i += 2; - // convert fixed-point, base 16 back to a number - result.volume = view.getUint8(i) + (view.getUint8(i + 1) / 8); - i += 2; - i += 2; - result.matrix = new Uint32Array(data.subarray(i, i + (9 * 4))); - i += 9 * 4; - result.width = view.getUint16(i) + (view.getUint16(i + 2) / 65536); - i += 4; - result.height = view.getUint16(i) + (view.getUint16(i + 2) / 65536); - return result; - }, - traf: function(data) { - return { - boxes: inspectMp4(data) - }; - }, - trak: function(data) { - return { - boxes: inspectMp4(data) - }; - }, - trex: function(data) { - var view = new DataView(data.buffer, data.byteOffset, data.byteLength); - return { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - trackId: view.getUint32(4), - defaultSampleDescriptionIndex: view.getUint32(8), - defaultSampleDuration: view.getUint32(12), - defaultSampleSize: view.getUint32(16), - sampleDependsOn: data[20] & 0x03, - sampleIsDependedOn: (data[21] & 0xc0) >> 6, - sampleHasRedundancy: (data[21] & 0x30) >> 4, - samplePaddingValue: (data[21] & 0x0e) >> 1, - sampleIsDifferenceSample: !!(data[21] & 0x01), - sampleDegradationPriority: view.getUint16(22) - }; - }, - trun: function(data) { - var - result = { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - samples: [] - }, - view = new DataView(data.buffer, data.byteOffset, data.byteLength), - // Flag interpretation - dataOffsetPresent = result.flags[2] & 0x01, // compare with 2nd byte of 0x1 - firstSampleFlagsPresent = result.flags[2] & 0x04, // compare with 2nd byte of 0x4 - sampleDurationPresent = result.flags[1] & 0x01, // compare with 2nd byte of 0x100 - sampleSizePresent = result.flags[1] & 0x02, // compare with 2nd byte of 0x200 - sampleFlagsPresent = result.flags[1] & 0x04, // compare with 2nd byte of 0x400 - sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08, // compare with 2nd byte of 0x800 - sampleCount = view.getUint32(4), - offset = 8, - sample; - - if (dataOffsetPresent) { - // 32 bit signed integer - result.dataOffset = view.getInt32(offset); - offset += 4; - } - - // Overrides the flags for the first sample only. The order of - // optional values will be: duration, size, compositionTimeOffset - if (firstSampleFlagsPresent && sampleCount) { - sample = { - flags: parseSampleFlags(data.subarray(offset, offset + 4)) - }; - offset += 4; - if (sampleDurationPresent) { - sample.duration = view.getUint32(offset); - offset += 4; - } - if (sampleSizePresent) { - sample.size = view.getUint32(offset); - offset += 4; - } - if (sampleCompositionTimeOffsetPresent) { - // Note: this should be a signed int if version is 1 - sample.compositionTimeOffset = view.getUint32(offset); - offset += 4; - } - result.samples.push(sample); - sampleCount--; - } - - while (sampleCount--) { - sample = {}; - if (sampleDurationPresent) { - sample.duration = view.getUint32(offset); - offset += 4; - } - if (sampleSizePresent) { - sample.size = view.getUint32(offset); - offset += 4; - } - if (sampleFlagsPresent) { - sample.flags = parseSampleFlags(data.subarray(offset, offset + 4)); - offset += 4; - } - if (sampleCompositionTimeOffsetPresent) { - // Note: this should be a signed int if version is 1 - sample.compositionTimeOffset = view.getUint32(offset); - offset += 4; - } - result.samples.push(sample); - } - return result; - }, - 'url ': function(data) { - return { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)) - }; - }, - vmhd: function(data) { - var view = new DataView(data.buffer, data.byteOffset, data.byteLength); - return { - version: data[0], - flags: new Uint8Array(data.subarray(1, 4)), - graphicsmode: view.getUint16(4), - opcolor: new Uint16Array([view.getUint16(6), - view.getUint16(8), - view.getUint16(10)]) - }; - } - }; - - -/** - * Return a javascript array of box objects parsed from an ISO base - * media file. - * @param data {Uint8Array} the binary data of the media to be inspected - * @return {array} a javascript array of potentially nested box objects - */ -inspectMp4 = function(data) { - var - i = 0, - result = [], - view, - size, - type, - end, - box; - - // Convert data from Uint8Array to ArrayBuffer, to follow Dataview API - var ab = new ArrayBuffer(data.length); - var v = new Uint8Array(ab); - for (var z = 0; z < data.length; ++z) { - v[z] = data[z]; - } - view = new DataView(ab); - - while (i < data.byteLength) { - // parse box data - size = view.getUint32(i); - type = parseType(data.subarray(i + 4, i + 8)); - end = size > 1 ? i + size : data.byteLength; - - // parse type-specific data - box = (parse$2[type] || function(data) { - return { - data: data - }; - })(data.subarray(i + 8, end)); - box.size = size; - box.type = type; - - // store this box and move to the next - result.push(box); - i = end; - } - return result; -}; - -/** - * Returns a textual representation of the javascript represtentation - * of an MP4 file. You can use it as an alternative to - * JSON.stringify() to compare inspected MP4s. - * @param inspectedMp4 {array} the parsed array of boxes in an MP4 - * file - * @param depth {number} (optional) the number of ancestor boxes of - * the elements of inspectedMp4. Assumed to be zero if unspecified. - * @return {string} a text representation of the parsed MP4 - */ -textifyMp4 = function(inspectedMp4, depth) { - var indent; - depth = depth || 0; - indent = new Array(depth * 2 + 1).join(' '); - - // iterate over all the boxes - return inspectedMp4.map(function(box, index) { - - // list the box type first at the current indentation level - return indent + box.type + '\n' + - - // the type is already included and handle child boxes separately - Object.keys(box).filter(function(key) { - return key !== 'type' && key !== 'boxes'; - - // output all the box properties - }).map(function(key) { - var prefix = indent + ' ' + key + ': ', - value = box[key]; - - // print out raw bytes as hexademical - if (value instanceof Uint8Array || value instanceof Uint32Array) { - var bytes = Array.prototype.slice.call(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)) - .map(function(byte) { - return ' ' + ('00' + byte.toString(16)).slice(-2); - }).join('').match(/.{1,24}/g); - if (!bytes) { - return prefix + '<>'; - } - if (bytes.length === 1) { - return prefix + '<' + bytes.join('').slice(1) + '>'; - } - return prefix + '<\n' + bytes.map(function(line) { - return indent + ' ' + line; - }).join('\n') + '\n' + indent + ' >'; - } - - // stringify generic objects - return prefix + - JSON.stringify(value, null, 2) - .split('\n').map(function(line, index) { - if (index === 0) { - return line; - } - return indent + ' ' + line; - }).join('\n'); - }).join('\n') + - - // recursively textify the child boxes - (box.boxes ? '\n' + textifyMp4(box.boxes, depth + 1) : ''); - }).join('\n'); -}; - -var mp4Inspector = { - inspect: inspectMp4, - textify: textifyMp4, - parseType: parseType, - findBox: findBox, - parseTraf: parse$2.traf, - parseTfdt: parse$2.tfdt, - parseHdlr: parse$2.hdlr, - parseTfhd: parse$2.tfhd, - parseTrun: parse$2.trun, - parseSidx: parse$2.sidx -}; - -var toUnsigned$2 = bin.toUnsigned; -var toHexString$1 = bin.toHexString; - -var timescale, startTime, compositionStartTime, getVideoTrackIds, getTracks; - -/** - * Parses an MP4 initialization segment and extracts the timescale - * values for any declared tracks. Timescale values indicate the - * number of clock ticks per second to assume for time-based values - * elsewhere in the MP4. - * - * To determine the start time of an MP4, you need two pieces of - * information: the timescale unit and the earliest base media decode - * time. Multiple timescales can be specified within an MP4 but the - * base media decode time is always expressed in the timescale from - * the media header box for the track: - * ``` - * moov > trak > mdia > mdhd.timescale - * ``` - * @param init {Uint8Array} the bytes of the init segment - * @return {object} a hash of track ids to timescale values or null if - * the init segment is malformed. - */ -timescale = function(init) { - var - result = {}, - traks = mp4Inspector.findBox(init, ['moov', 'trak']); - - // mdhd timescale - return traks.reduce(function(result, trak) { - var tkhd, version, index, id, mdhd; - - tkhd = mp4Inspector.findBox(trak, ['tkhd'])[0]; - if (!tkhd) { - return null; - } - version = tkhd[0]; - index = version === 0 ? 12 : 20; - id = toUnsigned$2(tkhd[index] << 24 | - tkhd[index + 1] << 16 | - tkhd[index + 2] << 8 | - tkhd[index + 3]); - - mdhd = mp4Inspector.findBox(trak, ['mdia', 'mdhd'])[0]; - if (!mdhd) { - return null; - } - version = mdhd[0]; - index = version === 0 ? 12 : 20; - result[id] = toUnsigned$2(mdhd[index] << 24 | - mdhd[index + 1] << 16 | - mdhd[index + 2] << 8 | - mdhd[index + 3]); - return result; - }, result); -}; - -/** - * Determine the base media decode start time, in seconds, for an MP4 - * fragment. If multiple fragments are specified, the earliest time is - * returned. - * - * The base media decode time can be parsed from track fragment - * metadata: - * ``` - * moof > traf > tfdt.baseMediaDecodeTime - * ``` - * It requires the timescale value from the mdhd to interpret. - * - * @param timescale {object} a hash of track ids to timescale values. - * @return {number} the earliest base media decode start time for the - * fragment, in seconds - */ -startTime = function(timescale, fragment) { - var trafs, baseTimes, result; - - // we need info from two childrend of each track fragment box - trafs = mp4Inspector.findBox(fragment, ['moof', 'traf']); - - // determine the start times for each track - baseTimes = [].concat.apply([], trafs.map(function(traf) { - return mp4Inspector.findBox(traf, ['tfhd']).map(function(tfhd) { - var id, scale, baseTime; - - // get the track id from the tfhd - id = toUnsigned$2(tfhd[4] << 24 | - tfhd[5] << 16 | - tfhd[6] << 8 | - tfhd[7]); - // assume a 90kHz clock if no timescale was specified - scale = timescale[id] || 90e3; - - // get the base media decode time from the tfdt - baseTime = mp4Inspector.findBox(traf, ['tfdt']).map(function(tfdt) { - var version, result; - - version = tfdt[0]; - result = toUnsigned$2(tfdt[4] << 24 | - tfdt[5] << 16 | - tfdt[6] << 8 | - tfdt[7]); - if (version === 1) { - result *= Math.pow(2, 32); - result += toUnsigned$2(tfdt[8] << 24 | - tfdt[9] << 16 | - tfdt[10] << 8 | - tfdt[11]); - } - return result; - })[0]; - baseTime = baseTime || Infinity; - - // convert base time to seconds - return baseTime / scale; - }); - })); - - // return the minimum - result = Math.min.apply(null, baseTimes); - return isFinite(result) ? result : 0; -}; - -/** - * Determine the composition start, in seconds, for an MP4 - * fragment. - * - * The composition start time of a fragment can be calculated using the base - * media decode time, composition time offset, and timescale, as follows: - * - * compositionStartTime = (baseMediaDecodeTime + compositionTimeOffset) / timescale - * - * All of the aforementioned information is contained within a media fragment's - * `traf` box, except for timescale info, which comes from the initialization - * segment, so a track id (also contained within a `traf`) is also necessary to - * associate it with a timescale - * - * - * @param timescales {object} - a hash of track ids to timescale values. - * @param fragment {Unit8Array} - the bytes of a media segment - * @return {number} the composition start time for the fragment, in seconds - **/ -compositionStartTime = function(timescales, fragment) { - var trafBoxes = mp4Inspector.findBox(fragment, ['moof', 'traf']); - var baseMediaDecodeTime = 0; - var compositionTimeOffset = 0; - var trackId; - - if (trafBoxes && trafBoxes.length) { - // The spec states that track run samples contained within a `traf` box are contiguous, but - // it does not explicitly state whether the `traf` boxes themselves are contiguous. - // We will assume that they are, so we only need the first to calculate start time. - var parsedTraf = mp4Inspector.parseTraf(trafBoxes[0]); - - for (var i = 0; i < parsedTraf.boxes.length; i++) { - if (parsedTraf.boxes[i].type === 'tfhd') { - trackId = parsedTraf.boxes[i].trackId; - } else if (parsedTraf.boxes[i].type === 'tfdt') { - baseMediaDecodeTime = parsedTraf.boxes[i].baseMediaDecodeTime; - } else if (parsedTraf.boxes[i].type === 'trun' && parsedTraf.boxes[i].samples.length) { - compositionTimeOffset = parsedTraf.boxes[i].samples[0].compositionTimeOffset || 0; - } - } - } - - // Get timescale for this specific track. Assume a 90kHz clock if no timescale was - // specified. - var timescale = timescales[trackId] || 90e3; - - // return the composition start time, in seconds - return (baseMediaDecodeTime + compositionTimeOffset) / timescale; -}; - -/** - * Find the trackIds of the video tracks in this source. - * Found by parsing the Handler Reference and Track Header Boxes: - * moov > trak > mdia > hdlr - * moov > trak > tkhd - * - * @param {Uint8Array} init - The bytes of the init segment for this source - * @return {Number[]} A list of trackIds - * - * @see ISO-BMFF-12/2015, Section 8.4.3 - **/ -getVideoTrackIds = function(init) { - var traks = mp4Inspector.findBox(init, ['moov', 'trak']); - var videoTrackIds = []; - - traks.forEach(function(trak) { - var hdlrs = mp4Inspector.findBox(trak, ['mdia', 'hdlr']); - var tkhds = mp4Inspector.findBox(trak, ['tkhd']); - - hdlrs.forEach(function(hdlr, index) { - var handlerType = mp4Inspector.parseType(hdlr.subarray(8, 12)); - var tkhd = tkhds[index]; - var view; - var version; - var trackId; - - if (handlerType === 'vide') { - view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); - version = view.getUint8(0); - trackId = (version === 0) ? view.getUint32(12) : view.getUint32(20); - - videoTrackIds.push(trackId); - } - }); - }); - - return videoTrackIds; -}; - -/** - * Get all the video, audio, and hint tracks from a non fragmented - * mp4 segment - */ -getTracks = function(init) { - var traks = mp4Inspector.findBox(init, ['moov', 'trak']); - var tracks = []; - - traks.forEach(function(trak) { - var track = {}; - var tkhd = mp4Inspector.findBox(trak, ['tkhd'])[0]; - var view, version; - - // id - if (tkhd) { - view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); - version = view.getUint8(0); - - track.id = (version === 0) ? view.getUint32(12) : view.getUint32(20); - } - - var hdlr = mp4Inspector.findBox(trak, ['mdia', 'hdlr'])[0]; - - // type - if (hdlr) { - var type = mp4Inspector.parseType(hdlr.subarray(8, 12)); - - if (type === 'vide') { - track.type = 'video'; - } else if (type === 'soun') { - track.type = 'audio'; - } else { - track.type = type; - } - } - - - // codec - var stsd = mp4Inspector.findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; - - if (stsd) { - var sampleDescriptions = stsd.subarray(8); - // gives the codec type string - track.codec = mp4Inspector.parseType(sampleDescriptions.subarray(4, 8)); - - var codecBox = mp4Inspector.findBox(sampleDescriptions, [track.codec])[0]; - var codecConfig, codecConfigType; - - if (codecBox) { - // https://tools.ietf.org/html/rfc6381#section-3.3 - if ((/^[a-z]vc[1-9]$/i).test(track.codec)) { - // we don't need anything but the "config" parameter of the - // avc1 codecBox - codecConfig = codecBox.subarray(78); - codecConfigType = mp4Inspector.parseType(codecConfig.subarray(4, 8)); - - if (codecConfigType === 'avcC' && codecConfig.length > 11) { - track.codec += '.'; - - // left padded with zeroes for single digit hex - // profile idc - track.codec += toHexString$1(codecConfig[9]); - // the byte containing the constraint_set flags - track.codec += toHexString$1(codecConfig[10]); - // level idc - track.codec += toHexString$1(codecConfig[11]); - } else { - // TODO: show a warning that we couldn't parse the codec - // and are using the default - track.codec = 'avc1.4d400d'; - } - } else if ((/^mp4[a,v]$/i).test(track.codec)) { - // we do not need anything but the streamDescriptor of the mp4a codecBox - codecConfig = codecBox.subarray(28); - codecConfigType = mp4Inspector.parseType(codecConfig.subarray(4, 8)); - - if (codecConfigType === 'esds' && codecConfig.length > 20 && codecConfig[19] !== 0) { - track.codec += '.' + toHexString$1(codecConfig[19]); - // this value is only a single digit - track.codec += '.' + toHexString$1((codecConfig[20] >>> 2) & 0x3f).replace(/^0/, ''); - } else { - // TODO: show a warning that we couldn't parse the codec - // and are using the default - track.codec = 'mp4a.40.2'; - } - } else ; - } - } - - var mdhd = mp4Inspector.findBox(trak, ['mdia', 'mdhd'])[0]; - - if (mdhd && tkhd) { - var index = version === 0 ? 12 : 20; - - track.timescale = toUnsigned$2(mdhd[index] << 24 | - mdhd[index + 1] << 16 | - mdhd[index + 2] << 8 | - mdhd[index + 3]); - } - - tracks.push(track); - }); - - return tracks; -}; - -var probe = { - // export mp4 inspector's findBox and parseType for backwards compatibility - findBox: mp4Inspector.findBox, - parseType: mp4Inspector.parseType, - timescale: timescale, - startTime: startTime, - compositionStartTime: compositionStartTime, - videoTrackIds: getVideoTrackIds, - tracks: getTracks -}; - -/** - * mux.js - * - * Copyright (c) Brightcove - * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE - * - * Reads in-band caption information from a video elementary - * stream. Captions must follow the CEA-708 standard for injection - * into an MPEG-2 transport streams. - * @see https://en.wikipedia.org/wiki/CEA-708 - * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf - */ - -// Supplemental enhancement information (SEI) NAL units have a -// payload type field to indicate how they are to be -// interpreted. CEAS-708 caption content is always transmitted with -// payload type 0x04. -var USER_DATA_REGISTERED_ITU_T_T35 = 4, - RBSP_TRAILING_BITS = 128; - -/** - * Parse a supplemental enhancement information (SEI) NAL unit. - * Stops parsing once a message of type ITU T T35 has been found. - * - * @param bytes {Uint8Array} the bytes of a SEI NAL unit - * @return {object} the parsed SEI payload - * @see Rec. ITU-T H.264, 7.3.2.3.1 - */ -var parseSei = function(bytes) { - var - i = 0, - result = { - payloadType: -1, - payloadSize: 0 - }, - payloadType = 0, - payloadSize = 0; - - // go through the sei_rbsp parsing each each individual sei_message - while (i < bytes.byteLength) { - // stop once we have hit the end of the sei_rbsp - if (bytes[i] === RBSP_TRAILING_BITS) { - break; - } - - // Parse payload type - while (bytes[i] === 0xFF) { - payloadType += 255; - i++; - } - payloadType += bytes[i++]; - - // Parse payload size - while (bytes[i] === 0xFF) { - payloadSize += 255; - i++; - } - payloadSize += bytes[i++]; - - // this sei_message is a 608/708 caption so save it and break - // there can only ever be one caption message in a frame's sei - if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) { - var userIdentifier = String.fromCharCode( - bytes[i + 3], - bytes[i + 4], - bytes[i + 5], - bytes[i + 6]); - - if (userIdentifier === 'GA94') { - result.payloadType = payloadType; - result.payloadSize = payloadSize; - result.payload = bytes.subarray(i, i + payloadSize); - break; - } else { - result.payload = void 0; - } - } - - // skip the payload and parse the next message - i += payloadSize; - payloadType = 0; - payloadSize = 0; - } - - return result; -}; - -// see ANSI/SCTE 128-1 (2013), section 8.1 -var parseUserData = function(sei) { - // itu_t_t35_contry_code must be 181 (United States) for - // captions - if (sei.payload[0] !== 181) { - return null; - } - - // itu_t_t35_provider_code should be 49 (ATSC) for captions - if (((sei.payload[1] << 8) | sei.payload[2]) !== 49) { - return null; - } - - // the user_identifier should be "GA94" to indicate ATSC1 data - if (String.fromCharCode(sei.payload[3], - sei.payload[4], - sei.payload[5], - sei.payload[6]) !== 'GA94') { - return null; - } - - // finally, user_data_type_code should be 0x03 for caption data - if (sei.payload[7] !== 0x03) { - return null; - } - - // return the user_data_type_structure and strip the trailing - // marker bits - return sei.payload.subarray(8, sei.payload.length - 1); -}; - -// see CEA-708-D, section 4.4 -var parseCaptionPackets = function(pts, userData) { - var results = [], i, count, offset, data; - - // if this is just filler, return immediately - if (!(userData[0] & 0x40)) { - return results; - } - - // parse out the cc_data_1 and cc_data_2 fields - count = userData[0] & 0x1f; - for (i = 0; i < count; i++) { - offset = i * 3; - data = { - type: userData[offset + 2] & 0x03, - pts: pts - }; - - // capture cc data when cc_valid is 1 - if (userData[offset + 2] & 0x04) { - data.ccData = (userData[offset + 3] << 8) | userData[offset + 4]; - results.push(data); - } - } - return results; -}; - -var discardEmulationPreventionBytes = function(data) { - var - length = data.byteLength, - emulationPreventionBytesPositions = [], - i = 1, - newLength, newData; - - // Find all `Emulation Prevention Bytes` - while (i < length - 2) { - if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { - emulationPreventionBytesPositions.push(i + 2); - i += 2; - } else { - i++; - } - } - - // If no Emulation Prevention Bytes were found just return the original - // array - if (emulationPreventionBytesPositions.length === 0) { - return data; - } - - // Create a new array to hold the NAL unit data - newLength = length - emulationPreventionBytesPositions.length; - newData = new Uint8Array(newLength); - var sourceIndex = 0; - - for (i = 0; i < newLength; sourceIndex++, i++) { - if (sourceIndex === emulationPreventionBytesPositions[0]) { - // Skip this byte - sourceIndex++; - // Remove this position index - emulationPreventionBytesPositions.shift(); - } - newData[i] = data[sourceIndex]; - } - - return newData; -}; - -// exports -var captionPacketParser = { - parseSei: parseSei, - parseUserData: parseUserData, - parseCaptionPackets: parseCaptionPackets, - discardEmulationPreventionBytes: discardEmulationPreventionBytes, - USER_DATA_REGISTERED_ITU_T_T35: USER_DATA_REGISTERED_ITU_T_T35 -}; - -/** - * mux.js - * - * Copyright (c) Brightcove - * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE - * - * A lightweight readable stream implemention that handles event dispatching. - * Objects that inherit from streams should call init in their constructors. - */ - -var Stream$1 = function() { - this.init = function() { - var listeners = {}; - /** - * Add a listener for a specified event type. - * @param type {string} the event name - * @param listener {function} the callback to be invoked when an event of - * the specified type occurs - */ - this.on = function(type, listener) { - if (!listeners[type]) { - listeners[type] = []; - } - listeners[type] = listeners[type].concat(listener); - }; - /** - * Remove a listener for a specified event type. - * @param type {string} the event name - * @param listener {function} a function previously registered for this - * type of event through `on` - */ - this.off = function(type, listener) { - var index; - if (!listeners[type]) { - return false; - } - index = listeners[type].indexOf(listener); - listeners[type] = listeners[type].slice(); - listeners[type].splice(index, 1); - return index > -1; - }; - /** - * Trigger an event of the specified type on this stream. Any additional - * arguments to this function are passed as parameters to event listeners. - * @param type {string} the event name - */ - this.trigger = function(type) { - var callbacks, i, length, args; - callbacks = listeners[type]; - if (!callbacks) { - return; - } - // Slicing the arguments on every invocation of this method - // can add a significant amount of overhead. Avoid the - // intermediate object creation for the common case of a - // single callback argument - if (arguments.length === 2) { - length = callbacks.length; - for (i = 0; i < length; ++i) { - callbacks[i].call(this, arguments[1]); - } - } else { - args = []; - i = arguments.length; - for (i = 1; i < arguments.length; ++i) { - args.push(arguments[i]); - } - length = callbacks.length; - for (i = 0; i < length; ++i) { - callbacks[i].apply(this, args); - } - } - }; - /** - * Destroys the stream and cleans up. - */ - this.dispose = function() { - listeners = {}; - }; - }; -}; - -/** - * Forwards all `data` events on this stream to the destination stream. The - * destination stream should provide a method `push` to receive the data - * events as they arrive. - * @param destination {stream} the stream that will receive all `data` events - * @param autoFlush {boolean} if false, we will not call `flush` on the destination - * when the current stream emits a 'done' event - * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options - */ -Stream$1.prototype.pipe = function(destination) { - this.on('data', function(data) { - destination.push(data); - }); - - this.on('done', function(flushSource) { - destination.flush(flushSource); - }); - - this.on('partialdone', function(flushSource) { - destination.partialFlush(flushSource); - }); - - this.on('endedtimeline', function(flushSource) { - destination.endTimeline(flushSource); - }); - - this.on('reset', function(flushSource) { - destination.reset(flushSource); - }); - - return destination; -}; - -// Default stream functions that are expected to be overridden to perform -// actual work. These are provided by the prototype as a sort of no-op -// implementation so that we don't have to check for their existence in the -// `pipe` function above. -Stream$1.prototype.push = function(data) { - this.trigger('data', data); -}; - -Stream$1.prototype.flush = function(flushSource) { - this.trigger('done', flushSource); -}; - -Stream$1.prototype.partialFlush = function(flushSource) { - this.trigger('partialdone', flushSource); -}; - -Stream$1.prototype.endTimeline = function(flushSource) { - this.trigger('endedtimeline', flushSource); -}; - -Stream$1.prototype.reset = function(flushSource) { - this.trigger('reset', flushSource); -}; - -var stream = Stream$1; - -// ----------------- -// Link To Transport -// ----------------- - - - - -var CaptionStream = function() { - - CaptionStream.prototype.init.call(this); - - this.captionPackets_ = []; - - this.ccStreams_ = [ - new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define - new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define - new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define - new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define - ]; - - this.reset(); - - // forward data and done events from CCs to this CaptionStream - this.ccStreams_.forEach(function(cc) { - cc.on('data', this.trigger.bind(this, 'data')); - cc.on('partialdone', this.trigger.bind(this, 'partialdone')); - cc.on('done', this.trigger.bind(this, 'done')); - }, this); - -}; - -CaptionStream.prototype = new stream(); -CaptionStream.prototype.push = function(event) { - var sei, userData, newCaptionPackets; - - // only examine SEI NALs - if (event.nalUnitType !== 'sei_rbsp') { - return; - } - - // parse the sei - sei = captionPacketParser.parseSei(event.escapedRBSP); - - // ignore everything but user_data_registered_itu_t_t35 - if (sei.payloadType !== captionPacketParser.USER_DATA_REGISTERED_ITU_T_T35) { - return; - } - - // parse out the user data payload - userData = captionPacketParser.parseUserData(sei); - - // ignore unrecognized userData - if (!userData) { - return; - } - - // Sometimes, the same segment # will be downloaded twice. To stop the - // caption data from being processed twice, we track the latest dts we've - // received and ignore everything with a dts before that. However, since - // data for a specific dts can be split across packets on either side of - // a segment boundary, we need to make sure we *don't* ignore the packets - // from the *next* segment that have dts === this.latestDts_. By constantly - // tracking the number of packets received with dts === this.latestDts_, we - // know how many should be ignored once we start receiving duplicates. - if (event.dts < this.latestDts_) { - // We've started getting older data, so set the flag. - this.ignoreNextEqualDts_ = true; - return; - } else if ((event.dts === this.latestDts_) && (this.ignoreNextEqualDts_)) { - this.numSameDts_--; - if (!this.numSameDts_) { - // We've received the last duplicate packet, time to start processing again - this.ignoreNextEqualDts_ = false; - } - return; - } - - // parse out CC data packets and save them for later - newCaptionPackets = captionPacketParser.parseCaptionPackets(event.pts, userData); - this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets); - if (this.latestDts_ !== event.dts) { - this.numSameDts_ = 0; - } - this.numSameDts_++; - this.latestDts_ = event.dts; -}; - -CaptionStream.prototype.flushCCStreams = function(flushType) { - this.ccStreams_.forEach(function(cc) { - return flushType === 'flush' ? cc.flush() : cc.partialFlush(); - }, this); -}; - -CaptionStream.prototype.flushStream = function(flushType) { - // make sure we actually parsed captions before proceeding - if (!this.captionPackets_.length) { - this.flushCCStreams(flushType); - return; - } - - // In Chrome, the Array#sort function is not stable so add a - // presortIndex that we can use to ensure we get a stable-sort - this.captionPackets_.forEach(function(elem, idx) { - elem.presortIndex = idx; - }); - - // sort caption byte-pairs based on their PTS values - this.captionPackets_.sort(function(a, b) { - if (a.pts === b.pts) { - return a.presortIndex - b.presortIndex; - } - return a.pts - b.pts; - }); - - this.captionPackets_.forEach(function(packet) { - if (packet.type < 2) { - // Dispatch packet to the right Cea608Stream - this.dispatchCea608Packet(packet); - } - // this is where an 'else' would go for a dispatching packets - // to a theoretical Cea708Stream that handles SERVICEn data - }, this); - - this.captionPackets_.length = 0; - this.flushCCStreams(flushType); -}; - -CaptionStream.prototype.flush = function() { - return this.flushStream('flush'); -}; - -// Only called if handling partial data -CaptionStream.prototype.partialFlush = function() { - return this.flushStream('partialFlush'); -}; - -CaptionStream.prototype.reset = function() { - this.latestDts_ = null; - this.ignoreNextEqualDts_ = false; - this.numSameDts_ = 0; - this.activeCea608Channel_ = [null, null]; - this.ccStreams_.forEach(function(ccStream) { - ccStream.reset(); - }); -}; - -// From the CEA-608 spec: -/* - * When XDS sub-packets are interleaved with other services, the end of each sub-packet shall be followed - * by a control pair to change to a different service. When any of the control codes from 0x10 to 0x1F is - * used to begin a control code pair, it indicates the return to captioning or Text data. The control code pair - * and subsequent data should then be processed according to the FCC rules. It may be necessary for the - * line 21 data encoder to automatically insert a control code pair (i.e. RCL, RU2, RU3, RU4, RDC, or RTD) - * to switch to captioning or Text. -*/ -// With that in mind, we ignore any data between an XDS control code and a -// subsequent closed-captioning control code. -CaptionStream.prototype.dispatchCea608Packet = function(packet) { - // NOTE: packet.type is the CEA608 field - if (this.setsTextOrXDSActive(packet)) { - this.activeCea608Channel_[packet.type] = null; - } else if (this.setsChannel1Active(packet)) { - this.activeCea608Channel_[packet.type] = 0; - } else if (this.setsChannel2Active(packet)) { - this.activeCea608Channel_[packet.type] = 1; - } - if (this.activeCea608Channel_[packet.type] === null) { - // If we haven't received anything to set the active channel, or the - // packets are Text/XDS data, discard the data; we don't want jumbled - // captions - return; - } - this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet); -}; - -CaptionStream.prototype.setsChannel1Active = function(packet) { - return ((packet.ccData & 0x7800) === 0x1000); -}; -CaptionStream.prototype.setsChannel2Active = function(packet) { - return ((packet.ccData & 0x7800) === 0x1800); -}; -CaptionStream.prototype.setsTextOrXDSActive = function(packet) { - return ((packet.ccData & 0x7100) === 0x0100) || - ((packet.ccData & 0x78fe) === 0x102a) || - ((packet.ccData & 0x78fe) === 0x182a); -}; - -// ---------------------- -// Session to Application -// ---------------------- - -// This hash maps non-ASCII, special, and extended character codes to their -// proper Unicode equivalent. The first keys that are only a single byte -// are the non-standard ASCII characters, which simply map the CEA608 byte -// to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608 -// character codes, but have their MSB bitmasked with 0x03 so that a lookup -// can be performed regardless of the field and data channel on which the -// character code was received. -var CHARACTER_TRANSLATION = { - 0x2a: 0xe1, // á - 0x5c: 0xe9, // é - 0x5e: 0xed, // í - 0x5f: 0xf3, // ó - 0x60: 0xfa, // ú - 0x7b: 0xe7, // ç - 0x7c: 0xf7, // ÷ - 0x7d: 0xd1, // Ñ - 0x7e: 0xf1, // ñ - 0x7f: 0x2588, // █ - 0x0130: 0xae, // ® - 0x0131: 0xb0, // ° - 0x0132: 0xbd, // ½ - 0x0133: 0xbf, // ¿ - 0x0134: 0x2122, // ™ - 0x0135: 0xa2, // ¢ - 0x0136: 0xa3, // £ - 0x0137: 0x266a, // ♪ - 0x0138: 0xe0, // à - 0x0139: 0xa0, // - 0x013a: 0xe8, // è - 0x013b: 0xe2, // â - 0x013c: 0xea, // ê - 0x013d: 0xee, // î - 0x013e: 0xf4, // ô - 0x013f: 0xfb, // û - 0x0220: 0xc1, // Á - 0x0221: 0xc9, // É - 0x0222: 0xd3, // Ó - 0x0223: 0xda, // Ú - 0x0224: 0xdc, // Ü - 0x0225: 0xfc, // ü - 0x0226: 0x2018, // ‘ - 0x0227: 0xa1, // ¡ - 0x0228: 0x2a, // * - 0x0229: 0x27, // ' - 0x022a: 0x2014, // — - 0x022b: 0xa9, // © - 0x022c: 0x2120, // ℠ - 0x022d: 0x2022, // • - 0x022e: 0x201c, // “ - 0x022f: 0x201d, // ” - 0x0230: 0xc0, // À - 0x0231: 0xc2, // Â - 0x0232: 0xc7, // Ç - 0x0233: 0xc8, // È - 0x0234: 0xca, // Ê - 0x0235: 0xcb, // Ë - 0x0236: 0xeb, // ë - 0x0237: 0xce, // Î - 0x0238: 0xcf, // Ï - 0x0239: 0xef, // ï - 0x023a: 0xd4, // Ô - 0x023b: 0xd9, // Ù - 0x023c: 0xf9, // ù - 0x023d: 0xdb, // Û - 0x023e: 0xab, // « - 0x023f: 0xbb, // » - 0x0320: 0xc3, // Ã - 0x0321: 0xe3, // ã - 0x0322: 0xcd, // Í - 0x0323: 0xcc, // Ì - 0x0324: 0xec, // ì - 0x0325: 0xd2, // Ò - 0x0326: 0xf2, // ò - 0x0327: 0xd5, // Õ - 0x0328: 0xf5, // õ - 0x0329: 0x7b, // { - 0x032a: 0x7d, // } - 0x032b: 0x5c, // \ - 0x032c: 0x5e, // ^ - 0x032d: 0x5f, // _ - 0x032e: 0x7c, // | - 0x032f: 0x7e, // ~ - 0x0330: 0xc4, // Ä - 0x0331: 0xe4, // ä - 0x0332: 0xd6, // Ö - 0x0333: 0xf6, // ö - 0x0334: 0xdf, // ß - 0x0335: 0xa5, // ¥ - 0x0336: 0xa4, // ¤ - 0x0337: 0x2502, // │ - 0x0338: 0xc5, // Å - 0x0339: 0xe5, // å - 0x033a: 0xd8, // Ø - 0x033b: 0xf8, // ø - 0x033c: 0x250c, // ┌ - 0x033d: 0x2510, // ┐ - 0x033e: 0x2514, // └ - 0x033f: 0x2518 // ┘ -}; - -var getCharFromCode = function(code) { - if (code === null) { - return ''; - } - code = CHARACTER_TRANSLATION[code] || code; - return String.fromCharCode(code); -}; - -// the index of the last row in a CEA-608 display buffer -var BOTTOM_ROW = 14; - -// This array is used for mapping PACs -> row #, since there's no way of -// getting it through bit logic. -var ROWS = [0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620, - 0x1700, 0x1720, 0x1000, 0x1300, 0x1320, 0x1400, 0x1420]; - -// CEA-608 captions are rendered onto a 34x15 matrix of character -// cells. The "bottom" row is the last element in the outer array. -var createDisplayBuffer = function() { - var result = [], i = BOTTOM_ROW + 1; - while (i--) { - result.push(''); - } - return result; -}; - -var Cea608Stream = function(field, dataChannel) { - Cea608Stream.prototype.init.call(this); - - this.field_ = field || 0; - this.dataChannel_ = dataChannel || 0; - - this.name_ = 'CC' + (((this.field_ << 1) | this.dataChannel_) + 1); - - this.setConstants(); - this.reset(); - - this.push = function(packet) { - var data, swap, char0, char1, text; - // remove the parity bits - data = packet.ccData & 0x7f7f; - - // ignore duplicate control codes; the spec demands they're sent twice - if (data === this.lastControlCode_) { - this.lastControlCode_ = null; - return; - } - - // Store control codes - if ((data & 0xf000) === 0x1000) { - this.lastControlCode_ = data; - } else if (data !== this.PADDING_) { - this.lastControlCode_ = null; - } - - char0 = data >>> 8; - char1 = data & 0xff; - - if (data === this.PADDING_) { - return; - - } else if (data === this.RESUME_CAPTION_LOADING_) { - this.mode_ = 'popOn'; - - } else if (data === this.END_OF_CAPTION_) { - // If an EOC is received while in paint-on mode, the displayed caption - // text should be swapped to non-displayed memory as if it was a pop-on - // caption. Because of that, we should explicitly switch back to pop-on - // mode - this.mode_ = 'popOn'; - this.clearFormatting(packet.pts); - // if a caption was being displayed, it's gone now - this.flushDisplayed(packet.pts); - - // flip memory - swap = this.displayed_; - this.displayed_ = this.nonDisplayed_; - this.nonDisplayed_ = swap; - - // start measuring the time to display the caption - this.startPts_ = packet.pts; - - } else if (data === this.ROLL_UP_2_ROWS_) { - this.rollUpRows_ = 2; - this.setRollUp(packet.pts); - } else if (data === this.ROLL_UP_3_ROWS_) { - this.rollUpRows_ = 3; - this.setRollUp(packet.pts); - } else if (data === this.ROLL_UP_4_ROWS_) { - this.rollUpRows_ = 4; - this.setRollUp(packet.pts); - } else if (data === this.CARRIAGE_RETURN_) { - this.clearFormatting(packet.pts); - this.flushDisplayed(packet.pts); - this.shiftRowsUp_(); - this.startPts_ = packet.pts; - - } else if (data === this.BACKSPACE_) { - if (this.mode_ === 'popOn') { - this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1); - } else { - this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1); - } - } else if (data === this.ERASE_DISPLAYED_MEMORY_) { - this.flushDisplayed(packet.pts); - this.displayed_ = createDisplayBuffer(); - } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) { - this.nonDisplayed_ = createDisplayBuffer(); - - } else if (data === this.RESUME_DIRECT_CAPTIONING_) { - if (this.mode_ !== 'paintOn') { - // NOTE: This should be removed when proper caption positioning is - // implemented - this.flushDisplayed(packet.pts); - this.displayed_ = createDisplayBuffer(); - } - this.mode_ = 'paintOn'; - this.startPts_ = packet.pts; - - // Append special characters to caption text - } else if (this.isSpecialCharacter(char0, char1)) { - // Bitmask char0 so that we can apply character transformations - // regardless of field and data channel. - // Then byte-shift to the left and OR with char1 so we can pass the - // entire character code to `getCharFromCode`. - char0 = (char0 & 0x03) << 8; - text = getCharFromCode(char0 | char1); - this[this.mode_](packet.pts, text); - this.column_++; - - // Append extended characters to caption text - } else if (this.isExtCharacter(char0, char1)) { - // Extended characters always follow their "non-extended" equivalents. - // IE if a "è" is desired, you'll always receive "eè"; non-compliant - // decoders are supposed to drop the "è", while compliant decoders - // backspace the "e" and insert "è". - - // Delete the previous character - if (this.mode_ === 'popOn') { - this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1); - } else { - this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1); - } - - // Bitmask char0 so that we can apply character transformations - // regardless of field and data channel. - // Then byte-shift to the left and OR with char1 so we can pass the - // entire character code to `getCharFromCode`. - char0 = (char0 & 0x03) << 8; - text = getCharFromCode(char0 | char1); - this[this.mode_](packet.pts, text); - this.column_++; - - // Process mid-row codes - } else if (this.isMidRowCode(char0, char1)) { - // Attributes are not additive, so clear all formatting - this.clearFormatting(packet.pts); - - // According to the standard, mid-row codes - // should be replaced with spaces, so add one now - this[this.mode_](packet.pts, ' '); - this.column_++; - - if ((char1 & 0xe) === 0xe) { - this.addFormatting(packet.pts, ['i']); - } - - if ((char1 & 0x1) === 0x1) { - this.addFormatting(packet.pts, ['u']); - } - - // Detect offset control codes and adjust cursor - } else if (this.isOffsetControlCode(char0, char1)) { - // Cursor position is set by indent PAC (see below) in 4-column - // increments, with an additional offset code of 1-3 to reach any - // of the 32 columns specified by CEA-608. So all we need to do - // here is increment the column cursor by the given offset. - this.column_ += (char1 & 0x03); - - // Detect PACs (Preamble Address Codes) - } else if (this.isPAC(char0, char1)) { - - // There's no logic for PAC -> row mapping, so we have to just - // find the row code in an array and use its index :( - var row = ROWS.indexOf(data & 0x1f20); - - // Configure the caption window if we're in roll-up mode - if (this.mode_ === 'rollUp') { - // This implies that the base row is incorrectly set. - // As per the recommendation in CEA-608(Base Row Implementation), defer to the number - // of roll-up rows set. - if (row - this.rollUpRows_ + 1 < 0) { - row = this.rollUpRows_ - 1; - } - - this.setRollUp(packet.pts, row); - } - - if (row !== this.row_) { - // formatting is only persistent for current row - this.clearFormatting(packet.pts); - this.row_ = row; - } - // All PACs can apply underline, so detect and apply - // (All odd-numbered second bytes set underline) - if ((char1 & 0x1) && (this.formatting_.indexOf('u') === -1)) { - this.addFormatting(packet.pts, ['u']); - } - - if ((data & 0x10) === 0x10) { - // We've got an indent level code. Each successive even number - // increments the column cursor by 4, so we can get the desired - // column position by bit-shifting to the right (to get n/2) - // and multiplying by 4. - this.column_ = ((data & 0xe) >> 1) * 4; - } - - if (this.isColorPAC(char1)) { - // it's a color code, though we only support white, which - // can be either normal or italicized. white italics can be - // either 0x4e or 0x6e depending on the row, so we just - // bitwise-and with 0xe to see if italics should be turned on - if ((char1 & 0xe) === 0xe) { - this.addFormatting(packet.pts, ['i']); - } - } - - // We have a normal character in char0, and possibly one in char1 - } else if (this.isNormalChar(char0)) { - if (char1 === 0x00) { - char1 = null; - } - text = getCharFromCode(char0); - text += getCharFromCode(char1); - this[this.mode_](packet.pts, text); - this.column_ += text.length; - - } // finish data processing - - }; -}; -Cea608Stream.prototype = new stream(); -// Trigger a cue point that captures the current state of the -// display buffer -Cea608Stream.prototype.flushDisplayed = function(pts) { - var content = this.displayed_ - // remove spaces from the start and end of the string - .map(function(row) { - try { - return row.trim(); - } catch (e) { - // Ordinarily, this shouldn't happen. However, caption - // parsing errors should not throw exceptions and - // break playback. - // eslint-disable-next-line no-console - console.error('Skipping malformed caption.'); - return ''; - } - }) - // combine all text rows to display in one cue - .join('\n') - // and remove blank rows from the start and end, but not the middle - .replace(/^\n+|\n+$/g, ''); - - if (content.length) { - this.trigger('data', { - startPts: this.startPts_, - endPts: pts, - text: content, - stream: this.name_ - }); - } -}; - -/** - * Zero out the data, used for startup and on seek - */ -Cea608Stream.prototype.reset = function() { - this.mode_ = 'popOn'; - // When in roll-up mode, the index of the last row that will - // actually display captions. If a caption is shifted to a row - // with a lower index than this, it is cleared from the display - // buffer - this.topRow_ = 0; - this.startPts_ = 0; - this.displayed_ = createDisplayBuffer(); - this.nonDisplayed_ = createDisplayBuffer(); - this.lastControlCode_ = null; - - // Track row and column for proper line-breaking and spacing - this.column_ = 0; - this.row_ = BOTTOM_ROW; - this.rollUpRows_ = 2; - - // This variable holds currently-applied formatting - this.formatting_ = []; -}; - -/** - * Sets up control code and related constants for this instance - */ -Cea608Stream.prototype.setConstants = function() { - // The following attributes have these uses: - // ext_ : char0 for mid-row codes, and the base for extended - // chars (ext_+0, ext_+1, and ext_+2 are char0s for - // extended codes) - // control_: char0 for control codes, except byte-shifted to the - // left so that we can do this.control_ | CONTROL_CODE - // offset_: char0 for tab offset codes - // - // It's also worth noting that control codes, and _only_ control codes, - // differ between field 1 and field2. Field 2 control codes are always - // their field 1 value plus 1. That's why there's the "| field" on the - // control value. - if (this.dataChannel_ === 0) { - this.BASE_ = 0x10; - this.EXT_ = 0x11; - this.CONTROL_ = (0x14 | this.field_) << 8; - this.OFFSET_ = 0x17; - } else if (this.dataChannel_ === 1) { - this.BASE_ = 0x18; - this.EXT_ = 0x19; - this.CONTROL_ = (0x1c | this.field_) << 8; - this.OFFSET_ = 0x1f; - } - - // Constants for the LSByte command codes recognized by Cea608Stream. This - // list is not exhaustive. For a more comprehensive listing and semantics see - // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf - // Padding - this.PADDING_ = 0x0000; - // Pop-on Mode - this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20; - this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; - // Roll-up Mode - this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25; - this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26; - this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27; - this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; - // paint-on mode - this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; - // Erasure - this.BACKSPACE_ = this.CONTROL_ | 0x21; - this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c; - this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e; -}; - -/** - * Detects if the 2-byte packet data is a special character - * - * Special characters have a second byte in the range 0x30 to 0x3f, - * with the first byte being 0x11 (for data channel 1) or 0x19 (for - * data channel 2). - * - * @param {Integer} char0 The first byte - * @param {Integer} char1 The second byte - * @return {Boolean} Whether the 2 bytes are an special character - */ -Cea608Stream.prototype.isSpecialCharacter = function(char0, char1) { - return (char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f); -}; - -/** - * Detects if the 2-byte packet data is an extended character - * - * Extended characters have a second byte in the range 0x20 to 0x3f, - * with the first byte being 0x12 or 0x13 (for data channel 1) or - * 0x1a or 0x1b (for data channel 2). - * - * @param {Integer} char0 The first byte - * @param {Integer} char1 The second byte - * @return {Boolean} Whether the 2 bytes are an extended character - */ -Cea608Stream.prototype.isExtCharacter = function(char0, char1) { - return ((char0 === (this.EXT_ + 1) || char0 === (this.EXT_ + 2)) && - (char1 >= 0x20 && char1 <= 0x3f)); -}; - -/** - * Detects if the 2-byte packet is a mid-row code - * - * Mid-row codes have a second byte in the range 0x20 to 0x2f, with - * the first byte being 0x11 (for data channel 1) or 0x19 (for data - * channel 2). - * - * @param {Integer} char0 The first byte - * @param {Integer} char1 The second byte - * @return {Boolean} Whether the 2 bytes are a mid-row code - */ -Cea608Stream.prototype.isMidRowCode = function(char0, char1) { - return (char0 === this.EXT_ && (char1 >= 0x20 && char1 <= 0x2f)); -}; - -/** - * Detects if the 2-byte packet is an offset control code - * - * Offset control codes have a second byte in the range 0x21 to 0x23, - * with the first byte being 0x17 (for data channel 1) or 0x1f (for - * data channel 2). - * - * @param {Integer} char0 The first byte - * @param {Integer} char1 The second byte - * @return {Boolean} Whether the 2 bytes are an offset control code - */ -Cea608Stream.prototype.isOffsetControlCode = function(char0, char1) { - return (char0 === this.OFFSET_ && (char1 >= 0x21 && char1 <= 0x23)); -}; - -/** - * Detects if the 2-byte packet is a Preamble Address Code - * - * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1) - * or 0x18 to 0x1f (for data channel 2), with the second byte in the - * range 0x40 to 0x7f. - * - * @param {Integer} char0 The first byte - * @param {Integer} char1 The second byte - * @return {Boolean} Whether the 2 bytes are a PAC - */ -Cea608Stream.prototype.isPAC = function(char0, char1) { - return (char0 >= this.BASE_ && char0 < (this.BASE_ + 8) && - (char1 >= 0x40 && char1 <= 0x7f)); -}; - -/** - * Detects if a packet's second byte is in the range of a PAC color code - * - * PAC color codes have the second byte be in the range 0x40 to 0x4f, or - * 0x60 to 0x6f. - * - * @param {Integer} char1 The second byte - * @return {Boolean} Whether the byte is a color PAC - */ -Cea608Stream.prototype.isColorPAC = function(char1) { - return ((char1 >= 0x40 && char1 <= 0x4f) || (char1 >= 0x60 && char1 <= 0x7f)); -}; - -/** - * Detects if a single byte is in the range of a normal character - * - * Normal text bytes are in the range 0x20 to 0x7f. - * - * @param {Integer} char The byte - * @return {Boolean} Whether the byte is a normal character - */ -Cea608Stream.prototype.isNormalChar = function(char) { - return (char >= 0x20 && char <= 0x7f); -}; - -/** - * Configures roll-up - * - * @param {Integer} pts Current PTS - * @param {Integer} newBaseRow Used by PACs to slide the current window to - * a new position - */ -Cea608Stream.prototype.setRollUp = function(pts, newBaseRow) { - // Reset the base row to the bottom row when switching modes - if (this.mode_ !== 'rollUp') { - this.row_ = BOTTOM_ROW; - this.mode_ = 'rollUp'; - // Spec says to wipe memories when switching to roll-up - this.flushDisplayed(pts); - this.nonDisplayed_ = createDisplayBuffer(); - this.displayed_ = createDisplayBuffer(); - } - - if (newBaseRow !== undefined && newBaseRow !== this.row_) { - // move currently displayed captions (up or down) to the new base row - for (var i = 0; i < this.rollUpRows_; i++) { - this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i]; - this.displayed_[this.row_ - i] = ''; - } - } - - if (newBaseRow === undefined) { - newBaseRow = this.row_; - } - - this.topRow_ = newBaseRow - this.rollUpRows_ + 1; -}; - -// Adds the opening HTML tag for the passed character to the caption text, -// and keeps track of it for later closing -Cea608Stream.prototype.addFormatting = function(pts, format) { - this.formatting_ = this.formatting_.concat(format); - var text = format.reduce(function(text, format) { - return text + '<' + format + '>'; - }, ''); - this[this.mode_](pts, text); -}; - -// Adds HTML closing tags for current formatting to caption text and -// clears remembered formatting -Cea608Stream.prototype.clearFormatting = function(pts) { - if (!this.formatting_.length) { - return; - } - var text = this.formatting_.reverse().reduce(function(text, format) { - return text + ''; - }, ''); - this.formatting_ = []; - this[this.mode_](pts, text); -}; - -// Mode Implementations -Cea608Stream.prototype.popOn = function(pts, text) { - var baseRow = this.nonDisplayed_[this.row_]; - - // buffer characters - baseRow += text; - this.nonDisplayed_[this.row_] = baseRow; -}; - -Cea608Stream.prototype.rollUp = function(pts, text) { - var baseRow = this.displayed_[this.row_]; - - baseRow += text; - this.displayed_[this.row_] = baseRow; - -}; - -Cea608Stream.prototype.shiftRowsUp_ = function() { - var i; - // clear out inactive rows - for (i = 0; i < this.topRow_; i++) { - this.displayed_[i] = ''; - } - for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) { - this.displayed_[i] = ''; - } - // shift displayed rows up - for (i = this.topRow_; i < this.row_; i++) { - this.displayed_[i] = this.displayed_[i + 1]; - } - // clear out the bottom row - this.displayed_[this.row_] = ''; -}; - -Cea608Stream.prototype.paintOn = function(pts, text) { - var baseRow = this.displayed_[this.row_]; - - baseRow += text; - this.displayed_[this.row_] = baseRow; -}; - -// exports -var captionStream = { - CaptionStream: CaptionStream, - Cea608Stream: Cea608Stream -}; - -var discardEmulationPreventionBytes$1 = captionPacketParser.discardEmulationPreventionBytes; -var CaptionStream$1 = captionStream.CaptionStream; - - - -/** - * Maps an offset in the mdat to a sample based on the the size of the samples. - * Assumes that `parseSamples` has been called first. - * - * @param {Number} offset - The offset into the mdat - * @param {Object[]} samples - An array of samples, parsed using `parseSamples` - * @return {?Object} The matching sample, or null if no match was found. - * - * @see ISO-BMFF-12/2015, Section 8.8.8 - **/ -var mapToSample = function(offset, samples) { - var approximateOffset = offset; - - for (var i = 0; i < samples.length; i++) { - var sample = samples[i]; - - if (approximateOffset < sample.size) { - return sample; - } - - approximateOffset -= sample.size; - } - - return null; -}; - -/** - * Finds SEI nal units contained in a Media Data Box. - * Assumes that `parseSamples` has been called first. - * - * @param {Uint8Array} avcStream - The bytes of the mdat - * @param {Object[]} samples - The samples parsed out by `parseSamples` - * @param {Number} trackId - The trackId of this video track - * @return {Object[]} seiNals - the parsed SEI NALUs found. - * The contents of the seiNal should match what is expected by - * CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts) - * - * @see ISO-BMFF-12/2015, Section 8.1.1 - * @see Rec. ITU-T H.264, 7.3.2.3.1 - **/ -var findSeiNals = function(avcStream, samples, trackId) { - var - avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength), - result = [], - seiNal, - i, - length, - lastMatchedSample; - - for (i = 0; i + 4 < avcStream.length; i += length) { - length = avcView.getUint32(i); - i += 4; - - // Bail if this doesn't appear to be an H264 stream - if (length <= 0) { - continue; - } - - switch (avcStream[i] & 0x1F) { - case 0x06: - var data = avcStream.subarray(i + 1, i + 1 + length); - var matchingSample = mapToSample(i, samples); - - seiNal = { - nalUnitType: 'sei_rbsp', - size: length, - data: data, - escapedRBSP: discardEmulationPreventionBytes$1(data), - trackId: trackId - }; - - if (matchingSample) { - seiNal.pts = matchingSample.pts; - seiNal.dts = matchingSample.dts; - lastMatchedSample = matchingSample; - } else if (lastMatchedSample) { - // If a matching sample cannot be found, use the last - // sample's values as they should be as close as possible - seiNal.pts = lastMatchedSample.pts; - seiNal.dts = lastMatchedSample.dts; - } else { - // eslint-disable-next-line no-console - console.log("We've encountered a nal unit without data. See mux.js#233."); - break; - } - - result.push(seiNal); - break; - } - } - - return result; -}; - -/** - * Parses sample information out of Track Run Boxes and calculates - * the absolute presentation and decode timestamps of each sample. - * - * @param {Array} truns - The Trun Run boxes to be parsed - * @param {Number} baseMediaDecodeTime - base media decode time from tfdt - @see ISO-BMFF-12/2015, Section 8.8.12 - * @param {Object} tfhd - The parsed Track Fragment Header - * @see inspect.parseTfhd - * @return {Object[]} the parsed samples - * - * @see ISO-BMFF-12/2015, Section 8.8.8 - **/ -var parseSamples = function(truns, baseMediaDecodeTime, tfhd) { - var currentDts = baseMediaDecodeTime; - var defaultSampleDuration = tfhd.defaultSampleDuration || 0; - var defaultSampleSize = tfhd.defaultSampleSize || 0; - var trackId = tfhd.trackId; - var allSamples = []; - - truns.forEach(function(trun) { - // Note: We currently do not parse the sample table as well - // as the trun. It's possible some sources will require this. - // moov > trak > mdia > minf > stbl - var trackRun = mp4Inspector.parseTrun(trun); - var samples = trackRun.samples; - - samples.forEach(function(sample) { - if (sample.duration === undefined) { - sample.duration = defaultSampleDuration; - } - if (sample.size === undefined) { - sample.size = defaultSampleSize; - } - sample.trackId = trackId; - sample.dts = currentDts; - if (sample.compositionTimeOffset === undefined) { - sample.compositionTimeOffset = 0; - } - sample.pts = currentDts + sample.compositionTimeOffset; - - currentDts += sample.duration; - }); - - allSamples = allSamples.concat(samples); - }); - - return allSamples; -}; - -/** - * Parses out caption nals from an FMP4 segment's video tracks. - * - * @param {Uint8Array} segment - The bytes of a single segment - * @param {Number} videoTrackId - The trackId of a video track in the segment - * @return {Object.} A mapping of video trackId to - * a list of seiNals found in that track - **/ -var parseCaptionNals = function(segment, videoTrackId) { - // To get the samples - var trafs = probe.findBox(segment, ['moof', 'traf']); - // To get SEI NAL units - var mdats = probe.findBox(segment, ['mdat']); - var captionNals = {}; - var mdatTrafPairs = []; - - // Pair up each traf with a mdat as moofs and mdats are in pairs - mdats.forEach(function(mdat, index) { - var matchingTraf = trafs[index]; - mdatTrafPairs.push({ - mdat: mdat, - traf: matchingTraf - }); - }); - - mdatTrafPairs.forEach(function(pair) { - var mdat = pair.mdat; - var traf = pair.traf; - var tfhd = probe.findBox(traf, ['tfhd']); - // Exactly 1 tfhd per traf - var headerInfo = mp4Inspector.parseTfhd(tfhd[0]); - var trackId = headerInfo.trackId; - var tfdt = probe.findBox(traf, ['tfdt']); - // Either 0 or 1 tfdt per traf - var baseMediaDecodeTime = (tfdt.length > 0) ? mp4Inspector.parseTfdt(tfdt[0]).baseMediaDecodeTime : 0; - var truns = probe.findBox(traf, ['trun']); - var samples; - var seiNals; - - // Only parse video data for the chosen video track - if (videoTrackId === trackId && truns.length > 0) { - samples = parseSamples(truns, baseMediaDecodeTime, headerInfo); - - seiNals = findSeiNals(mdat, samples, trackId); - - if (!captionNals[trackId]) { - captionNals[trackId] = []; - } - - captionNals[trackId] = captionNals[trackId].concat(seiNals); - } - }); - - return captionNals; -}; - -/** - * Parses out inband captions from an MP4 container and returns - * caption objects that can be used by WebVTT and the TextTrack API. - * @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue - * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack - * Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first - * - * @param {Uint8Array} segment - The fmp4 segment containing embedded captions - * @param {Number} trackId - The id of the video track to parse - * @param {Number} timescale - The timescale for the video track from the init segment - * - * @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks - * @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds - * @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds - * @return {String} parsedCaptions[].text - The visible content of the caption - **/ -var parseEmbeddedCaptions = function(segment, trackId, timescale) { - var seiNals; - - // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there - if (trackId === null) { - return null; - } - - seiNals = parseCaptionNals(segment, trackId); - - return { - seiNals: seiNals[trackId], - timescale: timescale - }; -}; - -/** - * Converts SEI NALUs into captions that can be used by video.js - **/ -var CaptionParser = function() { - var isInitialized = false; - var captionStream; - - // Stores segments seen before trackId and timescale are set - var segmentCache; - // Stores video track ID of the track being parsed - var trackId; - // Stores the timescale of the track being parsed - var timescale; - // Stores captions parsed so far - var parsedCaptions; - // Stores whether we are receiving partial data or not - var parsingPartial; - - /** - * A method to indicate whether a CaptionParser has been initalized - * @returns {Boolean} - **/ - this.isInitialized = function() { - return isInitialized; - }; - - /** - * Initializes the underlying CaptionStream, SEI NAL parsing - * and management, and caption collection - **/ - this.init = function(options) { - captionStream = new CaptionStream$1(); - isInitialized = true; - parsingPartial = options ? options.isPartial : false; - - // Collect dispatched captions - captionStream.on('data', function(event) { - // Convert to seconds in the source's timescale - event.startTime = event.startPts / timescale; - event.endTime = event.endPts / timescale; - - parsedCaptions.captions.push(event); - parsedCaptions.captionStreams[event.stream] = true; - }); - }; - - /** - * Determines if a new video track will be selected - * or if the timescale changed - * @return {Boolean} - **/ - this.isNewInit = function(videoTrackIds, timescales) { - if ((videoTrackIds && videoTrackIds.length === 0) || - (timescales && typeof timescales === 'object' && - Object.keys(timescales).length === 0)) { - return false; - } - - return trackId !== videoTrackIds[0] || - timescale !== timescales[trackId]; - }; - - /** - * Parses out SEI captions and interacts with underlying - * CaptionStream to return dispatched captions - * - * @param {Uint8Array} segment - The fmp4 segment containing embedded captions - * @param {Number[]} videoTrackIds - A list of video tracks found in the init segment - * @param {Object.} timescales - The timescales found in the init segment - * @see parseEmbeddedCaptions - * @see m2ts/caption-stream.js - **/ - this.parse = function(segment, videoTrackIds, timescales) { - var parsedData; - - if (!this.isInitialized()) { - return null; - - // This is not likely to be a video segment - } else if (!videoTrackIds || !timescales) { - return null; - - } else if (this.isNewInit(videoTrackIds, timescales)) { - // Use the first video track only as there is no - // mechanism to switch to other video tracks - trackId = videoTrackIds[0]; - timescale = timescales[trackId]; - - // If an init segment has not been seen yet, hold onto segment - // data until we have one. - // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there - } else if (trackId === null || !timescale) { - segmentCache.push(segment); - return null; - } - - // Now that a timescale and trackId is set, parse cached segments - while (segmentCache.length > 0) { - var cachedSegment = segmentCache.shift(); - - this.parse(cachedSegment, videoTrackIds, timescales); - } - - parsedData = parseEmbeddedCaptions(segment, trackId, timescale); - - if (parsedData === null || !parsedData.seiNals) { - return null; - } - - this.pushNals(parsedData.seiNals); - // Force the parsed captions to be dispatched - this.flushStream(); - - return parsedCaptions; - }; - - /** - * Pushes SEI NALUs onto CaptionStream - * @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals` - * Assumes that `parseCaptionNals` has been called first - * @see m2ts/caption-stream.js - **/ - this.pushNals = function(nals) { - if (!this.isInitialized() || !nals || nals.length === 0) { - return null; - } - - nals.forEach(function(nal) { - captionStream.push(nal); - }); - }; - - /** - * Flushes underlying CaptionStream to dispatch processed, displayable captions - * @see m2ts/caption-stream.js - **/ - this.flushStream = function() { - if (!this.isInitialized()) { - return null; - } - - if (!parsingPartial) { - captionStream.flush(); - } else { - captionStream.partialFlush(); - } - }; - - /** - * Reset caption buckets for new data - **/ - this.clearParsedCaptions = function() { - parsedCaptions.captions = []; - parsedCaptions.captionStreams = {}; - }; - - /** - * Resets underlying CaptionStream - * @see m2ts/caption-stream.js - **/ - this.resetCaptionStream = function() { - if (!this.isInitialized()) { - return null; - } - - captionStream.reset(); - }; - - /** - * Convenience method to clear all captions flushed from the - * CaptionStream and still being parsed - * @see m2ts/caption-stream.js - **/ - this.clearAllCaptions = function() { - this.clearParsedCaptions(); - this.resetCaptionStream(); - }; - - /** - * Reset caption parser - **/ - this.reset = function() { - segmentCache = []; - trackId = null; - timescale = null; - - if (!parsedCaptions) { - parsedCaptions = { - captions: [], - // CC1, CC2, CC3, CC4 - captionStreams: {} - }; - } else { - this.clearParsedCaptions(); - } - - this.resetCaptionStream(); - }; - - this.reset(); -}; - -var captionParser = CaptionParser; - -/** - * mux.js - * - * Copyright (c) Brightcove - * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE - */ - -var streamTypes = { - H264_STREAM_TYPE: 0x1B, - ADTS_STREAM_TYPE: 0x0F, - METADATA_STREAM_TYPE: 0x15 -}; - -var MAX_TS = 8589934592; - -var RO_THRESH = 4294967296; - -var TYPE_SHARED = 'shared'; - -var handleRollover = function(value, reference) { - var direction = 1; - - if (value > reference) { - // If the current timestamp value is greater than our reference timestamp and we detect a - // timestamp rollover, this means the roll over is happening in the opposite direction. - // Example scenario: Enter a long stream/video just after a rollover occurred. The reference - // point will be set to a small number, e.g. 1. The user then seeks backwards over the - // rollover point. In loading this segment, the timestamp values will be very large, - // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust - // the time stamp to be `value - 2^33`. - direction = -1; - } - - // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will - // cause an incorrect adjustment. - while (Math.abs(reference - value) > RO_THRESH) { - value += (direction * MAX_TS); - } - - return value; -}; - -var TimestampRolloverStream = function(type) { - var lastDTS, referenceDTS; - - TimestampRolloverStream.prototype.init.call(this); - - // The "shared" type is used in cases where a stream will contain muxed - // video and audio. We could use `undefined` here, but having a string - // makes debugging a little clearer. - this.type_ = type || TYPE_SHARED; - - this.push = function(data) { - - // Any "shared" rollover streams will accept _all_ data. Otherwise, - // streams will only accept data that matches their type. - if (this.type_ !== TYPE_SHARED && data.type !== this.type_) { - return; - } - - if (referenceDTS === undefined) { - referenceDTS = data.dts; - } - - data.dts = handleRollover(data.dts, referenceDTS); - data.pts = handleRollover(data.pts, referenceDTS); - - lastDTS = data.dts; - - this.trigger('data', data); - }; - - this.flush = function() { - referenceDTS = lastDTS; - this.trigger('done'); - }; - - this.endTimeline = function() { - this.flush(); - this.trigger('endedtimeline'); - }; - - this.discontinuity = function() { - referenceDTS = void 0; - lastDTS = void 0; - }; - - this.reset = function() { - this.discontinuity(); - this.trigger('reset'); - }; -}; - -TimestampRolloverStream.prototype = new stream(); - -var timestampRolloverStream = { - TimestampRolloverStream: TimestampRolloverStream, - handleRollover: handleRollover -}; - -var parsePid = function(packet) { - var pid = packet[1] & 0x1f; - pid <<= 8; - pid |= packet[2]; - return pid; -}; - -var parsePayloadUnitStartIndicator = function(packet) { - return !!(packet[1] & 0x40); -}; - -var parseAdaptionField = function(packet) { - var offset = 0; - // if an adaption field is present, its length is specified by the - // fifth byte of the TS packet header. The adaptation field is - // used to add stuffing to PES packets that don't fill a complete - // TS packet, and to specify some forms of timing and control data - // that we do not currently use. - if (((packet[3] & 0x30) >>> 4) > 0x01) { - offset += packet[4] + 1; - } - return offset; -}; - -var parseType$1 = function(packet, pmtPid) { - var pid = parsePid(packet); - if (pid === 0) { - return 'pat'; - } else if (pid === pmtPid) { - return 'pmt'; - } else if (pmtPid) { - return 'pes'; - } - return null; -}; - -var parsePat = function(packet) { - var pusi = parsePayloadUnitStartIndicator(packet); - var offset = 4 + parseAdaptionField(packet); - - if (pusi) { - offset += packet[offset] + 1; - } - - return (packet[offset + 10] & 0x1f) << 8 | packet[offset + 11]; -}; - -var parsePmt = function(packet) { - var programMapTable = {}; - var pusi = parsePayloadUnitStartIndicator(packet); - var payloadOffset = 4 + parseAdaptionField(packet); - - if (pusi) { - payloadOffset += packet[payloadOffset] + 1; - } - - // PMTs can be sent ahead of the time when they should actually - // take effect. We don't believe this should ever be the case - // for HLS but we'll ignore "forward" PMT declarations if we see - // them. Future PMT declarations have the current_next_indicator - // set to zero. - if (!(packet[payloadOffset + 5] & 0x01)) { - return; - } - - var sectionLength, tableEnd, programInfoLength; - // the mapping table ends at the end of the current section - sectionLength = (packet[payloadOffset + 1] & 0x0f) << 8 | packet[payloadOffset + 2]; - tableEnd = 3 + sectionLength - 4; - - // to determine where the table is, we have to figure out how - // long the program info descriptors are - programInfoLength = (packet[payloadOffset + 10] & 0x0f) << 8 | packet[payloadOffset + 11]; - - // advance the offset to the first entry in the mapping table - var offset = 12 + programInfoLength; - while (offset < tableEnd) { - var i = payloadOffset + offset; - // add an entry that maps the elementary_pid to the stream_type - programMapTable[(packet[i + 1] & 0x1F) << 8 | packet[i + 2]] = packet[i]; - - // move to the next table entry - // skip past the elementary stream descriptors, if present - offset += ((packet[i + 3] & 0x0F) << 8 | packet[i + 4]) + 5; - } - return programMapTable; -}; - -var parsePesType = function(packet, programMapTable) { - var pid = parsePid(packet); - var type = programMapTable[pid]; - switch (type) { - case streamTypes.H264_STREAM_TYPE: - return 'video'; - case streamTypes.ADTS_STREAM_TYPE: - return 'audio'; - case streamTypes.METADATA_STREAM_TYPE: - return 'timed-metadata'; - default: - return null; - } -}; - -var parsePesTime = function(packet) { - var pusi = parsePayloadUnitStartIndicator(packet); - if (!pusi) { - return null; - } - - var offset = 4 + parseAdaptionField(packet); - - if (offset >= packet.byteLength) { - // From the H 222.0 MPEG-TS spec - // "For transport stream packets carrying PES packets, stuffing is needed when there - // is insufficient PES packet data to completely fill the transport stream packet - // payload bytes. Stuffing is accomplished by defining an adaptation field longer than - // the sum of the lengths of the data elements in it, so that the payload bytes - // remaining after the adaptation field exactly accommodates the available PES packet - // data." - // - // If the offset is >= the length of the packet, then the packet contains no data - // and instead is just adaption field stuffing bytes - return null; - } - - var pes = null; - var ptsDtsFlags; - - // PES packets may be annotated with a PTS value, or a PTS value - // and a DTS value. Determine what combination of values is - // available to work with. - ptsDtsFlags = packet[offset + 7]; - - // PTS and DTS are normally stored as a 33-bit number. Javascript - // performs all bitwise operations on 32-bit integers but javascript - // supports a much greater range (52-bits) of integer using standard - // mathematical operations. - // We construct a 31-bit value using bitwise operators over the 31 - // most significant bits and then multiply by 4 (equal to a left-shift - // of 2) before we add the final 2 least significant bits of the - // timestamp (equal to an OR.) - if (ptsDtsFlags & 0xC0) { - pes = {}; - // the PTS and DTS are not written out directly. For information - // on how they are encoded, see - // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html - pes.pts = (packet[offset + 9] & 0x0E) << 27 | - (packet[offset + 10] & 0xFF) << 20 | - (packet[offset + 11] & 0xFE) << 12 | - (packet[offset + 12] & 0xFF) << 5 | - (packet[offset + 13] & 0xFE) >>> 3; - pes.pts *= 4; // Left shift by 2 - pes.pts += (packet[offset + 13] & 0x06) >>> 1; // OR by the two LSBs - pes.dts = pes.pts; - if (ptsDtsFlags & 0x40) { - pes.dts = (packet[offset + 14] & 0x0E) << 27 | - (packet[offset + 15] & 0xFF) << 20 | - (packet[offset + 16] & 0xFE) << 12 | - (packet[offset + 17] & 0xFF) << 5 | - (packet[offset + 18] & 0xFE) >>> 3; - pes.dts *= 4; // Left shift by 2 - pes.dts += (packet[offset + 18] & 0x06) >>> 1; // OR by the two LSBs - } - } - return pes; -}; - -var parseNalUnitType = function(type) { - switch (type) { - case 0x05: - return 'slice_layer_without_partitioning_rbsp_idr'; - case 0x06: - return 'sei_rbsp'; - case 0x07: - return 'seq_parameter_set_rbsp'; - case 0x08: - return 'pic_parameter_set_rbsp'; - case 0x09: - return 'access_unit_delimiter_rbsp'; - default: - return null; - } -}; - -var videoPacketContainsKeyFrame = function(packet) { - var offset = 4 + parseAdaptionField(packet); - var frameBuffer = packet.subarray(offset); - var frameI = 0; - var frameSyncPoint = 0; - var foundKeyFrame = false; - var nalType; - - // advance the sync point to a NAL start, if necessary - for (; frameSyncPoint < frameBuffer.byteLength - 3; frameSyncPoint++) { - if (frameBuffer[frameSyncPoint + 2] === 1) { - // the sync point is properly aligned - frameI = frameSyncPoint + 5; - break; - } - } - - while (frameI < frameBuffer.byteLength) { - // look at the current byte to determine if we've hit the end of - // a NAL unit boundary - switch (frameBuffer[frameI]) { - case 0: - // skip past non-sync sequences - if (frameBuffer[frameI - 1] !== 0) { - frameI += 2; - break; - } else if (frameBuffer[frameI - 2] !== 0) { - frameI++; - break; - } - - if (frameSyncPoint + 3 !== frameI - 2) { - nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); - if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { - foundKeyFrame = true; - } - } - - // drop trailing zeroes - do { - frameI++; - } while (frameBuffer[frameI] !== 1 && frameI < frameBuffer.length); - frameSyncPoint = frameI - 2; - frameI += 3; - break; - case 1: - // skip past non-sync sequences - if (frameBuffer[frameI - 1] !== 0 || - frameBuffer[frameI - 2] !== 0) { - frameI += 3; - break; - } - - nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); - if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { - foundKeyFrame = true; - } - frameSyncPoint = frameI - 2; - frameI += 3; - break; - default: - // the current byte isn't a one or zero, so it cannot be part - // of a sync sequence - frameI += 3; - break; - } - } - frameBuffer = frameBuffer.subarray(frameSyncPoint); - frameI -= frameSyncPoint; - frameSyncPoint = 0; - // parse the final nal - if (frameBuffer && frameBuffer.byteLength > 3) { - nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); - if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { - foundKeyFrame = true; - } - } - - return foundKeyFrame; -}; - - -var probe$1 = { - parseType: parseType$1, - parsePat: parsePat, - parsePmt: parsePmt, - parsePayloadUnitStartIndicator: parsePayloadUnitStartIndicator, - parsePesType: parsePesType, - parsePesTime: parsePesTime, - videoPacketContainsKeyFrame: videoPacketContainsKeyFrame -}; - -/** - * mux.js - * - * Copyright (c) Brightcove - * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE - * - * Utilities to detect basic properties and metadata about Aac data. - */ - -var ADTS_SAMPLING_FREQUENCIES = [ - 96000, - 88200, - 64000, - 48000, - 44100, - 32000, - 24000, - 22050, - 16000, - 12000, - 11025, - 8000, - 7350 -]; - -var isLikelyAacData = function(data) { - if ((data[0] === 'I'.charCodeAt(0)) && - (data[1] === 'D'.charCodeAt(0)) && - (data[2] === '3'.charCodeAt(0))) { - return true; - } - return false; -}; - -var parseSyncSafeInteger = function(data) { - return (data[0] << 21) | - (data[1] << 14) | - (data[2] << 7) | - (data[3]); -}; - -// return a percent-encoded representation of the specified byte range -// @see http://en.wikipedia.org/wiki/Percent-encoding -var percentEncode = function(bytes, start, end) { - var i, result = ''; - for (i = start; i < end; i++) { - result += '%' + ('00' + bytes[i].toString(16)).slice(-2); - } - return result; -}; - -// return the string representation of the specified byte range, -// interpreted as ISO-8859-1. -var parseIso88591 = function(bytes, start, end) { - return unescape(percentEncode(bytes, start, end)); // jshint ignore:line -}; - -var parseId3TagSize = function(header, byteIndex) { - var - returnSize = (header[byteIndex + 6] << 21) | - (header[byteIndex + 7] << 14) | - (header[byteIndex + 8] << 7) | - (header[byteIndex + 9]), - flags = header[byteIndex + 5], - footerPresent = (flags & 16) >> 4; - - if (footerPresent) { - return returnSize + 20; - } - return returnSize + 10; -}; - -var parseAdtsSize = function(header, byteIndex) { - var - lowThree = (header[byteIndex + 5] & 0xE0) >> 5, - middle = header[byteIndex + 4] << 3, - highTwo = header[byteIndex + 3] & 0x3 << 11; - - return (highTwo | middle) | lowThree; -}; - -var parseType$2 = function(header, byteIndex) { - if ((header[byteIndex] === 'I'.charCodeAt(0)) && - (header[byteIndex + 1] === 'D'.charCodeAt(0)) && - (header[byteIndex + 2] === '3'.charCodeAt(0))) { - return 'timed-metadata'; - } else if ((header[byteIndex] & 0xff === 0xff) && - ((header[byteIndex + 1] & 0xf0) === 0xf0)) { - return 'audio'; - } - return null; -}; - -var parseSampleRate = function(packet) { - var i = 0; - - while (i + 5 < packet.length) { - if (packet[i] !== 0xFF || (packet[i + 1] & 0xF6) !== 0xF0) { - // If a valid header was not found, jump one forward and attempt to - // find a valid ADTS header starting at the next byte - i++; - continue; - } - return ADTS_SAMPLING_FREQUENCIES[(packet[i + 2] & 0x3c) >>> 2]; - } - - return null; -}; - -var parseAacTimestamp = function(packet) { - var frameStart, frameSize, frame, frameHeader; - - // find the start of the first frame and the end of the tag - frameStart = 10; - if (packet[5] & 0x40) { - // advance the frame start past the extended header - frameStart += 4; // header size field - frameStart += parseSyncSafeInteger(packet.subarray(10, 14)); - } - - // parse one or more ID3 frames - // http://id3.org/id3v2.3.0#ID3v2_frame_overview - do { - // determine the number of bytes in this frame - frameSize = parseSyncSafeInteger(packet.subarray(frameStart + 4, frameStart + 8)); - if (frameSize < 1) { - return null; - } - frameHeader = String.fromCharCode(packet[frameStart], - packet[frameStart + 1], - packet[frameStart + 2], - packet[frameStart + 3]); - - if (frameHeader === 'PRIV') { - frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10); - - for (var i = 0; i < frame.byteLength; i++) { - if (frame[i] === 0) { - var owner = parseIso88591(frame, 0, i); - if (owner === 'com.apple.streaming.transportStreamTimestamp') { - var d = frame.subarray(i + 1); - var size = ((d[3] & 0x01) << 30) | - (d[4] << 22) | - (d[5] << 14) | - (d[6] << 6) | - (d[7] >>> 2); - size *= 4; - size += d[7] & 0x03; - - return size; - } - break; - } - } - } - - frameStart += 10; // advance past the frame header - frameStart += frameSize; // advance past the frame body - } while (frameStart < packet.byteLength); - return null; -}; - -var utils = { - isLikelyAacData: isLikelyAacData, - parseId3TagSize: parseId3TagSize, - parseAdtsSize: parseAdtsSize, - parseType: parseType$2, - parseSampleRate: parseSampleRate, - parseAacTimestamp: parseAacTimestamp -}; - -/** - * mux.js - * - * Copyright (c) Brightcove - * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE - */ -var - ONE_SECOND_IN_TS = 90000, // 90kHz clock - secondsToVideoTs, - secondsToAudioTs, - videoTsToSeconds, - audioTsToSeconds, - audioTsToVideoTs, - videoTsToAudioTs, - metadataTsToSeconds; - -secondsToVideoTs = function(seconds) { - return seconds * ONE_SECOND_IN_TS; -}; - -secondsToAudioTs = function(seconds, sampleRate) { - return seconds * sampleRate; -}; - -videoTsToSeconds = function(timestamp) { - return timestamp / ONE_SECOND_IN_TS; -}; - -audioTsToSeconds = function(timestamp, sampleRate) { - return timestamp / sampleRate; -}; - -audioTsToVideoTs = function(timestamp, sampleRate) { - return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); -}; - -videoTsToAudioTs = function(timestamp, sampleRate) { - return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); -}; - -/** - * Adjust ID3 tag or caption timing information by the timeline pts values - * (if keepOriginalTimestamps is false) and convert to seconds - */ -metadataTsToSeconds = function(timestamp, timelineStartPts, keepOriginalTimestamps) { - return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts); -}; - -var clock = { - ONE_SECOND_IN_TS: ONE_SECOND_IN_TS, - secondsToVideoTs: secondsToVideoTs, - secondsToAudioTs: secondsToAudioTs, - videoTsToSeconds: videoTsToSeconds, - audioTsToSeconds: audioTsToSeconds, - audioTsToVideoTs: audioTsToVideoTs, - videoTsToAudioTs: videoTsToAudioTs, - metadataTsToSeconds: metadataTsToSeconds -}; - -var handleRollover$1 = timestampRolloverStream.handleRollover; -var probe$2 = {}; -probe$2.ts = probe$1; -probe$2.aac = utils; -var ONE_SECOND_IN_TS$1 = clock.ONE_SECOND_IN_TS; - -var - MP2T_PACKET_LENGTH = 188, // bytes - SYNC_BYTE = 0x47; - -/** - * walks through segment data looking for pat and pmt packets to parse out - * program map table information - */ -var parsePsi_ = function(bytes, pmt) { - var - startIndex = 0, - endIndex = MP2T_PACKET_LENGTH, - packet, type; - - while (endIndex < bytes.byteLength) { - // Look for a pair of start and end sync bytes in the data.. - if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { - // We found a packet - packet = bytes.subarray(startIndex, endIndex); - type = probe$2.ts.parseType(packet, pmt.pid); - - switch (type) { - case 'pat': - if (!pmt.pid) { - pmt.pid = probe$2.ts.parsePat(packet); - } - break; - case 'pmt': - if (!pmt.table) { - pmt.table = probe$2.ts.parsePmt(packet); - } - break; - } - - // Found the pat and pmt, we can stop walking the segment - if (pmt.pid && pmt.table) { - return; - } - - startIndex += MP2T_PACKET_LENGTH; - endIndex += MP2T_PACKET_LENGTH; - continue; - } - - // If we get here, we have somehow become de-synchronized and we need to step - // forward one byte at a time until we find a pair of sync bytes that denote - // a packet - startIndex++; - endIndex++; - } -}; - -/** - * walks through the segment data from the start and end to get timing information - * for the first and last audio pes packets - */ -var parseAudioPes_ = function(bytes, pmt, result) { - var - startIndex = 0, - endIndex = MP2T_PACKET_LENGTH, - packet, type, pesType, pusi, parsed; - - var endLoop = false; - - // Start walking from start of segment to get first audio packet - while (endIndex <= bytes.byteLength) { - // Look for a pair of start and end sync bytes in the data.. - if (bytes[startIndex] === SYNC_BYTE && - (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) { - // We found a packet - packet = bytes.subarray(startIndex, endIndex); - type = probe$2.ts.parseType(packet, pmt.pid); - - switch (type) { - case 'pes': - pesType = probe$2.ts.parsePesType(packet, pmt.table); - pusi = probe$2.ts.parsePayloadUnitStartIndicator(packet); - if (pesType === 'audio' && pusi) { - parsed = probe$2.ts.parsePesTime(packet); - if (parsed) { - parsed.type = 'audio'; - result.audio.push(parsed); - endLoop = true; - } - } - break; - } - - if (endLoop) { - break; - } - - startIndex += MP2T_PACKET_LENGTH; - endIndex += MP2T_PACKET_LENGTH; - continue; - } - - // If we get here, we have somehow become de-synchronized and we need to step - // forward one byte at a time until we find a pair of sync bytes that denote - // a packet - startIndex++; - endIndex++; - } - - // Start walking from end of segment to get last audio packet - endIndex = bytes.byteLength; - startIndex = endIndex - MP2T_PACKET_LENGTH; - endLoop = false; - while (startIndex >= 0) { - // Look for a pair of start and end sync bytes in the data.. - if (bytes[startIndex] === SYNC_BYTE && - (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) { - // We found a packet - packet = bytes.subarray(startIndex, endIndex); - type = probe$2.ts.parseType(packet, pmt.pid); - - switch (type) { - case 'pes': - pesType = probe$2.ts.parsePesType(packet, pmt.table); - pusi = probe$2.ts.parsePayloadUnitStartIndicator(packet); - if (pesType === 'audio' && pusi) { - parsed = probe$2.ts.parsePesTime(packet); - if (parsed) { - parsed.type = 'audio'; - result.audio.push(parsed); - endLoop = true; - } - } - break; - } - - if (endLoop) { - break; - } - - startIndex -= MP2T_PACKET_LENGTH; - endIndex -= MP2T_PACKET_LENGTH; - continue; - } - - // If we get here, we have somehow become de-synchronized and we need to step - // forward one byte at a time until we find a pair of sync bytes that denote - // a packet - startIndex--; - endIndex--; - } -}; - -/** - * walks through the segment data from the start and end to get timing information - * for the first and last video pes packets as well as timing information for the first - * key frame. - */ -var parseVideoPes_ = function(bytes, pmt, result) { - var - startIndex = 0, - endIndex = MP2T_PACKET_LENGTH, - packet, type, pesType, pusi, parsed, frame, i, pes; - - var endLoop = false; - - var currentFrame = { - data: [], - size: 0 - }; - - // Start walking from start of segment to get first video packet - while (endIndex < bytes.byteLength) { - // Look for a pair of start and end sync bytes in the data.. - if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { - // We found a packet - packet = bytes.subarray(startIndex, endIndex); - type = probe$2.ts.parseType(packet, pmt.pid); - - switch (type) { - case 'pes': - pesType = probe$2.ts.parsePesType(packet, pmt.table); - pusi = probe$2.ts.parsePayloadUnitStartIndicator(packet); - if (pesType === 'video') { - if (pusi && !endLoop) { - parsed = probe$2.ts.parsePesTime(packet); - if (parsed) { - parsed.type = 'video'; - result.video.push(parsed); - endLoop = true; - } - } - if (!result.firstKeyFrame) { - if (pusi) { - if (currentFrame.size !== 0) { - frame = new Uint8Array(currentFrame.size); - i = 0; - while (currentFrame.data.length) { - pes = currentFrame.data.shift(); - frame.set(pes, i); - i += pes.byteLength; - } - if (probe$2.ts.videoPacketContainsKeyFrame(frame)) { - var firstKeyFrame = probe$2.ts.parsePesTime(frame); - - // PTS/DTS may not be available. Simply *not* setting - // the keyframe seems to work fine with HLS playback - // and definitely preferable to a crash with TypeError... - if (firstKeyFrame) { - result.firstKeyFrame = firstKeyFrame; - result.firstKeyFrame.type = 'video'; - } else { - // eslint-disable-next-line - console.warn( - 'Failed to extract PTS/DTS from PES at first keyframe. ' + - 'This could be an unusual TS segment, or else mux.js did not ' + - 'parse your TS segment correctly. If you know your TS ' + - 'segments do contain PTS/DTS on keyframes please file a bug ' + - 'report! You can try ffprobe to double check for yourself.' - ); - } - } - currentFrame.size = 0; - } - } - currentFrame.data.push(packet); - currentFrame.size += packet.byteLength; - } - } - break; - } - - if (endLoop && result.firstKeyFrame) { - break; - } - - startIndex += MP2T_PACKET_LENGTH; - endIndex += MP2T_PACKET_LENGTH; - continue; - } - - // If we get here, we have somehow become de-synchronized and we need to step - // forward one byte at a time until we find a pair of sync bytes that denote - // a packet - startIndex++; - endIndex++; - } - - // Start walking from end of segment to get last video packet - endIndex = bytes.byteLength; - startIndex = endIndex - MP2T_PACKET_LENGTH; - endLoop = false; - while (startIndex >= 0) { - // Look for a pair of start and end sync bytes in the data.. - if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { - // We found a packet - packet = bytes.subarray(startIndex, endIndex); - type = probe$2.ts.parseType(packet, pmt.pid); - - switch (type) { - case 'pes': - pesType = probe$2.ts.parsePesType(packet, pmt.table); - pusi = probe$2.ts.parsePayloadUnitStartIndicator(packet); - if (pesType === 'video' && pusi) { - parsed = probe$2.ts.parsePesTime(packet); - if (parsed) { - parsed.type = 'video'; - result.video.push(parsed); - endLoop = true; - } - } - break; - } - - if (endLoop) { - break; - } - - startIndex -= MP2T_PACKET_LENGTH; - endIndex -= MP2T_PACKET_LENGTH; - continue; - } - - // If we get here, we have somehow become de-synchronized and we need to step - // forward one byte at a time until we find a pair of sync bytes that denote - // a packet - startIndex--; - endIndex--; - } -}; - -/** - * Adjusts the timestamp information for the segment to account for - * rollover and convert to seconds based on pes packet timescale (90khz clock) - */ -var adjustTimestamp_ = function(segmentInfo, baseTimestamp) { - if (segmentInfo.audio && segmentInfo.audio.length) { - var audioBaseTimestamp = baseTimestamp; - if (typeof audioBaseTimestamp === 'undefined') { - audioBaseTimestamp = segmentInfo.audio[0].dts; - } - segmentInfo.audio.forEach(function(info) { - info.dts = handleRollover$1(info.dts, audioBaseTimestamp); - info.pts = handleRollover$1(info.pts, audioBaseTimestamp); - // time in seconds - info.dtsTime = info.dts / ONE_SECOND_IN_TS$1; - info.ptsTime = info.pts / ONE_SECOND_IN_TS$1; - }); - } - - if (segmentInfo.video && segmentInfo.video.length) { - var videoBaseTimestamp = baseTimestamp; - if (typeof videoBaseTimestamp === 'undefined') { - videoBaseTimestamp = segmentInfo.video[0].dts; - } - segmentInfo.video.forEach(function(info) { - info.dts = handleRollover$1(info.dts, videoBaseTimestamp); - info.pts = handleRollover$1(info.pts, videoBaseTimestamp); - // time in seconds - info.dtsTime = info.dts / ONE_SECOND_IN_TS$1; - info.ptsTime = info.pts / ONE_SECOND_IN_TS$1; - }); - if (segmentInfo.firstKeyFrame) { - var frame = segmentInfo.firstKeyFrame; - frame.dts = handleRollover$1(frame.dts, videoBaseTimestamp); - frame.pts = handleRollover$1(frame.pts, videoBaseTimestamp); - // time in seconds - frame.dtsTime = frame.dts / ONE_SECOND_IN_TS$1; - frame.ptsTime = frame.dts / ONE_SECOND_IN_TS$1; - } - } -}; - -/** - * inspects the aac data stream for start and end time information - */ -var inspectAac_ = function(bytes) { - var - endLoop = false, - audioCount = 0, - sampleRate = null, - timestamp = null, - frameSize = 0, - byteIndex = 0, - packet; - - while (bytes.length - byteIndex >= 3) { - var type = probe$2.aac.parseType(bytes, byteIndex); - switch (type) { - case 'timed-metadata': - // Exit early because we don't have enough to parse - // the ID3 tag header - if (bytes.length - byteIndex < 10) { - endLoop = true; - break; - } - - frameSize = probe$2.aac.parseId3TagSize(bytes, byteIndex); - - // Exit early if we don't have enough in the buffer - // to emit a full packet - if (frameSize > bytes.length) { - endLoop = true; - break; - } - if (timestamp === null) { - packet = bytes.subarray(byteIndex, byteIndex + frameSize); - timestamp = probe$2.aac.parseAacTimestamp(packet); - } - byteIndex += frameSize; - break; - case 'audio': - // Exit early because we don't have enough to parse - // the ADTS frame header - if (bytes.length - byteIndex < 7) { - endLoop = true; - break; - } - - frameSize = probe$2.aac.parseAdtsSize(bytes, byteIndex); - - // Exit early if we don't have enough in the buffer - // to emit a full packet - if (frameSize > bytes.length) { - endLoop = true; - break; - } - if (sampleRate === null) { - packet = bytes.subarray(byteIndex, byteIndex + frameSize); - sampleRate = probe$2.aac.parseSampleRate(packet); - } - audioCount++; - byteIndex += frameSize; - break; - default: - byteIndex++; - break; - } - if (endLoop) { - return null; - } - } - if (sampleRate === null || timestamp === null) { - return null; - } - - var audioTimescale = ONE_SECOND_IN_TS$1 / sampleRate; - - var result = { - audio: [ - { - type: 'audio', - dts: timestamp, - pts: timestamp - }, - { - type: 'audio', - dts: timestamp + (audioCount * 1024 * audioTimescale), - pts: timestamp + (audioCount * 1024 * audioTimescale) - } - ] - }; - - return result; -}; - -/** - * inspects the transport stream segment data for start and end time information - * of the audio and video tracks (when present) as well as the first key frame's - * start time. - */ -var inspectTs_ = function(bytes) { - var pmt = { - pid: null, - table: null - }; - - var result = {}; - - parsePsi_(bytes, pmt); - - for (var pid in pmt.table) { - if (pmt.table.hasOwnProperty(pid)) { - var type = pmt.table[pid]; - switch (type) { - case streamTypes.H264_STREAM_TYPE: - result.video = []; - parseVideoPes_(bytes, pmt, result); - if (result.video.length === 0) { - delete result.video; - } - break; - case streamTypes.ADTS_STREAM_TYPE: - result.audio = []; - parseAudioPes_(bytes, pmt, result); - if (result.audio.length === 0) { - delete result.audio; - } - break; - } - } - } - return result; -}; - -/** - * Inspects segment byte data and returns an object with start and end timing information - * - * @param {Uint8Array} bytes The segment byte data - * @param {Number} baseTimestamp Relative reference timestamp used when adjusting frame - * timestamps for rollover. This value must be in 90khz clock. - * @return {Object} Object containing start and end frame timing info of segment. - */ -var inspect = function(bytes, baseTimestamp) { - var isAacData = probe$2.aac.isLikelyAacData(bytes); - - var result; - - if (isAacData) { - result = inspectAac_(bytes); - } else { - result = inspectTs_(bytes); - } - - if (!result || (!result.audio && !result.video)) { - return null; - } - - adjustTimestamp_(result, baseTimestamp); - - return result; -}; - -var tsInspector = { - inspect: inspect, - parseAudioPes_: parseAudioPes_ -}; - -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); - } -} - -function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - return Constructor; -} - -var createClass = _createClass; - -/*! @name @videojs/vhs-utils @version 1.3.0 @license MIT */ - -/** - * @file stream.js - */ - -/** - * A lightweight readable stream implemention that handles event dispatching. - * - * @class Stream - */ -var Stream$2 = -/*#__PURE__*/ -function () { - function Stream() { - this.listeners = {}; - } - /** - * Add a listener for a specified event type. - * - * @param {string} type the event name - * @param {Function} listener the callback to be invoked when an event of - * the specified type occurs - */ - - - var _proto = Stream.prototype; - - _proto.on = function on(type, listener) { - if (!this.listeners[type]) { - this.listeners[type] = []; - } - - this.listeners[type].push(listener); - } - /** - * Remove a listener for a specified event type. - * - * @param {string} type the event name - * @param {Function} listener a function previously registered for this - * type of event through `on` - * @return {boolean} if we could turn it off or not - */ - ; - - _proto.off = function off(type, listener) { - if (!this.listeners[type]) { - return false; - } - - var index = this.listeners[type].indexOf(listener); // TODO: which is better? - // In Video.js we slice listener functions - // on trigger so that it does not mess up the order - // while we loop through. - // - // Here we slice on off so that the loop in trigger - // can continue using it's old reference to loop without - // messing up the order. - - this.listeners[type] = this.listeners[type].slice(0); - this.listeners[type].splice(index, 1); - return index > -1; - } - /** - * Trigger an event of the specified type on this stream. Any additional - * arguments to this function are passed as parameters to event listeners. - * - * @param {string} type the event name - */ - ; - - _proto.trigger = function trigger(type) { - var callbacks = this.listeners[type]; - - if (!callbacks) { - return; - } // Slicing the arguments on every invocation of this method - // can add a significant amount of overhead. Avoid the - // intermediate object creation for the common case of a - // single callback argument - - - if (arguments.length === 2) { - var length = callbacks.length; - - for (var i = 0; i < length; ++i) { - callbacks[i].call(this, arguments[1]); - } - } else { - var args = Array.prototype.slice.call(arguments, 1); - var _length = callbacks.length; - - for (var _i = 0; _i < _length; ++_i) { - callbacks[_i].apply(this, args); - } - } - } - /** - * Destroys the stream and cleans up. - */ - ; - - _proto.dispose = function dispose() { - this.listeners = {}; - } - /** - * Forwards all `data` events on this stream to the destination stream. The - * destination stream should provide a method `push` to receive the data - * events as they arrive. - * - * @param {Stream} destination the stream that will receive all `data` events - * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options - */ - ; - - _proto.pipe = function pipe(destination) { - this.on('data', function (data) { - destination.push(data); - }); - }; - - return Stream; -}(); - -var stream$1 = Stream$2; - -/*! @name pkcs7 @version 1.0.4 @license Apache-2.0 */ - -/** - * Returns the subarray of a Uint8Array without PKCS#7 padding. - * - * @param padded {Uint8Array} unencrypted bytes that have been padded - * @return {Uint8Array} the unpadded bytes - * @see http://tools.ietf.org/html/rfc5652 - */ -function unpad(padded) { - return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]); -} - -/*! @name aes-decrypter @version 3.0.2 @license Apache-2.0 */ - -/** - * @file aes.js - * - * This file contains an adaptation of the AES decryption algorithm - * from the Standford Javascript Cryptography Library. That work is - * covered by the following copyright and permissions notice: - * - * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR - * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE - * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN - * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * The views and conclusions contained in the software and documentation - * are those of the authors and should not be interpreted as representing - * official policies, either expressed or implied, of the authors. - */ - -/** - * Expand the S-box tables. - * - * @private - */ -var precompute = function precompute() { - var tables = [[[], [], [], [], []], [[], [], [], [], []]]; - var encTable = tables[0]; - var decTable = tables[1]; - var sbox = encTable[4]; - var sboxInv = decTable[4]; - var i; - var x; - var xInv; - var d = []; - var th = []; - var x2; - var x4; - var x8; - var s; - var tEnc; - var tDec; // Compute double and third tables - - for (i = 0; i < 256; i++) { - th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i; - } - - for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { - // Compute sbox - s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; - s = s >> 8 ^ s & 255 ^ 99; - sbox[x] = s; - sboxInv[s] = x; // Compute MixColumns - - x8 = d[x4 = d[x2 = d[x]]]; - tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; - tEnc = d[s] * 0x101 ^ s * 0x1010100; - - for (i = 0; i < 4; i++) { - encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8; - decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8; - } - } // Compactify. Considerable speedup on Firefox. - - - for (i = 0; i < 5; i++) { - encTable[i] = encTable[i].slice(0); - decTable[i] = decTable[i].slice(0); - } - - return tables; -}; - -var aesTables = null; -/** - * Schedule out an AES key for both encryption and decryption. This - * is a low-level class. Use a cipher mode to do bulk encryption. - * - * @class AES - * @param key {Array} The key as an array of 4, 6 or 8 words. - */ - -var AES = -/*#__PURE__*/ -function () { - function AES(key) { - /** - * The expanded S-box and inverse S-box tables. These will be computed - * on the client so that we don't have to send them down the wire. - * - * There are two tables, _tables[0] is for encryption and - * _tables[1] is for decryption. - * - * The first 4 sub-tables are the expanded S-box with MixColumns. The - * last (_tables[01][4]) is the S-box itself. - * - * @private - */ - // if we have yet to precompute the S-box tables - // do so now - if (!aesTables) { - aesTables = precompute(); - } // then make a copy of that object for use - - - this._tables = [[aesTables[0][0].slice(), aesTables[0][1].slice(), aesTables[0][2].slice(), aesTables[0][3].slice(), aesTables[0][4].slice()], [aesTables[1][0].slice(), aesTables[1][1].slice(), aesTables[1][2].slice(), aesTables[1][3].slice(), aesTables[1][4].slice()]]; - var i; - var j; - var tmp; - var sbox = this._tables[0][4]; - var decTable = this._tables[1]; - var keyLen = key.length; - var rcon = 1; - - if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { - throw new Error('Invalid aes key size'); - } - - var encKey = key.slice(0); - var decKey = []; - this._key = [encKey, decKey]; // schedule encryption keys - - for (i = keyLen; i < 4 * keyLen + 28; i++) { - tmp = encKey[i - 1]; // apply sbox - - if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) { - tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; // shift rows and add rcon - - if (i % keyLen === 0) { - tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24; - rcon = rcon << 1 ^ (rcon >> 7) * 283; - } - } - - encKey[i] = encKey[i - keyLen] ^ tmp; - } // schedule decryption keys - - - for (j = 0; i; j++, i--) { - tmp = encKey[j & 3 ? i : i - 4]; - - if (i <= 4 || j < 4) { - decKey[j] = tmp; - } else { - decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]]; - } - } - } - /** - * Decrypt 16 bytes, specified as four 32-bit words. - * - * @param {number} encrypted0 the first word to decrypt - * @param {number} encrypted1 the second word to decrypt - * @param {number} encrypted2 the third word to decrypt - * @param {number} encrypted3 the fourth word to decrypt - * @param {Int32Array} out the array to write the decrypted words - * into - * @param {number} offset the offset into the output array to start - * writing results - * @return {Array} The plaintext. - */ - - - var _proto = AES.prototype; - - _proto.decrypt = function decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { - var key = this._key[1]; // state variables a,b,c,d are loaded with pre-whitened data - - var a = encrypted0 ^ key[0]; - var b = encrypted3 ^ key[1]; - var c = encrypted2 ^ key[2]; - var d = encrypted1 ^ key[3]; - var a2; - var b2; - var c2; // key.length === 2 ? - - var nInnerRounds = key.length / 4 - 2; - var i; - var kIndex = 4; - var table = this._tables[1]; // load up the tables - - var table0 = table[0]; - var table1 = table[1]; - var table2 = table[2]; - var table3 = table[3]; - var sbox = table[4]; // Inner rounds. Cribbed from OpenSSL. - - for (i = 0; i < nInnerRounds; i++) { - a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex]; - b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; - c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; - d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; - kIndex += 4; - a = a2; - b = b2; - c = c2; - } // Last round. - - - for (i = 0; i < 4; i++) { - out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++]; - a2 = a; - a = b; - b = c; - c = d; - d = a2; - } - }; - - return AES; -}(); - -/** - * A wrapper around the Stream class to use setTimeout - * and run stream "jobs" Asynchronously - * - * @class AsyncStream - * @extends Stream - */ - -var AsyncStream = -/*#__PURE__*/ -function (_Stream) { - inheritsLoose(AsyncStream, _Stream); - - function AsyncStream() { - var _this; - - _this = _Stream.call(this, stream$1) || this; - _this.jobs = []; - _this.delay = 1; - _this.timeout_ = null; - return _this; - } - /** - * process an async job - * - * @private - */ - - - var _proto = AsyncStream.prototype; - - _proto.processJob_ = function processJob_() { - this.jobs.shift()(); - - if (this.jobs.length) { - this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); - } else { - this.timeout_ = null; - } - } - /** - * push a job into the stream - * - * @param {Function} job the job to push into the stream - */ - ; - - _proto.push = function push(job) { - this.jobs.push(job); - - if (!this.timeout_) { - this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); - } - }; - - return AsyncStream; -}(stream$1); - -/** - * Convert network-order (big-endian) bytes into their little-endian - * representation. - */ - -var ntoh = function ntoh(word) { - return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; -}; -/** - * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. - * - * @param {Uint8Array} encrypted the encrypted bytes - * @param {Uint32Array} key the bytes of the decryption key - * @param {Uint32Array} initVector the initialization vector (IV) to - * use for the first round of CBC. - * @return {Uint8Array} the decrypted bytes - * - * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard - * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 - * @see https://tools.ietf.org/html/rfc2315 - */ - - -var decrypt = function decrypt(encrypted, key, initVector) { - // word-level access to the encrypted bytes - var encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2); - var decipher = new AES(Array.prototype.slice.call(key)); // byte and word-level access for the decrypted output - - var decrypted = new Uint8Array(encrypted.byteLength); - var decrypted32 = new Int32Array(decrypted.buffer); // temporary variables for working with the IV, encrypted, and - // decrypted data - - var init0; - var init1; - var init2; - var init3; - var encrypted0; - var encrypted1; - var encrypted2; - var encrypted3; // iteration variable - - var wordIx; // pull out the words of the IV to ensure we don't modify the - // passed-in reference and easier access - - init0 = initVector[0]; - init1 = initVector[1]; - init2 = initVector[2]; - init3 = initVector[3]; // decrypt four word sequences, applying cipher-block chaining (CBC) - // to each decrypted block - - for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { - // convert big-endian (network order) words into little-endian - // (javascript order) - encrypted0 = ntoh(encrypted32[wordIx]); - encrypted1 = ntoh(encrypted32[wordIx + 1]); - encrypted2 = ntoh(encrypted32[wordIx + 2]); - encrypted3 = ntoh(encrypted32[wordIx + 3]); // decrypt the block - - decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); // XOR with the IV, and restore network byte-order to obtain the - // plaintext - - decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); - decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); - decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); - decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); // setup the IV for the next round - - init0 = encrypted0; - init1 = encrypted1; - init2 = encrypted2; - init3 = encrypted3; - } - - return decrypted; -}; -/** - * The `Decrypter` class that manages decryption of AES - * data through `AsyncStream` objects and the `decrypt` - * function - * - * @param {Uint8Array} encrypted the encrypted bytes - * @param {Uint32Array} key the bytes of the decryption key - * @param {Uint32Array} initVector the initialization vector (IV) to - * @param {Function} done the function to run when done - * @class Decrypter - */ - - -var Decrypter = -/*#__PURE__*/ -function () { - function Decrypter(encrypted, key, initVector, done) { - var step = Decrypter.STEP; - var encrypted32 = new Int32Array(encrypted.buffer); - var decrypted = new Uint8Array(encrypted.byteLength); - var i = 0; - this.asyncStream_ = new AsyncStream(); // split up the encryption job and do the individual chunks asynchronously - - this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); - - for (i = step; i < encrypted32.length; i += step) { - initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]); - this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); - } // invoke the done() callback when everything is finished - - - this.asyncStream_.push(function () { - // remove pkcs#7 padding from the decrypted bytes - done(null, unpad(decrypted)); - }); - } - /** - * a getter for step the maximum number of bytes to process at one time - * - * @return {number} the value of step 32000 - */ - - - var _proto = Decrypter.prototype; - - /** - * @private - */ - _proto.decryptChunk_ = function decryptChunk_(encrypted, key, initVector, decrypted) { - return function () { - var bytes = decrypt(encrypted, key, initVector); - decrypted.set(bytes, encrypted.byteOffset); - }; - }; - - createClass(Decrypter, null, [{ - key: "STEP", - get: function get() { - // 4 * 8000; - return 32000; - } - }]); - - return Decrypter; -}(); - -/** - * @license - * Video.js 7.9.6 - * Copyright Brightcove, Inc. - * Available under Apache License Version 2.0 - * - * - * Includes vtt.js - * Available under Apache License Version 2.0 - * - */ - -var version = "7.9.6"; - -/** - * @file create-logger.js - * @module create-logger - */ - -var history = []; -/** - * Log messages to the console and history based on the type of message - * - * @private - * @param {string} type - * The name of the console method to use. - * - * @param {Array} args - * The arguments to be passed to the matching console method. - */ - -var LogByTypeFactory = function LogByTypeFactory(name, log) { - return function (type, level, args) { - var lvl = log.levels[level]; - var lvlRegExp = new RegExp("^(" + lvl + ")$"); - - if (type !== 'log') { - // Add the type to the front of the message when it's not "log". - args.unshift(type.toUpperCase() + ':'); - } // Add console prefix after adding to history. - - - args.unshift(name + ':'); // Add a clone of the args at this point to history. - - if (history) { - history.push([].concat(args)); // only store 1000 history entries - - var splice = history.length - 1000; - history.splice(0, splice > 0 ? splice : 0); - } // If there's no console then don't try to output messages, but they will - // still be stored in history. - - - if (!window_1$1.console) { - return; - } // Was setting these once outside of this function, but containing them - // in the function makes it easier to test cases where console doesn't exist - // when the module is executed. - - - var fn = window_1$1.console[type]; - - if (!fn && type === 'debug') { - // Certain browsers don't have support for console.debug. For those, we - // should default to the closest comparable log. - fn = window_1$1.console.info || window_1$1.console.log; - } // Bail out if there's no console or if this type is not allowed by the - // current logging level. - - - if (!fn || !lvl || !lvlRegExp.test(type)) { - return; - } - - fn[Array.isArray(args) ? 'apply' : 'call'](window_1$1.console, args); - }; -}; - -function createLogger(name) { - // This is the private tracking variable for logging level. - var level = 'info'; // the curried logByType bound to the specific log and history - - var logByType; - /** - * Logs plain debug messages. Similar to `console.log`. - * - * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149) - * of our JSDoc template, we cannot properly document this as both a function - * and a namespace, so its function signature is documented here. - * - * #### Arguments - * ##### *args - * Mixed[] - * - * Any combination of values that could be passed to `console.log()`. - * - * #### Return Value - * - * `undefined` - * - * @namespace - * @param {Mixed[]} args - * One or more messages or objects that should be logged. - */ - - var log = function log() { - for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { - args[_key] = arguments[_key]; - } - - logByType('log', level, args); - }; // This is the logByType helper that the logging methods below use - - - logByType = LogByTypeFactory(name, log); - /** - * Create a new sublogger which chains the old name to the new name. - * - * For example, doing `videojs.log.createLogger('player')` and then using that logger will log the following: - * ```js - * mylogger('foo'); - * // > VIDEOJS: player: foo - * ``` - * - * @param {string} name - * The name to add call the new logger - * @return {Object} - */ - - log.createLogger = function (subname) { - return createLogger(name + ': ' + subname); - }; - /** - * Enumeration of available logging levels, where the keys are the level names - * and the values are `|`-separated strings containing logging methods allowed - * in that logging level. These strings are used to create a regular expression - * matching the function name being called. - * - * Levels provided by Video.js are: - * - * - `off`: Matches no calls. Any value that can be cast to `false` will have - * this effect. The most restrictive. - * - `all`: Matches only Video.js-provided functions (`debug`, `log`, - * `log.warn`, and `log.error`). - * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls. - * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls. - * - `warn`: Matches `log.warn` and `log.error` calls. - * - `error`: Matches only `log.error` calls. - * - * @type {Object} - */ - - - log.levels = { - all: 'debug|log|warn|error', - off: '', - debug: 'debug|log|warn|error', - info: 'log|warn|error', - warn: 'warn|error', - error: 'error', - DEFAULT: level - }; - /** - * Get or set the current logging level. - * - * If a string matching a key from {@link module:log.levels} is provided, acts - * as a setter. - * - * @param {string} [lvl] - * Pass a valid level to set a new logging level. - * - * @return {string} - * The current logging level. - */ - - log.level = function (lvl) { - if (typeof lvl === 'string') { - if (!log.levels.hasOwnProperty(lvl)) { - throw new Error("\"" + lvl + "\" in not a valid log level"); - } - - level = lvl; - } - - return level; - }; - /** - * Returns an array containing everything that has been logged to the history. - * - * This array is a shallow clone of the internal history record. However, its - * contents are _not_ cloned; so, mutating objects inside this array will - * mutate them in history. - * - * @return {Array} - */ - - - log.history = function () { - return history ? [].concat(history) : []; - }; - /** - * Allows you to filter the history by the given logger name - * - * @param {string} fname - * The name to filter by - * - * @return {Array} - * The filtered list to return - */ - - - log.history.filter = function (fname) { - return (history || []).filter(function (historyItem) { - // if the first item in each historyItem includes `fname`, then it's a match - return new RegExp(".*" + fname + ".*").test(historyItem[0]); - }); - }; - /** - * Clears the internal history tracking, but does not prevent further history - * tracking. - */ - - - log.history.clear = function () { - if (history) { - history.length = 0; - } - }; - /** - * Disable history tracking if it is currently enabled. - */ - - - log.history.disable = function () { - if (history !== null) { - history.length = 0; - history = null; - } - }; - /** - * Enable history tracking if it is currently disabled. - */ - - - log.history.enable = function () { - if (history === null) { - history = []; - } - }; - /** - * Logs error messages. Similar to `console.error`. - * - * @param {Mixed[]} args - * One or more messages or objects that should be logged as an error - */ - - - log.error = function () { - for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { - args[_key2] = arguments[_key2]; - } - - return logByType('error', level, args); - }; - /** - * Logs warning messages. Similar to `console.warn`. - * - * @param {Mixed[]} args - * One or more messages or objects that should be logged as a warning. - */ - - - log.warn = function () { - for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { - args[_key3] = arguments[_key3]; - } - - return logByType('warn', level, args); - }; - /** - * Logs debug messages. Similar to `console.debug`, but may also act as a comparable - * log if `console.debug` is not available - * - * @param {Mixed[]} args - * One or more messages or objects that should be logged as debug. - */ - - - log.debug = function () { - for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { - args[_key4] = arguments[_key4]; - } - - return logByType('debug', level, args); - }; - - return log; -} - -/** - * @file log.js - * @module log - */ -var log = createLogger('VIDEOJS'); -var createLogger$1 = log.createLogger; - -/** - * @file obj.js - * @module obj - */ - -/** - * @callback obj:EachCallback - * - * @param {Mixed} value - * The current key for the object that is being iterated over. - * - * @param {string} key - * The current key-value for object that is being iterated over - */ - -/** - * @callback obj:ReduceCallback - * - * @param {Mixed} accum - * The value that is accumulating over the reduce loop. - * - * @param {Mixed} value - * The current key for the object that is being iterated over. - * - * @param {string} key - * The current key-value for object that is being iterated over - * - * @return {Mixed} - * The new accumulated value. - */ -var toString$1 = Object.prototype.toString; -/** - * Get the keys of an Object - * - * @param {Object} - * The Object to get the keys from - * - * @return {string[]} - * An array of the keys from the object. Returns an empty array if the - * object passed in was invalid or had no keys. - * - * @private - */ - -var keys = function keys(object) { - return isObject$1(object) ? Object.keys(object) : []; -}; -/** - * Array-like iteration for objects. - * - * @param {Object} object - * The object to iterate over - * - * @param {obj:EachCallback} fn - * The callback function which is called for each key in the object. - */ - - -function each(object, fn) { - keys(object).forEach(function (key) { - return fn(object[key], key); - }); -} -/** - * Array-like reduce for objects. - * - * @param {Object} object - * The Object that you want to reduce. - * - * @param {Function} fn - * A callback function which is called for each key in the object. It - * receives the accumulated value and the per-iteration value and key - * as arguments. - * - * @param {Mixed} [initial = 0] - * Starting value - * - * @return {Mixed} - * The final accumulated value. - */ - -function reduce(object, fn, initial) { - if (initial === void 0) { - initial = 0; - } - - return keys(object).reduce(function (accum, key) { - return fn(accum, object[key], key); - }, initial); -} -/** - * Object.assign-style object shallow merge/extend. - * - * @param {Object} target - * @param {Object} ...sources - * @return {Object} - */ - -function assign(target) { - for (var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - sources[_key - 1] = arguments[_key]; - } - - if (Object.assign) { - return _extends_1.apply(void 0, [target].concat(sources)); - } - - sources.forEach(function (source) { - if (!source) { - return; - } - - each(source, function (value, key) { - target[key] = value; - }); - }); - return target; -} -/** - * Returns whether a value is an object of any kind - including DOM nodes, - * arrays, regular expressions, etc. Not functions, though. - * - * This avoids the gotcha where using `typeof` on a `null` value - * results in `'object'`. - * - * @param {Object} value - * @return {boolean} - */ - -function isObject$1(value) { - return !!value && typeof value === 'object'; -} -/** - * Returns whether an object appears to be a "plain" object - that is, a - * direct instance of `Object`. - * - * @param {Object} value - * @return {boolean} - */ - -function isPlain(value) { - return isObject$1(value) && toString$1.call(value) === '[object Object]' && value.constructor === Object; -} - -/** - * @file computed-style.js - * @module computed-style - */ -/** - * A safe getComputedStyle. - * - * This is needed because in Firefox, if the player is loaded in an iframe with - * `display:none`, then `getComputedStyle` returns `null`, so, we do a - * null-check to make sure that the player doesn't break in these cases. - * - * @function - * @param {Element} el - * The element you want the computed style of - * - * @param {string} prop - * The property name you want - * - * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 - */ - -function computedStyle(el, prop) { - if (!el || !prop) { - return ''; - } - - if (typeof window_1$1.getComputedStyle === 'function') { - var computedStyleValue = window_1$1.getComputedStyle(el); - return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : ''; - } - - return ''; -} - -/** - * @file dom.js - * @module dom - */ -/** - * Detect if a value is a string with any non-whitespace characters. - * - * @private - * @param {string} str - * The string to check - * - * @return {boolean} - * Will be `true` if the string is non-blank, `false` otherwise. - * - */ - -function isNonBlankString(str) { - // we use str.trim as it will trim any whitespace characters - // from the front or back of non-whitespace characters. aka - // Any string that contains non-whitespace characters will - // still contain them after `trim` but whitespace only strings - // will have a length of 0, failing this check. - return typeof str === 'string' && Boolean(str.trim()); -} -/** - * Throws an error if the passed string has whitespace. This is used by - * class methods to be relatively consistent with the classList API. - * - * @private - * @param {string} str - * The string to check for whitespace. - * - * @throws {Error} - * Throws an error if there is whitespace in the string. - */ - - -function throwIfWhitespace(str) { - // str.indexOf instead of regex because str.indexOf is faster performance wise. - if (str.indexOf(' ') >= 0) { - throw new Error('class has illegal whitespace characters'); - } -} -/** - * Produce a regular expression for matching a className within an elements className. - * - * @private - * @param {string} className - * The className to generate the RegExp for. - * - * @return {RegExp} - * The RegExp that will check for a specific `className` in an elements - * className. - */ - - -function classRegExp(className) { - return new RegExp('(^|\\s)' + className + '($|\\s)'); -} -/** - * Whether the current DOM interface appears to be real (i.e. not simulated). - * - * @return {boolean} - * Will be `true` if the DOM appears to be real, `false` otherwise. - */ - - -function isReal() { - // Both document and window will never be undefined thanks to `global`. - return document_1 === window_1$1.document; -} -/** - * Determines, via duck typing, whether or not a value is a DOM element. - * - * @param {Mixed} value - * The value to check. - * - * @return {boolean} - * Will be `true` if the value is a DOM element, `false` otherwise. - */ - -function isEl(value) { - return isObject$1(value) && value.nodeType === 1; -} -/** - * Determines if the current DOM is embedded in an iframe. - * - * @return {boolean} - * Will be `true` if the DOM is embedded in an iframe, `false` - * otherwise. - */ - -function isInFrame() { - // We need a try/catch here because Safari will throw errors when attempting - // to get either `parent` or `self` - try { - return window_1$1.parent !== window_1$1.self; - } catch (x) { - return true; - } -} -/** - * Creates functions to query the DOM using a given method. - * - * @private - * @param {string} method - * The method to create the query with. - * - * @return {Function} - * The query method - */ - -function createQuerier(method) { - return function (selector, context) { - if (!isNonBlankString(selector)) { - return document_1[method](null); - } - - if (isNonBlankString(context)) { - context = document_1.querySelector(context); - } - - var ctx = isEl(context) ? context : document_1; - return ctx[method] && ctx[method](selector); - }; -} -/** - * Creates an element and applies properties, attributes, and inserts content. - * - * @param {string} [tagName='div'] - * Name of tag to be created. - * - * @param {Object} [properties={}] - * Element properties to be applied. - * - * @param {Object} [attributes={}] - * Element attributes to be applied. - * - * @param {module:dom~ContentDescriptor} content - * A content descriptor object. - * - * @return {Element} - * The element that was created. - */ - - -function createEl(tagName, properties, attributes, content) { - if (tagName === void 0) { - tagName = 'div'; - } - - if (properties === void 0) { - properties = {}; - } - - if (attributes === void 0) { - attributes = {}; - } - - var el = document_1.createElement(tagName); - Object.getOwnPropertyNames(properties).forEach(function (propName) { - var val = properties[propName]; // See #2176 - // We originally were accepting both properties and attributes in the - // same object, but that doesn't work so well. - - if (propName.indexOf('aria-') !== -1 || propName === 'role' || propName === 'type') { - log.warn('Setting attributes in the second argument of createEl()\n' + 'has been deprecated. Use the third argument instead.\n' + ("createEl(type, properties, attributes). Attempting to set " + propName + " to " + val + ".")); - el.setAttribute(propName, val); // Handle textContent since it's not supported everywhere and we have a - // method for it. - } else if (propName === 'textContent') { - textContent(el, val); - } else if (el[propName] !== val) { - el[propName] = val; - } - }); - Object.getOwnPropertyNames(attributes).forEach(function (attrName) { - el.setAttribute(attrName, attributes[attrName]); - }); - - if (content) { - appendContent(el, content); - } - - return el; -} -/** - * Injects text into an element, replacing any existing contents entirely. - * - * @param {Element} el - * The element to add text content into - * - * @param {string} text - * The text content to add. - * - * @return {Element} - * The element with added text content. - */ - -function textContent(el, text) { - if (typeof el.textContent === 'undefined') { - el.innerText = text; - } else { - el.textContent = text; - } - - return el; -} -/** - * Insert an element as the first child node of another - * - * @param {Element} child - * Element to insert - * - * @param {Element} parent - * Element to insert child into - */ - -function prependTo(child, parent) { - if (parent.firstChild) { - parent.insertBefore(child, parent.firstChild); - } else { - parent.appendChild(child); - } -} -/** - * Check if an element has a class name. - * - * @param {Element} element - * Element to check - * - * @param {string} classToCheck - * Class name to check for - * - * @return {boolean} - * Will be `true` if the element has a class, `false` otherwise. - * - * @throws {Error} - * Throws an error if `classToCheck` has white space. - */ - -function hasClass(element, classToCheck) { - throwIfWhitespace(classToCheck); - - if (element.classList) { - return element.classList.contains(classToCheck); - } - - return classRegExp(classToCheck).test(element.className); -} -/** - * Add a class name to an element. - * - * @param {Element} element - * Element to add class name to. - * - * @param {string} classToAdd - * Class name to add. - * - * @return {Element} - * The DOM element with the added class name. - */ - -function addClass(element, classToAdd) { - if (element.classList) { - element.classList.add(classToAdd); // Don't need to `throwIfWhitespace` here because `hasElClass` will do it - // in the case of classList not being supported. - } else if (!hasClass(element, classToAdd)) { - element.className = (element.className + ' ' + classToAdd).trim(); - } - - return element; -} -/** - * Remove a class name from an element. - * - * @param {Element} element - * Element to remove a class name from. - * - * @param {string} classToRemove - * Class name to remove - * - * @return {Element} - * The DOM element with class name removed. - */ - -function removeClass(element, classToRemove) { - if (element.classList) { - element.classList.remove(classToRemove); - } else { - throwIfWhitespace(classToRemove); - element.className = element.className.split(/\s+/).filter(function (c) { - return c !== classToRemove; - }).join(' '); - } - - return element; -} -/** - * The callback definition for toggleClass. - * - * @callback module:dom~PredicateCallback - * @param {Element} element - * The DOM element of the Component. - * - * @param {string} classToToggle - * The `className` that wants to be toggled - * - * @return {boolean|undefined} - * If `true` is returned, the `classToToggle` will be added to the - * `element`. If `false`, the `classToToggle` will be removed from - * the `element`. If `undefined`, the callback will be ignored. - */ - -/** - * Adds or removes a class name to/from an element depending on an optional - * condition or the presence/absence of the class name. - * - * @param {Element} element - * The element to toggle a class name on. - * - * @param {string} classToToggle - * The class that should be toggled. - * - * @param {boolean|module:dom~PredicateCallback} [predicate] - * See the return value for {@link module:dom~PredicateCallback} - * - * @return {Element} - * The element with a class that has been toggled. - */ - -function toggleClass(element, classToToggle, predicate) { - // This CANNOT use `classList` internally because IE11 does not support the - // second parameter to the `classList.toggle()` method! Which is fine because - // `classList` will be used by the add/remove functions. - var has = hasClass(element, classToToggle); - - if (typeof predicate === 'function') { - predicate = predicate(element, classToToggle); - } - - if (typeof predicate !== 'boolean') { - predicate = !has; - } // If the necessary class operation matches the current state of the - // element, no action is required. - - - if (predicate === has) { - return; - } - - if (predicate) { - addClass(element, classToToggle); - } else { - removeClass(element, classToToggle); - } - - return element; -} -/** - * Apply attributes to an HTML element. - * - * @param {Element} el - * Element to add attributes to. - * - * @param {Object} [attributes] - * Attributes to be applied. - */ - -function setAttributes(el, attributes) { - Object.getOwnPropertyNames(attributes).forEach(function (attrName) { - var attrValue = attributes[attrName]; - - if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) { - el.removeAttribute(attrName); - } else { - el.setAttribute(attrName, attrValue === true ? '' : attrValue); - } - }); -} -/** - * Get an element's attribute values, as defined on the HTML tag. - * - * Attributes are not the same as properties. They're defined on the tag - * or with setAttribute. - * - * @param {Element} tag - * Element from which to get tag attributes. - * - * @return {Object} - * All attributes of the element. Boolean attributes will be `true` or - * `false`, others will be strings. - */ - -function getAttributes(tag) { - var obj = {}; // known boolean attributes - // we can check for matching boolean properties, but not all browsers - // and not all tags know about these attributes, so, we still want to check them manually - - var knownBooleans = ',' + 'autoplay,controls,playsinline,loop,muted,default,defaultMuted' + ','; - - if (tag && tag.attributes && tag.attributes.length > 0) { - var attrs = tag.attributes; - - for (var i = attrs.length - 1; i >= 0; i--) { - var attrName = attrs[i].name; - var attrVal = attrs[i].value; // check for known booleans - // the matching element property will return a value for typeof - - if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(',' + attrName + ',') !== -1) { - // the value of an included boolean attribute is typically an empty - // string ('') which would equal false if we just check for a false value. - // we also don't want support bad code like autoplay='false' - attrVal = attrVal !== null ? true : false; - } - - obj[attrName] = attrVal; - } - } - - return obj; -} -/** - * Get the value of an element's attribute. - * - * @param {Element} el - * A DOM element. - * - * @param {string} attribute - * Attribute to get the value of. - * - * @return {string} - * The value of the attribute. - */ - -function getAttribute(el, attribute) { - return el.getAttribute(attribute); -} -/** - * Set the value of an element's attribute. - * - * @param {Element} el - * A DOM element. - * - * @param {string} attribute - * Attribute to set. - * - * @param {string} value - * Value to set the attribute to. - */ - -function setAttribute(el, attribute, value) { - el.setAttribute(attribute, value); -} -/** - * Remove an element's attribute. - * - * @param {Element} el - * A DOM element. - * - * @param {string} attribute - * Attribute to remove. - */ - -function removeAttribute(el, attribute) { - el.removeAttribute(attribute); -} -/** - * Attempt to block the ability to select text. - */ - -function blockTextSelection() { - document_1.body.focus(); - - document_1.onselectstart = function () { - return false; - }; -} -/** - * Turn off text selection blocking. - */ - -function unblockTextSelection() { - document_1.onselectstart = function () { - return true; - }; -} -/** - * Identical to the native `getBoundingClientRect` function, but ensures that - * the method is supported at all (it is in all browsers we claim to support) - * and that the element is in the DOM before continuing. - * - * This wrapper function also shims properties which are not provided by some - * older browsers (namely, IE8). - * - * Additionally, some browsers do not support adding properties to a - * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard - * properties (except `x` and `y` which are not widely supported). This helps - * avoid implementations where keys are non-enumerable. - * - * @param {Element} el - * Element whose `ClientRect` we want to calculate. - * - * @return {Object|undefined} - * Always returns a plain object - or `undefined` if it cannot. - */ - -function getBoundingClientRect(el) { - if (el && el.getBoundingClientRect && el.parentNode) { - var rect = el.getBoundingClientRect(); - var result = {}; - ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(function (k) { - if (rect[k] !== undefined) { - result[k] = rect[k]; - } - }); - - if (!result.height) { - result.height = parseFloat(computedStyle(el, 'height')); - } - - if (!result.width) { - result.width = parseFloat(computedStyle(el, 'width')); - } - - return result; - } -} -/** - * Represents the position of a DOM element on the page. - * - * @typedef {Object} module:dom~Position - * - * @property {number} left - * Pixels to the left. - * - * @property {number} top - * Pixels from the top. - */ - -/** - * Get the position of an element in the DOM. - * - * Uses `getBoundingClientRect` technique from John Resig. - * - * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/ - * - * @param {Element} el - * Element from which to get offset. - * - * @return {module:dom~Position} - * The position of the element that was passed in. - */ - -function findPosition(el) { - if (!el || el && !el.offsetParent) { - return { - left: 0, - top: 0, - width: 0, - height: 0 - }; - } - - var width = el.offsetWidth; - var height = el.offsetHeight; - var left = 0; - var top = 0; - - do { - left += el.offsetLeft; - top += el.offsetTop; - el = el.offsetParent; - } while (el); - - return { - left: left, - top: top, - width: width, - height: height - }; -} -/** - * Represents x and y coordinates for a DOM element or mouse pointer. - * - * @typedef {Object} module:dom~Coordinates - * - * @property {number} x - * x coordinate in pixels - * - * @property {number} y - * y coordinate in pixels - */ - -/** - * Get the pointer position within an element. - * - * The base on the coordinates are the bottom left of the element. - * - * @param {Element} el - * Element on which to get the pointer position on. - * - * @param {EventTarget~Event} event - * Event object. - * - * @return {module:dom~Coordinates} - * A coordinates object corresponding to the mouse position. - * - */ - -function getPointerPosition(el, event) { - var position = {}; - var boxTarget = findPosition(event.target); - var box = findPosition(el); - var boxW = box.width; - var boxH = box.height; - var offsetY = event.offsetY - (box.top - boxTarget.top); - var offsetX = event.offsetX - (box.left - boxTarget.left); - - if (event.changedTouches) { - offsetX = event.changedTouches[0].pageX - box.left; - offsetY = event.changedTouches[0].pageY + box.top; - } - - position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH)); - position.x = Math.max(0, Math.min(1, offsetX / boxW)); - return position; -} -/** - * Determines, via duck typing, whether or not a value is a text node. - * - * @param {Mixed} value - * Check if this value is a text node. - * - * @return {boolean} - * Will be `true` if the value is a text node, `false` otherwise. - */ - -function isTextNode(value) { - return isObject$1(value) && value.nodeType === 3; -} -/** - * Empties the contents of an element. - * - * @param {Element} el - * The element to empty children from - * - * @return {Element} - * The element with no children - */ - -function emptyEl(el) { - while (el.firstChild) { - el.removeChild(el.firstChild); - } - - return el; -} -/** - * This is a mixed value that describes content to be injected into the DOM - * via some method. It can be of the following types: - * - * Type | Description - * -----------|------------- - * `string` | The value will be normalized into a text node. - * `Element` | The value will be accepted as-is. - * `TextNode` | The value will be accepted as-is. - * `Array` | A one-dimensional array of strings, elements, text nodes, or functions. These functions should return a string, element, or text node (any other return value, like an array, will be ignored). - * `Function` | A function, which is expected to return a string, element, text node, or array - any of the other possible values described above. This means that a content descriptor could be a function that returns an array of functions, but those second-level functions must return strings, elements, or text nodes. - * - * @typedef {string|Element|TextNode|Array|Function} module:dom~ContentDescriptor - */ - -/** - * Normalizes content for eventual insertion into the DOM. - * - * This allows a wide range of content definition methods, but helps protect - * from falling into the trap of simply writing to `innerHTML`, which could - * be an XSS concern. - * - * The content for an element can be passed in multiple types and - * combinations, whose behavior is as follows: - * - * @param {module:dom~ContentDescriptor} content - * A content descriptor value. - * - * @return {Array} - * All of the content that was passed in, normalized to an array of - * elements or text nodes. - */ - -function normalizeContent(content) { - // First, invoke content if it is a function. If it produces an array, - // that needs to happen before normalization. - if (typeof content === 'function') { - content = content(); - } // Next up, normalize to an array, so one or many items can be normalized, - // filtered, and returned. - - - return (Array.isArray(content) ? content : [content]).map(function (value) { - // First, invoke value if it is a function to produce a new value, - // which will be subsequently normalized to a Node of some kind. - if (typeof value === 'function') { - value = value(); - } - - if (isEl(value) || isTextNode(value)) { - return value; - } - - if (typeof value === 'string' && /\S/.test(value)) { - return document_1.createTextNode(value); - } - }).filter(function (value) { - return value; - }); -} -/** - * Normalizes and appends content to an element. - * - * @param {Element} el - * Element to append normalized content to. - * - * @param {module:dom~ContentDescriptor} content - * A content descriptor value. - * - * @return {Element} - * The element with appended normalized content. - */ - -function appendContent(el, content) { - normalizeContent(content).forEach(function (node) { - return el.appendChild(node); - }); - return el; -} -/** - * Normalizes and inserts content into an element; this is identical to - * `appendContent()`, except it empties the element first. - * - * @param {Element} el - * Element to insert normalized content into. - * - * @param {module:dom~ContentDescriptor} content - * A content descriptor value. - * - * @return {Element} - * The element with inserted normalized content. - */ - -function insertContent(el, content) { - return appendContent(emptyEl(el), content); -} -/** - * Check if an event was a single left click. - * - * @param {EventTarget~Event} event - * Event object. - * - * @return {boolean} - * Will be `true` if a single left click, `false` otherwise. - */ - -function isSingleLeftClick(event) { - // Note: if you create something draggable, be sure to - // call it on both `mousedown` and `mousemove` event, - // otherwise `mousedown` should be enough for a button - if (event.button === undefined && event.buttons === undefined) { - // Why do we need `buttons` ? - // Because, middle mouse sometimes have this: - // e.button === 0 and e.buttons === 4 - // Furthermore, we want to prevent combination click, something like - // HOLD middlemouse then left click, that would be - // e.button === 0, e.buttons === 5 - // just `button` is not gonna work - // Alright, then what this block does ? - // this is for chrome `simulate mobile devices` - // I want to support this as well - return true; - } - - if (event.button === 0 && event.buttons === undefined) { - // Touch screen, sometimes on some specific device, `buttons` - // doesn't have anything (safari on ios, blackberry...) - return true; - } // `mouseup` event on a single left click has - // `button` and `buttons` equal to 0 - - - if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) { - return true; - } - - if (event.button !== 0 || event.buttons !== 1) { - // This is the reason we have those if else block above - // if any special case we can catch and let it slide - // we do it above, when get to here, this definitely - // is-not-left-click - return false; - } - - return true; -} -/** - * Finds a single DOM element matching `selector` within the optional - * `context` of another DOM element (defaulting to `document`). - * - * @param {string} selector - * A valid CSS selector, which will be passed to `querySelector`. - * - * @param {Element|String} [context=document] - * A DOM element within which to query. Can also be a selector - * string in which case the first matching element will be used - * as context. If missing (or no element matches selector), falls - * back to `document`. - * - * @return {Element|null} - * The element that was found or null. - */ - -var $ = createQuerier('querySelector'); -/** - * Finds a all DOM elements matching `selector` within the optional - * `context` of another DOM element (defaulting to `document`). - * - * @param {string} selector - * A valid CSS selector, which will be passed to `querySelectorAll`. - * - * @param {Element|String} [context=document] - * A DOM element within which to query. Can also be a selector - * string in which case the first matching element will be used - * as context. If missing (or no element matches selector), falls - * back to `document`. - * - * @return {NodeList} - * A element list of elements that were found. Will be empty if none - * were found. - * - */ - -var $$ = createQuerier('querySelectorAll'); - -var Dom = /*#__PURE__*/Object.freeze({ - __proto__: null, - isReal: isReal, - isEl: isEl, - isInFrame: isInFrame, - createEl: createEl, - textContent: textContent, - prependTo: prependTo, - hasClass: hasClass, - addClass: addClass, - removeClass: removeClass, - toggleClass: toggleClass, - setAttributes: setAttributes, - getAttributes: getAttributes, - getAttribute: getAttribute, - setAttribute: setAttribute, - removeAttribute: removeAttribute, - blockTextSelection: blockTextSelection, - unblockTextSelection: unblockTextSelection, - getBoundingClientRect: getBoundingClientRect, - findPosition: findPosition, - getPointerPosition: getPointerPosition, - isTextNode: isTextNode, - emptyEl: emptyEl, - normalizeContent: normalizeContent, - appendContent: appendContent, - insertContent: insertContent, - isSingleLeftClick: isSingleLeftClick, - $: $, - $$: $$ -}); - -/** - * @file setup.js - Functions for setting up a player without - * user interaction based on the data-setup `attribute` of the video tag. - * - * @module setup - */ -var _windowLoaded = false; -var videojs; -/** - * Set up any tags that have a data-setup `attribute` when the player is started. - */ - -var autoSetup = function autoSetup() { - // Protect against breakage in non-browser environments and check global autoSetup option. - if (!isReal() || videojs.options.autoSetup === false) { - return; - } - - var vids = Array.prototype.slice.call(document_1.getElementsByTagName('video')); - var audios = Array.prototype.slice.call(document_1.getElementsByTagName('audio')); - var divs = Array.prototype.slice.call(document_1.getElementsByTagName('video-js')); - var mediaEls = vids.concat(audios, divs); // Check if any media elements exist - - if (mediaEls && mediaEls.length > 0) { - for (var i = 0, e = mediaEls.length; i < e; i++) { - var mediaEl = mediaEls[i]; // Check if element exists, has getAttribute func. - - if (mediaEl && mediaEl.getAttribute) { - // Make sure this player hasn't already been set up. - if (mediaEl.player === undefined) { - var options = mediaEl.getAttribute('data-setup'); // Check if data-setup attr exists. - // We only auto-setup if they've added the data-setup attr. - - if (options !== null) { - // Create new video.js instance. - videojs(mediaEl); - } - } // If getAttribute isn't defined, we need to wait for the DOM. - - } else { - autoSetupTimeout(1); - break; - } - } // No videos were found, so keep looping unless page is finished loading. - - } else if (!_windowLoaded) { - autoSetupTimeout(1); - } -}; -/** - * Wait until the page is loaded before running autoSetup. This will be called in - * autoSetup if `hasLoaded` returns false. - * - * @param {number} wait - * How long to wait in ms - * - * @param {module:videojs} [vjs] - * The videojs library function - */ - - -function autoSetupTimeout(wait, vjs) { - if (vjs) { - videojs = vjs; - } - - window_1$1.setTimeout(autoSetup, wait); -} -/** - * Used to set the internal tracking of window loaded state to true. - * - * @private - */ - - -function setWindowLoaded() { - _windowLoaded = true; - window_1$1.removeEventListener('load', setWindowLoaded); -} - -if (isReal()) { - if (document_1.readyState === 'complete') { - setWindowLoaded(); - } else { - /** - * Listen for the load event on window, and set _windowLoaded to true. - * - * We use a standard event listener here to avoid incrementing the GUID - * before any players are created. - * - * @listens load - */ - window_1$1.addEventListener('load', setWindowLoaded); - } -} - -/** - * @file stylesheet.js - * @module stylesheet - */ -/** - * Create a DOM syle element given a className for it. - * - * @param {string} className - * The className to add to the created style element. - * - * @return {Element} - * The element that was created. - */ - -var createStyleElement = function createStyleElement(className) { - var style = document_1.createElement('style'); - style.className = className; - return style; -}; -/** - * Add text to a DOM element. - * - * @param {Element} el - * The Element to add text content to. - * - * @param {string} content - * The text to add to the element. - */ - -var setTextContent = function setTextContent(el, content) { - if (el.styleSheet) { - el.styleSheet.cssText = content; - } else { - el.textContent = content; - } -}; - -/** - * @file guid.js - * @module guid - */ -// Default value for GUIDs. This allows us to reset the GUID counter in tests. -// -// The initial GUID is 3 because some users have come to rely on the first -// default player ID ending up as `vjs_video_3`. -// -// See: https://github.com/videojs/video.js/pull/6216 -var _initialGuid = 3; -/** - * Unique ID for an element or function - * - * @type {Number} - */ - -var _guid = _initialGuid; -/** - * Get a unique auto-incrementing ID by number that has not been returned before. - * - * @return {number} - * A new unique ID. - */ - -function newGUID() { - return _guid++; -} - -/** - * @file dom-data.js - * @module dom-data - */ -var FakeWeakMap; - -if (!window_1$1.WeakMap) { - FakeWeakMap = /*#__PURE__*/function () { - function FakeWeakMap() { - this.vdata = 'vdata' + Math.floor(window_1$1.performance && window_1$1.performance.now() || Date.now()); - this.data = {}; - } - - var _proto = FakeWeakMap.prototype; - - _proto.set = function set(key, value) { - var access = key[this.vdata] || newGUID(); - - if (!key[this.vdata]) { - key[this.vdata] = access; - } - - this.data[access] = value; - return this; - }; - - _proto.get = function get(key) { - var access = key[this.vdata]; // we have data, return it - - if (access) { - return this.data[access]; - } // we don't have data, return nothing. - // return undefined explicitly as that's the contract for this method - - - log('We have no data for this element', key); - return undefined; - }; - - _proto.has = function has(key) { - var access = key[this.vdata]; - return access in this.data; - }; - - _proto["delete"] = function _delete(key) { - var access = key[this.vdata]; - - if (access) { - delete this.data[access]; - delete key[this.vdata]; - } - }; - - return FakeWeakMap; - }(); -} -/** - * Element Data Store. - * - * Allows for binding data to an element without putting it directly on the - * element. Ex. Event listeners are stored here. - * (also from jsninja.com, slightly modified and updated for closure compiler) - * - * @type {Object} - * @private - */ - - -var DomData = window_1$1.WeakMap ? new WeakMap() : new FakeWeakMap(); - -/** - * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/) - * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible) - * This should work very similarly to jQuery's events, however it's based off the book version which isn't as - * robust as jquery's, so there's probably some differences. - * - * @file events.js - * @module events - */ -/** - * Clean up the listener cache and dispatchers - * - * @param {Element|Object} elem - * Element to clean up - * - * @param {string} type - * Type of event to clean up - */ - -function _cleanUpEvents(elem, type) { - if (!DomData.has(elem)) { - return; - } - - var data = DomData.get(elem); // Remove the events of a particular type if there are none left - - if (data.handlers[type].length === 0) { - delete data.handlers[type]; // data.handlers[type] = null; - // Setting to null was causing an error with data.handlers - // Remove the meta-handler from the element - - if (elem.removeEventListener) { - elem.removeEventListener(type, data.dispatcher, false); - } else if (elem.detachEvent) { - elem.detachEvent('on' + type, data.dispatcher); - } - } // Remove the events object if there are no types left - - - if (Object.getOwnPropertyNames(data.handlers).length <= 0) { - delete data.handlers; - delete data.dispatcher; - delete data.disabled; - } // Finally remove the element data if there is no data left - - - if (Object.getOwnPropertyNames(data).length === 0) { - DomData["delete"](elem); - } -} -/** - * Loops through an array of event types and calls the requested method for each type. - * - * @param {Function} fn - * The event method we want to use. - * - * @param {Element|Object} elem - * Element or object to bind listeners to - * - * @param {string} type - * Type of event to bind to. - * - * @param {EventTarget~EventListener} callback - * Event listener. - */ - - -function _handleMultipleEvents(fn, elem, types, callback) { - types.forEach(function (type) { - // Call the event method for each one of the types - fn(elem, type, callback); - }); -} -/** - * Fix a native event to have standard property values - * - * @param {Object} event - * Event object to fix. - * - * @return {Object} - * Fixed event object. - */ - - -function fixEvent(event) { - if (event.fixed_) { - return event; - } - - function returnTrue() { - return true; - } - - function returnFalse() { - return false; - } // Test if fixing up is needed - // Used to check if !event.stopPropagation instead of isPropagationStopped - // But native events return true for stopPropagation, but don't have - // other expected methods like isPropagationStopped. Seems to be a problem - // with the Javascript Ninja code. So we're just overriding all events now. - - - if (!event || !event.isPropagationStopped) { - var old = event || window_1$1.event; - event = {}; // Clone the old object so that we can modify the values event = {}; - // IE8 Doesn't like when you mess with native event properties - // Firefox returns false for event.hasOwnProperty('type') and other props - // which makes copying more difficult. - // TODO: Probably best to create a whitelist of event props - - for (var key in old) { - // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y - // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation - // and webkitMovementX/Y - if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY') { - // Chrome 32+ warns if you try to copy deprecated returnValue, but - // we still want to if preventDefault isn't supported (IE8). - if (!(key === 'returnValue' && old.preventDefault)) { - event[key] = old[key]; - } - } - } // The event occurred on this element - - - if (!event.target) { - event.target = event.srcElement || document_1; - } // Handle which other element the event is related to - - - if (!event.relatedTarget) { - event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; - } // Stop the default browser action - - - event.preventDefault = function () { - if (old.preventDefault) { - old.preventDefault(); - } - - event.returnValue = false; - old.returnValue = false; - event.defaultPrevented = true; - }; - - event.defaultPrevented = false; // Stop the event from bubbling - - event.stopPropagation = function () { - if (old.stopPropagation) { - old.stopPropagation(); - } - - event.cancelBubble = true; - old.cancelBubble = true; - event.isPropagationStopped = returnTrue; - }; - - event.isPropagationStopped = returnFalse; // Stop the event from bubbling and executing other handlers - - event.stopImmediatePropagation = function () { - if (old.stopImmediatePropagation) { - old.stopImmediatePropagation(); - } - - event.isImmediatePropagationStopped = returnTrue; - event.stopPropagation(); - }; - - event.isImmediatePropagationStopped = returnFalse; // Handle mouse position - - if (event.clientX !== null && event.clientX !== undefined) { - var doc = document_1.documentElement; - var body = document_1.body; - event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); - event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); - } // Handle key presses - - - event.which = event.charCode || event.keyCode; // Fix button for mouse clicks: - // 0 == left; 1 == middle; 2 == right - - if (event.button !== null && event.button !== undefined) { - // The following is disabled because it does not pass videojs-standard - // and... yikes. - - /* eslint-disable */ - event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0; - /* eslint-enable */ - } - } - - event.fixed_ = true; // Returns fixed-up instance - - return event; -} -/** - * Whether passive event listeners are supported - */ - -var _supportsPassive; - -var supportsPassive = function supportsPassive() { - if (typeof _supportsPassive !== 'boolean') { - _supportsPassive = false; - - try { - var opts = Object.defineProperty({}, 'passive', { - get: function get() { - _supportsPassive = true; - } - }); - window_1$1.addEventListener('test', null, opts); - window_1$1.removeEventListener('test', null, opts); - } catch (e) {// disregard - } - } - - return _supportsPassive; -}; -/** - * Touch events Chrome expects to be passive - */ - - -var passiveEvents = ['touchstart', 'touchmove']; -/** - * Add an event listener to element - * It stores the handler function in a separate cache object - * and adds a generic handler to the element's event, - * along with a unique id (guid) to the element. - * - * @param {Element|Object} elem - * Element or object to bind listeners to - * - * @param {string|string[]} type - * Type of event to bind to. - * - * @param {EventTarget~EventListener} fn - * Event listener. - */ - -function on(elem, type, fn) { - if (Array.isArray(type)) { - return _handleMultipleEvents(on, elem, type, fn); - } - - if (!DomData.has(elem)) { - DomData.set(elem, {}); - } - - var data = DomData.get(elem); // We need a place to store all our handler data - - if (!data.handlers) { - data.handlers = {}; - } - - if (!data.handlers[type]) { - data.handlers[type] = []; - } - - if (!fn.guid) { - fn.guid = newGUID(); - } - - data.handlers[type].push(fn); - - if (!data.dispatcher) { - data.disabled = false; - - data.dispatcher = function (event, hash) { - if (data.disabled) { - return; - } - - event = fixEvent(event); - var handlers = data.handlers[event.type]; - - if (handlers) { - // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off. - var handlersCopy = handlers.slice(0); - - for (var m = 0, n = handlersCopy.length; m < n; m++) { - if (event.isImmediatePropagationStopped()) { - break; - } else { - try { - handlersCopy[m].call(elem, event, hash); - } catch (e) { - log.error(e); - } - } - } - } - }; - } - - if (data.handlers[type].length === 1) { - if (elem.addEventListener) { - var options = false; - - if (supportsPassive() && passiveEvents.indexOf(type) > -1) { - options = { - passive: true - }; - } - - elem.addEventListener(type, data.dispatcher, options); - } else if (elem.attachEvent) { - elem.attachEvent('on' + type, data.dispatcher); - } - } -} -/** - * Removes event listeners from an element - * - * @param {Element|Object} elem - * Object to remove listeners from. - * - * @param {string|string[]} [type] - * Type of listener to remove. Don't include to remove all events from element. - * - * @param {EventTarget~EventListener} [fn] - * Specific listener to remove. Don't include to remove listeners for an event - * type. - */ - -function off(elem, type, fn) { - // Don't want to add a cache object through getElData if not needed - if (!DomData.has(elem)) { - return; - } - - var data = DomData.get(elem); // If no events exist, nothing to unbind - - if (!data.handlers) { - return; - } - - if (Array.isArray(type)) { - return _handleMultipleEvents(off, elem, type, fn); - } // Utility function - - - var removeType = function removeType(el, t) { - data.handlers[t] = []; - - _cleanUpEvents(el, t); - }; // Are we removing all bound events? - - - if (type === undefined) { - for (var t in data.handlers) { - if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) { - removeType(elem, t); - } - } - - return; - } - - var handlers = data.handlers[type]; // If no handlers exist, nothing to unbind - - if (!handlers) { - return; - } // If no listener was provided, remove all listeners for type - - - if (!fn) { - removeType(elem, type); - return; - } // We're only removing a single handler - - - if (fn.guid) { - for (var n = 0; n < handlers.length; n++) { - if (handlers[n].guid === fn.guid) { - handlers.splice(n--, 1); - } - } - } - - _cleanUpEvents(elem, type); -} -/** - * Trigger an event for an element - * - * @param {Element|Object} elem - * Element to trigger an event on - * - * @param {EventTarget~Event|string} event - * A string (the type) or an event object with a type attribute - * - * @param {Object} [hash] - * data hash to pass along with the event - * - * @return {boolean|undefined} - * Returns the opposite of `defaultPrevented` if default was - * prevented. Otherwise, returns `undefined` - */ - -function trigger(elem, event, hash) { - // Fetches element data and a reference to the parent (for bubbling). - // Don't want to add a data object to cache for every parent, - // so checking hasElData first. - var elemData = DomData.has(elem) ? DomData.get(elem) : {}; - var parent = elem.parentNode || elem.ownerDocument; // type = event.type || event, - // handler; - // If an event name was passed as a string, creates an event out of it - - if (typeof event === 'string') { - event = { - type: event, - target: elem - }; - } else if (!event.target) { - event.target = elem; - } // Normalizes the event properties. - - - event = fixEvent(event); // If the passed element has a dispatcher, executes the established handlers. - - if (elemData.dispatcher) { - elemData.dispatcher.call(elem, event, hash); - } // Unless explicitly stopped or the event does not bubble (e.g. media events) - // recursively calls this function to bubble the event up the DOM. - - - if (parent && !event.isPropagationStopped() && event.bubbles === true) { - trigger.call(null, parent, event, hash); // If at the top of the DOM, triggers the default action unless disabled. - } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) { - if (!DomData.has(event.target)) { - DomData.set(event.target, {}); - } - - var targetData = DomData.get(event.target); // Checks if the target has a default action for this event. - - if (event.target[event.type]) { - // Temporarily disables event dispatching on the target as we have already executed the handler. - targetData.disabled = true; // Executes the default action. - - if (typeof event.target[event.type] === 'function') { - event.target[event.type](); - } // Re-enables event dispatching. - - - targetData.disabled = false; - } - } // Inform the triggerer if the default was prevented by returning false - - - return !event.defaultPrevented; -} -/** - * Trigger a listener only once for an event. - * - * @param {Element|Object} elem - * Element or object to bind to. - * - * @param {string|string[]} type - * Name/type of event - * - * @param {Event~EventListener} fn - * Event listener function - */ - -function one(elem, type, fn) { - if (Array.isArray(type)) { - return _handleMultipleEvents(one, elem, type, fn); - } - - var func = function func() { - off(elem, type, func); - fn.apply(this, arguments); - }; // copy the guid to the new function so it can removed using the original function's ID - - - func.guid = fn.guid = fn.guid || newGUID(); - on(elem, type, func); -} -/** - * Trigger a listener only once and then turn if off for all - * configured events - * - * @param {Element|Object} elem - * Element or object to bind to. - * - * @param {string|string[]} type - * Name/type of event - * - * @param {Event~EventListener} fn - * Event listener function - */ - -function any(elem, type, fn) { - var func = function func() { - off(elem, type, func); - fn.apply(this, arguments); - }; // copy the guid to the new function so it can removed using the original function's ID - - - func.guid = fn.guid = fn.guid || newGUID(); // multiple ons, but one off for everything - - on(elem, type, func); -} - -var Events = /*#__PURE__*/Object.freeze({ - __proto__: null, - fixEvent: fixEvent, - on: on, - off: off, - trigger: trigger, - one: one, - any: any -}); - -/** - * @file fn.js - * @module fn - */ -var UPDATE_REFRESH_INTERVAL = 30; -/** - * Bind (a.k.a proxy or context). A simple method for changing the context of - * a function. - * - * It also stores a unique id on the function so it can be easily removed from - * events. - * - * @function - * @param {Mixed} context - * The object to bind as scope. - * - * @param {Function} fn - * The function to be bound to a scope. - * - * @param {number} [uid] - * An optional unique ID for the function to be set - * - * @return {Function} - * The new function that will be bound into the context given - */ - -var bind = function bind(context, fn, uid) { - // Make sure the function has a unique ID - if (!fn.guid) { - fn.guid = newGUID(); - } // Create the new function that changes the context - - - var bound = fn.bind(context); // Allow for the ability to individualize this function - // Needed in the case where multiple objects might share the same prototype - // IF both items add an event listener with the same function, then you try to remove just one - // it will remove both because they both have the same guid. - // when using this, you need to use the bind method when you remove the listener as well. - // currently used in text tracks - - bound.guid = uid ? uid + '_' + fn.guid : fn.guid; - return bound; -}; -/** - * Wraps the given function, `fn`, with a new function that only invokes `fn` - * at most once per every `wait` milliseconds. - * - * @function - * @param {Function} fn - * The function to be throttled. - * - * @param {number} wait - * The number of milliseconds by which to throttle. - * - * @return {Function} - */ - -var throttle = function throttle(fn, wait) { - var last = window_1$1.performance.now(); - - var throttled = function throttled() { - var now = window_1$1.performance.now(); - - if (now - last >= wait) { - fn.apply(void 0, arguments); - last = now; - } - }; - - return throttled; -}; -/** - * Creates a debounced function that delays invoking `func` until after `wait` - * milliseconds have elapsed since the last time the debounced function was - * invoked. - * - * Inspired by lodash and underscore implementations. - * - * @function - * @param {Function} func - * The function to wrap with debounce behavior. - * - * @param {number} wait - * The number of milliseconds to wait after the last invocation. - * - * @param {boolean} [immediate] - * Whether or not to invoke the function immediately upon creation. - * - * @param {Object} [context=window] - * The "context" in which the debounced function should debounce. For - * example, if this function should be tied to a Video.js player, - * the player can be passed here. Alternatively, defaults to the - * global `window` object. - * - * @return {Function} - * A debounced function. - */ - -var debounce = function debounce(func, wait, immediate, context) { - if (context === void 0) { - context = window_1$1; - } - - var timeout; - - var cancel = function cancel() { - context.clearTimeout(timeout); - timeout = null; - }; - /* eslint-disable consistent-this */ - - - var debounced = function debounced() { - var self = this; - var args = arguments; - - var _later = function later() { - timeout = null; - _later = null; - - if (!immediate) { - func.apply(self, args); - } - }; - - if (!timeout && immediate) { - func.apply(self, args); - } - - context.clearTimeout(timeout); - timeout = context.setTimeout(_later, wait); - }; - /* eslint-enable consistent-this */ - - - debounced.cancel = cancel; - return debounced; -}; - -/** - * @file src/js/event-target.js - */ -/** - * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It - * adds shorthand functions that wrap around lengthy functions. For example: - * the `on` function is a wrapper around `addEventListener`. - * - * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget} - * @class EventTarget - */ - -var EventTarget = function EventTarget() {}; -/** - * A Custom DOM event. - * - * @typedef {Object} EventTarget~Event - * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent} - */ - -/** - * All event listeners should follow the following format. - * - * @callback EventTarget~EventListener - * @this {EventTarget} - * - * @param {EventTarget~Event} event - * the event that triggered this function - * - * @param {Object} [hash] - * hash of data sent during the event - */ - -/** - * An object containing event names as keys and booleans as values. - * - * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger} - * will have extra functionality. See that function for more information. - * - * @property EventTarget.prototype.allowedEvents_ - * @private - */ - - -EventTarget.prototype.allowedEvents_ = {}; -/** - * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a - * function that will get called when an event with a certain name gets triggered. - * - * @param {string|string[]} type - * An event name or an array of event names. - * - * @param {EventTarget~EventListener} fn - * The function to call with `EventTarget`s - */ - -EventTarget.prototype.on = function (type, fn) { - // Remove the addEventListener alias before calling Events.on - // so we don't get into an infinite type loop - var ael = this.addEventListener; - - this.addEventListener = function () {}; - - on(this, type, fn); - this.addEventListener = ael; -}; -/** - * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic - * the standard DOM API. - * - * @function - * @see {@link EventTarget#on} - */ - - -EventTarget.prototype.addEventListener = EventTarget.prototype.on; -/** - * Removes an `event listener` for a specific event from an instance of `EventTarget`. - * This makes it so that the `event listener` will no longer get called when the - * named event happens. - * - * @param {string|string[]} type - * An event name or an array of event names. - * - * @param {EventTarget~EventListener} fn - * The function to remove. - */ - -EventTarget.prototype.off = function (type, fn) { - off(this, type, fn); -}; -/** - * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic - * the standard DOM API. - * - * @function - * @see {@link EventTarget#off} - */ - - -EventTarget.prototype.removeEventListener = EventTarget.prototype.off; -/** - * This function will add an `event listener` that gets triggered only once. After the - * first trigger it will get removed. This is like adding an `event listener` - * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself. - * - * @param {string|string[]} type - * An event name or an array of event names. - * - * @param {EventTarget~EventListener} fn - * The function to be called once for each event name. - */ - -EventTarget.prototype.one = function (type, fn) { - // Remove the addEventListener aliasing Events.on - // so we don't get into an infinite type loop - var ael = this.addEventListener; - - this.addEventListener = function () {}; - - one(this, type, fn); - this.addEventListener = ael; -}; - -EventTarget.prototype.any = function (type, fn) { - // Remove the addEventListener aliasing Events.on - // so we don't get into an infinite type loop - var ael = this.addEventListener; - - this.addEventListener = function () {}; - - any(this, type, fn); - this.addEventListener = ael; -}; -/** - * This function causes an event to happen. This will then cause any `event listeners` - * that are waiting for that event, to get called. If there are no `event listeners` - * for an event then nothing will happen. - * - * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`. - * Trigger will also call the `on` + `uppercaseEventName` function. - * - * Example: - * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call - * `onClick` if it exists. - * - * @param {string|EventTarget~Event|Object} event - * The name of the event, an `Event`, or an object with a key of type set to - * an event name. - */ - - -EventTarget.prototype.trigger = function (event) { - var type = event.type || event; // deprecation - // In a future version we should default target to `this` - // similar to how we default the target to `elem` in - // `Events.trigger`. Right now the default `target` will be - // `document` due to the `Event.fixEvent` call. - - if (typeof event === 'string') { - event = { - type: type - }; - } - - event = fixEvent(event); - - if (this.allowedEvents_[type] && this['on' + type]) { - this['on' + type](event); - } - - trigger(this, event); -}; -/** - * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic - * the standard DOM API. - * - * @function - * @see {@link EventTarget#trigger} - */ - - -EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger; -var EVENT_MAP; - -EventTarget.prototype.queueTrigger = function (event) { - var _this = this; - - // only set up EVENT_MAP if it'll be used - if (!EVENT_MAP) { - EVENT_MAP = new Map(); - } - - var type = event.type || event; - var map = EVENT_MAP.get(this); - - if (!map) { - map = new Map(); - EVENT_MAP.set(this, map); - } - - var oldTimeout = map.get(type); - map["delete"](type); - window_1$1.clearTimeout(oldTimeout); - var timeout = window_1$1.setTimeout(function () { - // if we cleared out all timeouts for the current target, delete its map - if (map.size === 0) { - map = null; - EVENT_MAP["delete"](_this); - } - - _this.trigger(event); - }, 0); - map.set(type, timeout); -}; - -/** - * @file mixins/evented.js - * @module evented - */ -/** - * Returns whether or not an object has had the evented mixin applied. - * - * @param {Object} object - * An object to test. - * - * @return {boolean} - * Whether or not the object appears to be evented. - */ - -var isEvented = function isEvented(object) { - return object instanceof EventTarget || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(function (k) { - return typeof object[k] === 'function'; - }); -}; -/** - * Adds a callback to run after the evented mixin applied. - * - * @param {Object} object - * An object to Add - * @param {Function} callback - * The callback to run. - */ - - -var addEventedCallback = function addEventedCallback(target, callback) { - if (isEvented(target)) { - callback(); - } else { - if (!target.eventedCallbacks) { - target.eventedCallbacks = []; - } - - target.eventedCallbacks.push(callback); - } -}; -/** - * Whether a value is a valid event type - non-empty string or array. - * - * @private - * @param {string|Array} type - * The type value to test. - * - * @return {boolean} - * Whether or not the type is a valid event type. - */ - - -var isValidEventType = function isValidEventType(type) { - return (// The regex here verifies that the `type` contains at least one non- - // whitespace character. - typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length - ); -}; -/** - * Validates a value to determine if it is a valid event target. Throws if not. - * - * @private - * @throws {Error} - * If the target does not appear to be a valid event target. - * - * @param {Object} target - * The object to test. - */ - - -var validateTarget = function validateTarget(target) { - if (!target.nodeName && !isEvented(target)) { - throw new Error('Invalid target; must be a DOM node or evented object.'); - } -}; -/** - * Validates a value to determine if it is a valid event target. Throws if not. - * - * @private - * @throws {Error} - * If the type does not appear to be a valid event type. - * - * @param {string|Array} type - * The type to test. - */ - - -var validateEventType = function validateEventType(type) { - if (!isValidEventType(type)) { - throw new Error('Invalid event type; must be a non-empty string or array.'); - } -}; -/** - * Validates a value to determine if it is a valid listener. Throws if not. - * - * @private - * @throws {Error} - * If the listener is not a function. - * - * @param {Function} listener - * The listener to test. - */ - - -var validateListener = function validateListener(listener) { - if (typeof listener !== 'function') { - throw new Error('Invalid listener; must be a function.'); - } -}; -/** - * Takes an array of arguments given to `on()` or `one()`, validates them, and - * normalizes them into an object. - * - * @private - * @param {Object} self - * The evented object on which `on()` or `one()` was called. This - * object will be bound as the `this` value for the listener. - * - * @param {Array} args - * An array of arguments passed to `on()` or `one()`. - * - * @return {Object} - * An object containing useful values for `on()` or `one()` calls. - */ - - -var normalizeListenArgs = function normalizeListenArgs(self, args) { - // If the number of arguments is less than 3, the target is always the - // evented object itself. - var isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_; - var target; - var type; - var listener; - - if (isTargetingSelf) { - target = self.eventBusEl_; // Deal with cases where we got 3 arguments, but we are still listening to - // the evented object itself. - - if (args.length >= 3) { - args.shift(); - } - - type = args[0]; - listener = args[1]; - } else { - target = args[0]; - type = args[1]; - listener = args[2]; - } - - validateTarget(target); - validateEventType(type); - validateListener(listener); - listener = bind(self, listener); - return { - isTargetingSelf: isTargetingSelf, - target: target, - type: type, - listener: listener - }; -}; -/** - * Adds the listener to the event type(s) on the target, normalizing for - * the type of target. - * - * @private - * @param {Element|Object} target - * A DOM node or evented object. - * - * @param {string} method - * The event binding method to use ("on" or "one"). - * - * @param {string|Array} type - * One or more event type(s). - * - * @param {Function} listener - * A listener function. - */ - - -var listen = function listen(target, method, type, listener) { - validateTarget(target); - - if (target.nodeName) { - Events[method](target, type, listener); - } else { - target[method](type, listener); - } -}; -/** - * Contains methods that provide event capabilities to an object which is passed - * to {@link module:evented|evented}. - * - * @mixin EventedMixin - */ - - -var EventedMixin = { - /** - * Add a listener to an event (or events) on this object or another evented - * object. - * - * @param {string|Array|Element|Object} targetOrType - * If this is a string or array, it represents the event type(s) - * that will trigger the listener. - * - * Another evented object can be passed here instead, which will - * cause the listener to listen for events on _that_ object. - * - * In either case, the listener's `this` value will be bound to - * this object. - * - * @param {string|Array|Function} typeOrListener - * If the first argument was a string or array, this should be the - * listener function. Otherwise, this is a string or array of event - * type(s). - * - * @param {Function} [listener] - * If the first argument was another evented object, this will be - * the listener function. - */ - on: function on() { - var _this = this; - - for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { - args[_key] = arguments[_key]; - } - - var _normalizeListenArgs = normalizeListenArgs(this, args), - isTargetingSelf = _normalizeListenArgs.isTargetingSelf, - target = _normalizeListenArgs.target, - type = _normalizeListenArgs.type, - listener = _normalizeListenArgs.listener; - - listen(target, 'on', type, listener); // If this object is listening to another evented object. - - if (!isTargetingSelf) { - // If this object is disposed, remove the listener. - var removeListenerOnDispose = function removeListenerOnDispose() { - return _this.off(target, type, listener); - }; // Use the same function ID as the listener so we can remove it later it - // using the ID of the original listener. - - - removeListenerOnDispose.guid = listener.guid; // Add a listener to the target's dispose event as well. This ensures - // that if the target is disposed BEFORE this object, we remove the - // removal listener that was just added. Otherwise, we create a memory leak. - - var removeRemoverOnTargetDispose = function removeRemoverOnTargetDispose() { - return _this.off('dispose', removeListenerOnDispose); - }; // Use the same function ID as the listener so we can remove it later - // it using the ID of the original listener. - - - removeRemoverOnTargetDispose.guid = listener.guid; - listen(this, 'on', 'dispose', removeListenerOnDispose); - listen(target, 'on', 'dispose', removeRemoverOnTargetDispose); - } - }, - - /** - * Add a listener to an event (or events) on this object or another evented - * object. The listener will be called once per event and then removed. - * - * @param {string|Array|Element|Object} targetOrType - * If this is a string or array, it represents the event type(s) - * that will trigger the listener. - * - * Another evented object can be passed here instead, which will - * cause the listener to listen for events on _that_ object. - * - * In either case, the listener's `this` value will be bound to - * this object. - * - * @param {string|Array|Function} typeOrListener - * If the first argument was a string or array, this should be the - * listener function. Otherwise, this is a string or array of event - * type(s). - * - * @param {Function} [listener] - * If the first argument was another evented object, this will be - * the listener function. - */ - one: function one() { - var _this2 = this; - - for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { - args[_key2] = arguments[_key2]; - } - - var _normalizeListenArgs2 = normalizeListenArgs(this, args), - isTargetingSelf = _normalizeListenArgs2.isTargetingSelf, - target = _normalizeListenArgs2.target, - type = _normalizeListenArgs2.type, - listener = _normalizeListenArgs2.listener; // Targeting this evented object. - - - if (isTargetingSelf) { - listen(target, 'one', type, listener); // Targeting another evented object. - } else { - // TODO: This wrapper is incorrect! It should only - // remove the wrapper for the event type that called it. - // Instead all listners are removed on the first trigger! - // see https://github.com/videojs/video.js/issues/5962 - var wrapper = function wrapper() { - _this2.off(target, type, wrapper); - - for (var _len3 = arguments.length, largs = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { - largs[_key3] = arguments[_key3]; - } - - listener.apply(null, largs); - }; // Use the same function ID as the listener so we can remove it later - // it using the ID of the original listener. - - - wrapper.guid = listener.guid; - listen(target, 'one', type, wrapper); - } - }, - - /** - * Add a listener to an event (or events) on this object or another evented - * object. The listener will only be called once for the first event that is triggered - * then removed. - * - * @param {string|Array|Element|Object} targetOrType - * If this is a string or array, it represents the event type(s) - * that will trigger the listener. - * - * Another evented object can be passed here instead, which will - * cause the listener to listen for events on _that_ object. - * - * In either case, the listener's `this` value will be bound to - * this object. - * - * @param {string|Array|Function} typeOrListener - * If the first argument was a string or array, this should be the - * listener function. Otherwise, this is a string or array of event - * type(s). - * - * @param {Function} [listener] - * If the first argument was another evented object, this will be - * the listener function. - */ - any: function any() { - var _this3 = this; - - for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { - args[_key4] = arguments[_key4]; - } - - var _normalizeListenArgs3 = normalizeListenArgs(this, args), - isTargetingSelf = _normalizeListenArgs3.isTargetingSelf, - target = _normalizeListenArgs3.target, - type = _normalizeListenArgs3.type, - listener = _normalizeListenArgs3.listener; // Targeting this evented object. - - - if (isTargetingSelf) { - listen(target, 'any', type, listener); // Targeting another evented object. - } else { - var wrapper = function wrapper() { - _this3.off(target, type, wrapper); - - for (var _len5 = arguments.length, largs = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { - largs[_key5] = arguments[_key5]; - } - - listener.apply(null, largs); - }; // Use the same function ID as the listener so we can remove it later - // it using the ID of the original listener. - - - wrapper.guid = listener.guid; - listen(target, 'any', type, wrapper); - } - }, - - /** - * Removes listener(s) from event(s) on an evented object. - * - * @param {string|Array|Element|Object} [targetOrType] - * If this is a string or array, it represents the event type(s). - * - * Another evented object can be passed here instead, in which case - * ALL 3 arguments are _required_. - * - * @param {string|Array|Function} [typeOrListener] - * If the first argument was a string or array, this may be the - * listener function. Otherwise, this is a string or array of event - * type(s). - * - * @param {Function} [listener] - * If the first argument was another evented object, this will be - * the listener function; otherwise, _all_ listeners bound to the - * event type(s) will be removed. - */ - off: function off$1(targetOrType, typeOrListener, listener) { - // Targeting this evented object. - if (!targetOrType || isValidEventType(targetOrType)) { - off(this.eventBusEl_, targetOrType, typeOrListener); // Targeting another evented object. - } else { - var target = targetOrType; - var type = typeOrListener; // Fail fast and in a meaningful way! - - validateTarget(target); - validateEventType(type); - validateListener(listener); // Ensure there's at least a guid, even if the function hasn't been used - - listener = bind(this, listener); // Remove the dispose listener on this evented object, which was given - // the same guid as the event listener in on(). - - this.off('dispose', listener); - - if (target.nodeName) { - off(target, type, listener); - off(target, 'dispose', listener); - } else if (isEvented(target)) { - target.off(type, listener); - target.off('dispose', listener); - } - } - }, - - /** - * Fire an event on this evented object, causing its listeners to be called. - * - * @param {string|Object} event - * An event type or an object with a type property. - * - * @param {Object} [hash] - * An additional object to pass along to listeners. - * - * @return {boolean} - * Whether or not the default behavior was prevented. - */ - trigger: function trigger$1(event, hash) { - return trigger(this.eventBusEl_, event, hash); - } -}; -/** - * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object. - * - * @param {Object} target - * The object to which to add event methods. - * - * @param {Object} [options={}] - * Options for customizing the mixin behavior. - * - * @param {string} [options.eventBusKey] - * By default, adds a `eventBusEl_` DOM element to the target object, - * which is used as an event bus. If the target object already has a - * DOM element that should be used, pass its key here. - * - * @return {Object} - * The target object. - */ - -function evented(target, options) { - if (options === void 0) { - options = {}; - } - - var _options = options, - eventBusKey = _options.eventBusKey; // Set or create the eventBusEl_. - - if (eventBusKey) { - if (!target[eventBusKey].nodeName) { - throw new Error("The eventBusKey \"" + eventBusKey + "\" does not refer to an element."); - } - - target.eventBusEl_ = target[eventBusKey]; - } else { - target.eventBusEl_ = createEl('span', { - className: 'vjs-event-bus' - }); - } - - assign(target, EventedMixin); - - if (target.eventedCallbacks) { - target.eventedCallbacks.forEach(function (callback) { - callback(); - }); - } // When any evented object is disposed, it removes all its listeners. - - - target.on('dispose', function () { - target.off(); - window_1$1.setTimeout(function () { - target.eventBusEl_ = null; - }, 0); - }); - return target; -} - -/** - * @file mixins/stateful.js - * @module stateful - */ -/** - * Contains methods that provide statefulness to an object which is passed - * to {@link module:stateful}. - * - * @mixin StatefulMixin - */ - -var StatefulMixin = { - /** - * A hash containing arbitrary keys and values representing the state of - * the object. - * - * @type {Object} - */ - state: {}, - - /** - * Set the state of an object by mutating its - * {@link module:stateful~StatefulMixin.state|state} object in place. - * - * @fires module:stateful~StatefulMixin#statechanged - * @param {Object|Function} stateUpdates - * A new set of properties to shallow-merge into the plugin state. - * Can be a plain object or a function returning a plain object. - * - * @return {Object|undefined} - * An object containing changes that occurred. If no changes - * occurred, returns `undefined`. - */ - setState: function setState(stateUpdates) { - var _this = this; - - // Support providing the `stateUpdates` state as a function. - if (typeof stateUpdates === 'function') { - stateUpdates = stateUpdates(); - } - - var changes; - each(stateUpdates, function (value, key) { - // Record the change if the value is different from what's in the - // current state. - if (_this.state[key] !== value) { - changes = changes || {}; - changes[key] = { - from: _this.state[key], - to: value - }; - } - - _this.state[key] = value; - }); // Only trigger "statechange" if there were changes AND we have a trigger - // function. This allows us to not require that the target object be an - // evented object. - - if (changes && isEvented(this)) { - /** - * An event triggered on an object that is both - * {@link module:stateful|stateful} and {@link module:evented|evented} - * indicating that its state has changed. - * - * @event module:stateful~StatefulMixin#statechanged - * @type {Object} - * @property {Object} changes - * A hash containing the properties that were changed and - * the values they were changed `from` and `to`. - */ - this.trigger({ - changes: changes, - type: 'statechanged' - }); - } - - return changes; - } -}; -/** - * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target - * object. - * - * If the target object is {@link module:evented|evented} and has a - * `handleStateChanged` method, that method will be automatically bound to the - * `statechanged` event on itself. - * - * @param {Object} target - * The object to be made stateful. - * - * @param {Object} [defaultState] - * A default set of properties to populate the newly-stateful object's - * `state` property. - * - * @return {Object} - * Returns the `target`. - */ - -function stateful(target, defaultState) { - assign(target, StatefulMixin); // This happens after the mixing-in because we need to replace the `state` - // added in that step. - - target.state = assign({}, target.state, defaultState); // Auto-bind the `handleStateChanged` method of the target object if it exists. - - if (typeof target.handleStateChanged === 'function' && isEvented(target)) { - target.on('statechanged', target.handleStateChanged); - } - - return target; -} - -/** - * @file string-cases.js - * @module to-lower-case - */ - -/** - * Lowercase the first letter of a string. - * - * @param {string} string - * String to be lowercased - * - * @return {string} - * The string with a lowercased first letter - */ -var toLowerCase = function toLowerCase(string) { - if (typeof string !== 'string') { - return string; - } - - return string.replace(/./, function (w) { - return w.toLowerCase(); - }); -}; -/** - * Uppercase the first letter of a string. - * - * @param {string} string - * String to be uppercased - * - * @return {string} - * The string with an uppercased first letter - */ - -var toTitleCase = function toTitleCase(string) { - if (typeof string !== 'string') { - return string; - } - - return string.replace(/./, function (w) { - return w.toUpperCase(); - }); -}; -/** - * Compares the TitleCase versions of the two strings for equality. - * - * @param {string} str1 - * The first string to compare - * - * @param {string} str2 - * The second string to compare - * - * @return {boolean} - * Whether the TitleCase versions of the strings are equal - */ - -var titleCaseEquals = function titleCaseEquals(str1, str2) { - return toTitleCase(str1) === toTitleCase(str2); -}; - -/** - * @file merge-options.js - * @module merge-options - */ -/** - * Merge two objects recursively. - * - * Performs a deep merge like - * {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges - * plain objects (not arrays, elements, or anything else). - * - * Non-plain object values will be copied directly from the right-most - * argument. - * - * @static - * @param {Object[]} sources - * One or more objects to merge into a new object. - * - * @return {Object} - * A new object that is the merged result of all sources. - */ - -function mergeOptions() { - var result = {}; - - for (var _len = arguments.length, sources = new Array(_len), _key = 0; _key < _len; _key++) { - sources[_key] = arguments[_key]; - } - - sources.forEach(function (source) { - if (!source) { - return; - } - - each(source, function (value, key) { - if (!isPlain(value)) { - result[key] = value; - return; - } - - if (!isPlain(result[key])) { - result[key] = {}; - } - - result[key] = mergeOptions(result[key], value); - }); - }); - return result; -} - -var MapSham = /*#__PURE__*/function () { - function MapSham() { - this.map_ = {}; - } - - var _proto = MapSham.prototype; - - _proto.has = function has(key) { - return key in this.map_; - }; - - _proto["delete"] = function _delete(key) { - var has = this.has(key); - delete this.map_[key]; - return has; - }; - - _proto.set = function set(key, value) { - this.set_[key] = value; - return this; - }; - - _proto.forEach = function forEach(callback, thisArg) { - for (var key in this.map_) { - callback.call(thisArg, this.map_[key], key, this); - } - }; - - return MapSham; -}(); - -var Map$1 = window_1$1.Map ? window_1$1.Map : MapSham; - -var SetSham = /*#__PURE__*/function () { - function SetSham() { - this.set_ = {}; - } - - var _proto = SetSham.prototype; - - _proto.has = function has(key) { - return key in this.set_; - }; - - _proto["delete"] = function _delete(key) { - var has = this.has(key); - delete this.set_[key]; - return has; - }; - - _proto.add = function add(key) { - this.set_[key] = 1; - return this; - }; - - _proto.forEach = function forEach(callback, thisArg) { - for (var key in this.set_) { - callback.call(thisArg, key, key, this); - } - }; - - return SetSham; -}(); - -var Set = window_1$1.Set ? window_1$1.Set : SetSham; - -/** - * Player Component - Base class for all UI objects - * - * @file component.js - */ -/** - * Base class for all UI Components. - * Components are UI objects which represent both a javascript object and an element - * in the DOM. They can be children of other components, and can have - * children themselves. - * - * Components can also use methods from {@link EventTarget} - */ - -var Component = /*#__PURE__*/function () { - /** - * A callback that is called when a component is ready. Does not have any - * paramters and any callback value will be ignored. - * - * @callback Component~ReadyCallback - * @this Component - */ - - /** - * Creates an instance of this class. - * - * @param {Player} player - * The `Player` that this class should be attached to. - * - * @param {Object} [options] - * The key/value store of player options. - * - * @param {Object[]} [options.children] - * An array of children objects to intialize this component with. Children objects have - * a name property that will be used if more than one component of the same type needs to be - * added. - * - * @param {Component~ReadyCallback} [ready] - * Function that gets called when the `Component` is ready. - */ - function Component(player, options, ready) { - // The component might be the player itself and we can't pass `this` to super - if (!player && this.play) { - this.player_ = player = this; // eslint-disable-line - } else { - this.player_ = player; - } - - this.isDisposed_ = false; // Hold the reference to the parent component via `addChild` method - - this.parentComponent_ = null; // Make a copy of prototype.options_ to protect against overriding defaults - - this.options_ = mergeOptions({}, this.options_); // Updated options with supplied options - - options = this.options_ = mergeOptions(this.options_, options); // Get ID from options or options element if one is supplied - - this.id_ = options.id || options.el && options.el.id; // If there was no ID from the options, generate one - - if (!this.id_) { - // Don't require the player ID function in the case of mock players - var id = player && player.id && player.id() || 'no_player'; - this.id_ = id + "_component_" + newGUID(); - } - - this.name_ = options.name || null; // Create element if one wasn't provided in options - - if (options.el) { - this.el_ = options.el; - } else if (options.createEl !== false) { - this.el_ = this.createEl(); - } // if evented is anything except false, we want to mixin in evented - - - if (options.evented !== false) { - // Make this an evented object and use `el_`, if available, as its event bus - evented(this, { - eventBusKey: this.el_ ? 'el_' : null - }); - } - - stateful(this, this.constructor.defaultState); - this.children_ = []; - this.childIndex_ = {}; - this.childNameIndex_ = {}; - this.setTimeoutIds_ = new Set(); - this.setIntervalIds_ = new Set(); - this.rafIds_ = new Set(); - this.namedRafs_ = new Map$1(); - this.clearingTimersOnDispose_ = false; // Add any child components in options - - if (options.initChildren !== false) { - this.initChildren(); - } - - this.ready(ready); // Don't want to trigger ready here or it will before init is actually - // finished for all children that run this constructor - - if (options.reportTouchActivity !== false) { - this.enableTouchActivity(); - } - } - /** - * Dispose of the `Component` and all child components. - * - * @fires Component#dispose - */ - - - var _proto = Component.prototype; - - _proto.dispose = function dispose() { - // Bail out if the component has already been disposed. - if (this.isDisposed_) { - return; - } - /** - * Triggered when a `Component` is disposed. - * - * @event Component#dispose - * @type {EventTarget~Event} - * - * @property {boolean} [bubbles=false] - * set to false so that the dispose event does not - * bubble up - */ - - - this.trigger({ - type: 'dispose', - bubbles: false - }); - this.isDisposed_ = true; // Dispose all children. - - if (this.children_) { - for (var i = this.children_.length - 1; i >= 0; i--) { - if (this.children_[i].dispose) { - this.children_[i].dispose(); - } - } - } // Delete child references - - - this.children_ = null; - this.childIndex_ = null; - this.childNameIndex_ = null; - this.parentComponent_ = null; - - if (this.el_) { - // Remove element from DOM - if (this.el_.parentNode) { - this.el_.parentNode.removeChild(this.el_); - } - - if (DomData.has(this.el_)) { - DomData["delete"](this.el_); - } - - this.el_ = null; - } // remove reference to the player after disposing of the element - - - this.player_ = null; - } - /** - * Determine whether or not this component has been disposed. - * - * @return {boolean} - * If the component has been disposed, will be `true`. Otherwise, `false`. - */ - ; - - _proto.isDisposed = function isDisposed() { - return Boolean(this.isDisposed_); - } - /** - * Return the {@link Player} that the `Component` has attached to. - * - * @return {Player} - * The player that this `Component` has attached to. - */ - ; - - _proto.player = function player() { - return this.player_; - } - /** - * Deep merge of options objects with new options. - * > Note: When both `obj` and `options` contain properties whose values are objects. - * The two properties get merged using {@link module:mergeOptions} - * - * @param {Object} obj - * The object that contains new options. - * - * @return {Object} - * A new object of `this.options_` and `obj` merged together. - */ - ; - - _proto.options = function options(obj) { - if (!obj) { - return this.options_; - } - - this.options_ = mergeOptions(this.options_, obj); - return this.options_; - } - /** - * Get the `Component`s DOM element - * - * @return {Element} - * The DOM element for this `Component`. - */ - ; - - _proto.el = function el() { - return this.el_; - } - /** - * Create the `Component`s DOM element. - * - * @param {string} [tagName] - * Element's DOM node type. e.g. 'div' - * - * @param {Object} [properties] - * An object of properties that should be set. - * - * @param {Object} [attributes] - * An object of attributes that should be set. - * - * @return {Element} - * The element that gets created. - */ - ; - - _proto.createEl = function createEl$1(tagName, properties, attributes) { - return createEl(tagName, properties, attributes); - } - /** - * Localize a string given the string in english. - * - * If tokens are provided, it'll try and run a simple token replacement on the provided string. - * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array. - * - * If a `defaultValue` is provided, it'll use that over `string`, - * if a value isn't found in provided language files. - * This is useful if you want to have a descriptive key for token replacement - * but have a succinct localized string and not require `en.json` to be included. - * - * Currently, it is used for the progress bar timing. - * ```js - * { - * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}" - * } - * ``` - * It is then used like so: - * ```js - * this.localize('progress bar timing: currentTime={1} duration{2}', - * [this.player_.currentTime(), this.player_.duration()], - * '{1} of {2}'); - * ``` - * - * Which outputs something like: `01:23 of 24:56`. - * - * - * @param {string} string - * The string to localize and the key to lookup in the language files. - * @param {string[]} [tokens] - * If the current item has token replacements, provide the tokens here. - * @param {string} [defaultValue] - * Defaults to `string`. Can be a default value to use for token replacement - * if the lookup key is needed to be separate. - * - * @return {string} - * The localized string or if no localization exists the english string. - */ - ; - - _proto.localize = function localize(string, tokens, defaultValue) { - if (defaultValue === void 0) { - defaultValue = string; - } - - var code = this.player_.language && this.player_.language(); - var languages = this.player_.languages && this.player_.languages(); - var language = languages && languages[code]; - var primaryCode = code && code.split('-')[0]; - var primaryLang = languages && languages[primaryCode]; - var localizedString = defaultValue; - - if (language && language[string]) { - localizedString = language[string]; - } else if (primaryLang && primaryLang[string]) { - localizedString = primaryLang[string]; - } - - if (tokens) { - localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) { - var value = tokens[index - 1]; - var ret = value; - - if (typeof value === 'undefined') { - ret = match; - } - - return ret; - }); - } - - return localizedString; - } - /** - * Return the `Component`s DOM element. This is where children get inserted. - * This will usually be the the same as the element returned in {@link Component#el}. - * - * @return {Element} - * The content element for this `Component`. - */ - ; - - _proto.contentEl = function contentEl() { - return this.contentEl_ || this.el_; - } - /** - * Get this `Component`s ID - * - * @return {string} - * The id of this `Component` - */ - ; - - _proto.id = function id() { - return this.id_; - } - /** - * Get the `Component`s name. The name gets used to reference the `Component` - * and is set during registration. - * - * @return {string} - * The name of this `Component`. - */ - ; - - _proto.name = function name() { - return this.name_; - } - /** - * Get an array of all child components - * - * @return {Array} - * The children - */ - ; - - _proto.children = function children() { - return this.children_; - } - /** - * Returns the child `Component` with the given `id`. - * - * @param {string} id - * The id of the child `Component` to get. - * - * @return {Component|undefined} - * The child `Component` with the given `id` or undefined. - */ - ; - - _proto.getChildById = function getChildById(id) { - return this.childIndex_[id]; - } - /** - * Returns the child `Component` with the given `name`. - * - * @param {string} name - * The name of the child `Component` to get. - * - * @return {Component|undefined} - * The child `Component` with the given `name` or undefined. - */ - ; - - _proto.getChild = function getChild(name) { - if (!name) { - return; - } - - return this.childNameIndex_[name]; - } - /** - * Returns the descendant `Component` following the givent - * descendant `names`. For instance ['foo', 'bar', 'baz'] would - * try to get 'foo' on the current component, 'bar' on the 'foo' - * component and 'baz' on the 'bar' component and return undefined - * if any of those don't exist. - * - * @param {...string[]|...string} names - * The name of the child `Component` to get. - * - * @return {Component|undefined} - * The descendant `Component` following the given descendant - * `names` or undefined. - */ - ; - - _proto.getDescendant = function getDescendant() { - for (var _len = arguments.length, names = new Array(_len), _key = 0; _key < _len; _key++) { - names[_key] = arguments[_key]; - } - - // flatten array argument into the main array - names = names.reduce(function (acc, n) { - return acc.concat(n); - }, []); - var currentChild = this; - - for (var i = 0; i < names.length; i++) { - currentChild = currentChild.getChild(names[i]); - - if (!currentChild || !currentChild.getChild) { - return; - } - } - - return currentChild; - } - /** - * Add a child `Component` inside the current `Component`. - * - * - * @param {string|Component} child - * The name or instance of a child to add. - * - * @param {Object} [options={}] - * The key/value store of options that will get passed to children of - * the child. - * - * @param {number} [index=this.children_.length] - * The index to attempt to add a child into. - * - * @return {Component} - * The `Component` that gets added as a child. When using a string the - * `Component` will get created by this process. - */ - ; - - _proto.addChild = function addChild(child, options, index) { - if (options === void 0) { - options = {}; - } - - if (index === void 0) { - index = this.children_.length; - } - - var component; - var componentName; // If child is a string, create component with options - - if (typeof child === 'string') { - componentName = toTitleCase(child); - var componentClassName = options.componentClass || componentName; // Set name through options - - options.name = componentName; // Create a new object & element for this controls set - // If there's no .player_, this is a player - - var ComponentClass = Component.getComponent(componentClassName); - - if (!ComponentClass) { - throw new Error("Component " + componentClassName + " does not exist"); - } // data stored directly on the videojs object may be - // misidentified as a component to retain - // backwards-compatibility with 4.x. check to make sure the - // component class can be instantiated. - - - if (typeof ComponentClass !== 'function') { - return null; - } - - component = new ComponentClass(this.player_ || this, options); // child is a component instance - } else { - component = child; - } - - if (component.parentComponent_) { - component.parentComponent_.removeChild(component); - } - - this.children_.splice(index, 0, component); - component.parentComponent_ = this; - - if (typeof component.id === 'function') { - this.childIndex_[component.id()] = component; - } // If a name wasn't used to create the component, check if we can use the - // name function of the component - - - componentName = componentName || component.name && toTitleCase(component.name()); - - if (componentName) { - this.childNameIndex_[componentName] = component; - this.childNameIndex_[toLowerCase(componentName)] = component; - } // Add the UI object's element to the container div (box) - // Having an element is not required - - - if (typeof component.el === 'function' && component.el()) { - // If inserting before a component, insert before that component's element - var refNode = null; - - if (this.children_[index + 1]) { - // Most children are components, but the video tech is an HTML element - if (this.children_[index + 1].el_) { - refNode = this.children_[index + 1].el_; - } else if (isEl(this.children_[index + 1])) { - refNode = this.children_[index + 1]; - } - } - - this.contentEl().insertBefore(component.el(), refNode); - } // Return so it can stored on parent object if desired. - - - return component; - } - /** - * Remove a child `Component` from this `Component`s list of children. Also removes - * the child `Component`s element from this `Component`s element. - * - * @param {Component} component - * The child `Component` to remove. - */ - ; - - _proto.removeChild = function removeChild(component) { - if (typeof component === 'string') { - component = this.getChild(component); - } - - if (!component || !this.children_) { - return; - } - - var childFound = false; - - for (var i = this.children_.length - 1; i >= 0; i--) { - if (this.children_[i] === component) { - childFound = true; - this.children_.splice(i, 1); - break; - } - } - - if (!childFound) { - return; - } - - component.parentComponent_ = null; - this.childIndex_[component.id()] = null; - this.childNameIndex_[toTitleCase(component.name())] = null; - this.childNameIndex_[toLowerCase(component.name())] = null; - var compEl = component.el(); - - if (compEl && compEl.parentNode === this.contentEl()) { - this.contentEl().removeChild(component.el()); - } - } - /** - * Add and initialize default child `Component`s based upon options. - */ - ; - - _proto.initChildren = function initChildren() { - var _this = this; - - var children = this.options_.children; - - if (children) { - // `this` is `parent` - var parentOptions = this.options_; - - var handleAdd = function handleAdd(child) { - var name = child.name; - var opts = child.opts; // Allow options for children to be set at the parent options - // e.g. videojs(id, { controlBar: false }); - // instead of videojs(id, { children: { controlBar: false }); - - if (parentOptions[name] !== undefined) { - opts = parentOptions[name]; - } // Allow for disabling default components - // e.g. options['children']['posterImage'] = false - - - if (opts === false) { - return; - } // Allow options to be passed as a simple boolean if no configuration - // is necessary. - - - if (opts === true) { - opts = {}; - } // We also want to pass the original player options - // to each component as well so they don't need to - // reach back into the player for options later. - - - opts.playerOptions = _this.options_.playerOptions; // Create and add the child component. - // Add a direct reference to the child by name on the parent instance. - // If two of the same component are used, different names should be supplied - // for each - - var newChild = _this.addChild(name, opts); - - if (newChild) { - _this[name] = newChild; - } - }; // Allow for an array of children details to passed in the options - - - var workingChildren; - var Tech = Component.getComponent('Tech'); - - if (Array.isArray(children)) { - workingChildren = children; - } else { - workingChildren = Object.keys(children); - } - - workingChildren // children that are in this.options_ but also in workingChildren would - // give us extra children we do not want. So, we want to filter them out. - .concat(Object.keys(this.options_).filter(function (child) { - return !workingChildren.some(function (wchild) { - if (typeof wchild === 'string') { - return child === wchild; - } - - return child === wchild.name; - }); - })).map(function (child) { - var name; - var opts; - - if (typeof child === 'string') { - name = child; - opts = children[name] || _this.options_[name] || {}; - } else { - name = child.name; - opts = child; - } - - return { - name: name, - opts: opts - }; - }).filter(function (child) { - // we have to make sure that child.name isn't in the techOrder since - // techs are registerd as Components but can't aren't compatible - // See https://github.com/videojs/video.js/issues/2772 - var c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name)); - return c && !Tech.isTech(c); - }).forEach(handleAdd); - } - } - /** - * Builds the default DOM class name. Should be overriden by sub-components. - * - * @return {string} - * The DOM class name for this object. - * - * @abstract - */ - ; - - _proto.buildCSSClass = function buildCSSClass() { - // Child classes can include a function that does: - // return 'CLASS NAME' + this._super(); - return ''; - } - /** - * Bind a listener to the component's ready state. - * Different from event listeners in that if the ready event has already happened - * it will trigger the function immediately. - * - * @return {Component} - * Returns itself; method can be chained. - */ - ; - - _proto.ready = function ready(fn, sync) { - if (sync === void 0) { - sync = false; - } - - if (!fn) { - return; - } - - if (!this.isReady_) { - this.readyQueue_ = this.readyQueue_ || []; - this.readyQueue_.push(fn); - return; - } - - if (sync) { - fn.call(this); - } else { - // Call the function asynchronously by default for consistency - this.setTimeout(fn, 1); - } - } - /** - * Trigger all the ready listeners for this `Component`. - * - * @fires Component#ready - */ - ; - - _proto.triggerReady = function triggerReady() { - this.isReady_ = true; // Ensure ready is triggered asynchronously - - this.setTimeout(function () { - var readyQueue = this.readyQueue_; // Reset Ready Queue - - this.readyQueue_ = []; - - if (readyQueue && readyQueue.length > 0) { - readyQueue.forEach(function (fn) { - fn.call(this); - }, this); - } // Allow for using event listeners also - - /** - * Triggered when a `Component` is ready. - * - * @event Component#ready - * @type {EventTarget~Event} - */ - - - this.trigger('ready'); - }, 1); - } - /** - * Find a single DOM element matching a `selector`. This can be within the `Component`s - * `contentEl()` or another custom context. - * - * @param {string} selector - * A valid CSS selector, which will be passed to `querySelector`. - * - * @param {Element|string} [context=this.contentEl()] - * A DOM element within which to query. Can also be a selector string in - * which case the first matching element will get used as context. If - * missing `this.contentEl()` gets used. If `this.contentEl()` returns - * nothing it falls back to `document`. - * - * @return {Element|null} - * the dom element that was found, or null - * - * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) - */ - ; - - _proto.$ = function $$1(selector, context) { - return $(selector, context || this.contentEl()); - } - /** - * Finds all DOM element matching a `selector`. This can be within the `Component`s - * `contentEl()` or another custom context. - * - * @param {string} selector - * A valid CSS selector, which will be passed to `querySelectorAll`. - * - * @param {Element|string} [context=this.contentEl()] - * A DOM element within which to query. Can also be a selector string in - * which case the first matching element will get used as context. If - * missing `this.contentEl()` gets used. If `this.contentEl()` returns - * nothing it falls back to `document`. - * - * @return {NodeList} - * a list of dom elements that were found - * - * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) - */ - ; - - _proto.$$ = function $$$1(selector, context) { - return $$(selector, context || this.contentEl()); - } - /** - * Check if a component's element has a CSS class name. - * - * @param {string} classToCheck - * CSS class name to check. - * - * @return {boolean} - * - True if the `Component` has the class. - * - False if the `Component` does not have the class` - */ - ; - - _proto.hasClass = function hasClass$1(classToCheck) { - return hasClass(this.el_, classToCheck); - } - /** - * Add a CSS class name to the `Component`s element. - * - * @param {string} classToAdd - * CSS class name to add - */ - ; - - _proto.addClass = function addClass$1(classToAdd) { - addClass(this.el_, classToAdd); - } - /** - * Remove a CSS class name from the `Component`s element. - * - * @param {string} classToRemove - * CSS class name to remove - */ - ; - - _proto.removeClass = function removeClass$1(classToRemove) { - removeClass(this.el_, classToRemove); - } - /** - * Add or remove a CSS class name from the component's element. - * - `classToToggle` gets added when {@link Component#hasClass} would return false. - * - `classToToggle` gets removed when {@link Component#hasClass} would return true. - * - * @param {string} classToToggle - * The class to add or remove based on (@link Component#hasClass} - * - * @param {boolean|Dom~predicate} [predicate] - * An {@link Dom~predicate} function or a boolean - */ - ; - - _proto.toggleClass = function toggleClass$1(classToToggle, predicate) { - toggleClass(this.el_, classToToggle, predicate); - } - /** - * Show the `Component`s element if it is hidden by removing the - * 'vjs-hidden' class name from it. - */ - ; - - _proto.show = function show() { - this.removeClass('vjs-hidden'); - } - /** - * Hide the `Component`s element if it is currently showing by adding the - * 'vjs-hidden` class name to it. - */ - ; - - _proto.hide = function hide() { - this.addClass('vjs-hidden'); - } - /** - * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing' - * class name to it. Used during fadeIn/fadeOut. - * - * @private - */ - ; - - _proto.lockShowing = function lockShowing() { - this.addClass('vjs-lock-showing'); - } - /** - * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing' - * class name from it. Used during fadeIn/fadeOut. - * - * @private - */ - ; - - _proto.unlockShowing = function unlockShowing() { - this.removeClass('vjs-lock-showing'); - } - /** - * Get the value of an attribute on the `Component`s element. - * - * @param {string} attribute - * Name of the attribute to get the value from. - * - * @return {string|null} - * - The value of the attribute that was asked for. - * - Can be an empty string on some browsers if the attribute does not exist - * or has no value - * - Most browsers will return null if the attibute does not exist or has - * no value. - * - * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute} - */ - ; - - _proto.getAttribute = function getAttribute$1(attribute) { - return getAttribute(this.el_, attribute); - } - /** - * Set the value of an attribute on the `Component`'s element - * - * @param {string} attribute - * Name of the attribute to set. - * - * @param {string} value - * Value to set the attribute to. - * - * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute} - */ - ; - - _proto.setAttribute = function setAttribute$1(attribute, value) { - setAttribute(this.el_, attribute, value); - } - /** - * Remove an attribute from the `Component`s element. - * - * @param {string} attribute - * Name of the attribute to remove. - * - * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute} - */ - ; - - _proto.removeAttribute = function removeAttribute$1(attribute) { - removeAttribute(this.el_, attribute); - } - /** - * Get or set the width of the component based upon the CSS styles. - * See {@link Component#dimension} for more detailed information. - * - * @param {number|string} [num] - * The width that you want to set postfixed with '%', 'px' or nothing. - * - * @param {boolean} [skipListeners] - * Skip the componentresize event trigger - * - * @return {number|string} - * The width when getting, zero if there is no width. Can be a string - * postpixed with '%' or 'px'. - */ - ; - - _proto.width = function width(num, skipListeners) { - return this.dimension('width', num, skipListeners); - } - /** - * Get or set the height of the component based upon the CSS styles. - * See {@link Component#dimension} for more detailed information. - * - * @param {number|string} [num] - * The height that you want to set postfixed with '%', 'px' or nothing. - * - * @param {boolean} [skipListeners] - * Skip the componentresize event trigger - * - * @return {number|string} - * The width when getting, zero if there is no width. Can be a string - * postpixed with '%' or 'px'. - */ - ; - - _proto.height = function height(num, skipListeners) { - return this.dimension('height', num, skipListeners); - } - /** - * Set both the width and height of the `Component` element at the same time. - * - * @param {number|string} width - * Width to set the `Component`s element to. - * - * @param {number|string} height - * Height to set the `Component`s element to. - */ - ; - - _proto.dimensions = function dimensions(width, height) { - // Skip componentresize listeners on width for optimization - this.width(width, true); - this.height(height); - } - /** - * Get or set width or height of the `Component` element. This is the shared code - * for the {@link Component#width} and {@link Component#height}. - * - * Things to know: - * - If the width or height in an number this will return the number postfixed with 'px'. - * - If the width/height is a percent this will return the percent postfixed with '%' - * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function - * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`. - * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/} - * for more information - * - If you want the computed style of the component, use {@link Component#currentWidth} - * and {@link {Component#currentHeight} - * - * @fires Component#componentresize - * - * @param {string} widthOrHeight - 8 'width' or 'height' - * - * @param {number|string} [num] - 8 New dimension - * - * @param {boolean} [skipListeners] - * Skip componentresize event trigger - * - * @return {number} - * The dimension when getting or 0 if unset - */ - ; - - _proto.dimension = function dimension(widthOrHeight, num, skipListeners) { - if (num !== undefined) { - // Set to zero if null or literally NaN (NaN !== NaN) - if (num === null || num !== num) { - num = 0; - } // Check if using css width/height (% or px) and adjust - - - if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) { - this.el_.style[widthOrHeight] = num; - } else if (num === 'auto') { - this.el_.style[widthOrHeight] = ''; - } else { - this.el_.style[widthOrHeight] = num + 'px'; - } // skipListeners allows us to avoid triggering the resize event when setting both width and height - - - if (!skipListeners) { - /** - * Triggered when a component is resized. - * - * @event Component#componentresize - * @type {EventTarget~Event} - */ - this.trigger('componentresize'); - } - - return; - } // Not setting a value, so getting it - // Make sure element exists - - - if (!this.el_) { - return 0; - } // Get dimension value from style - - - var val = this.el_.style[widthOrHeight]; - var pxIndex = val.indexOf('px'); - - if (pxIndex !== -1) { - // Return the pixel value with no 'px' - return parseInt(val.slice(0, pxIndex), 10); - } // No px so using % or no style was set, so falling back to offsetWidth/height - // If component has display:none, offset will return 0 - // TODO: handle display:none and no dimension style using px - - - return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10); - } - /** - * Get the computed width or the height of the component's element. - * - * Uses `window.getComputedStyle`. - * - * @param {string} widthOrHeight - * A string containing 'width' or 'height'. Whichever one you want to get. - * - * @return {number} - * The dimension that gets asked for or 0 if nothing was set - * for that dimension. - */ - ; - - _proto.currentDimension = function currentDimension(widthOrHeight) { - var computedWidthOrHeight = 0; - - if (widthOrHeight !== 'width' && widthOrHeight !== 'height') { - throw new Error('currentDimension only accepts width or height value'); - } - - computedWidthOrHeight = computedStyle(this.el_, widthOrHeight); // remove 'px' from variable and parse as integer - - computedWidthOrHeight = parseFloat(computedWidthOrHeight); // if the computed value is still 0, it's possible that the browser is lying - // and we want to check the offset values. - // This code also runs wherever getComputedStyle doesn't exist. - - if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) { - var rule = "offset" + toTitleCase(widthOrHeight); - computedWidthOrHeight = this.el_[rule]; - } - - return computedWidthOrHeight; - } - /** - * An object that contains width and height values of the `Component`s - * computed style. Uses `window.getComputedStyle`. - * - * @typedef {Object} Component~DimensionObject - * - * @property {number} width - * The width of the `Component`s computed style. - * - * @property {number} height - * The height of the `Component`s computed style. - */ - - /** - * Get an object that contains computed width and height values of the - * component's element. - * - * Uses `window.getComputedStyle`. - * - * @return {Component~DimensionObject} - * The computed dimensions of the component's element. - */ - ; - - _proto.currentDimensions = function currentDimensions() { - return { - width: this.currentDimension('width'), - height: this.currentDimension('height') - }; - } - /** - * Get the computed width of the component's element. - * - * Uses `window.getComputedStyle`. - * - * @return {number} - * The computed width of the component's element. - */ - ; - - _proto.currentWidth = function currentWidth() { - return this.currentDimension('width'); - } - /** - * Get the computed height of the component's element. - * - * Uses `window.getComputedStyle`. - * - * @return {number} - * The computed height of the component's element. - */ - ; - - _proto.currentHeight = function currentHeight() { - return this.currentDimension('height'); - } - /** - * Set the focus to this component - */ - ; - - _proto.focus = function focus() { - this.el_.focus(); - } - /** - * Remove the focus from this component - */ - ; - - _proto.blur = function blur() { - this.el_.blur(); - } - /** - * When this Component receives a `keydown` event which it does not process, - * it passes the event to the Player for handling. - * - * @param {EventTarget~Event} event - * The `keydown` event that caused this function to be called. - */ - ; - - _proto.handleKeyDown = function handleKeyDown(event) { - if (this.player_) { - // We only stop propagation here because we want unhandled events to fall - // back to the browser. - event.stopPropagation(); - this.player_.handleKeyDown(event); - } - } - /** - * Many components used to have a `handleKeyPress` method, which was poorly - * named because it listened to a `keydown` event. This method name now - * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress` - * will not see their method calls stop working. - * - * @param {EventTarget~Event} event - * The event that caused this function to be called. - */ - ; - - _proto.handleKeyPress = function handleKeyPress(event) { - this.handleKeyDown(event); - } - /** - * Emit a 'tap' events when touch event support gets detected. This gets used to - * support toggling the controls through a tap on the video. They get enabled - * because every sub-component would have extra overhead otherwise. - * - * @private - * @fires Component#tap - * @listens Component#touchstart - * @listens Component#touchmove - * @listens Component#touchleave - * @listens Component#touchcancel - * @listens Component#touchend - */ - ; - - _proto.emitTapEvents = function emitTapEvents() { - // Track the start time so we can determine how long the touch lasted - var touchStart = 0; - var firstTouch = null; // Maximum movement allowed during a touch event to still be considered a tap - // Other popular libs use anywhere from 2 (hammer.js) to 15, - // so 10 seems like a nice, round number. - - var tapMovementThreshold = 10; // The maximum length a touch can be while still being considered a tap - - var touchTimeThreshold = 200; - var couldBeTap; - this.on('touchstart', function (event) { - // If more than one finger, don't consider treating this as a click - if (event.touches.length === 1) { - // Copy pageX/pageY from the object - firstTouch = { - pageX: event.touches[0].pageX, - pageY: event.touches[0].pageY - }; // Record start time so we can detect a tap vs. "touch and hold" - - touchStart = window_1$1.performance.now(); // Reset couldBeTap tracking - - couldBeTap = true; - } - }); - this.on('touchmove', function (event) { - // If more than one finger, don't consider treating this as a click - if (event.touches.length > 1) { - couldBeTap = false; - } else if (firstTouch) { - // Some devices will throw touchmoves for all but the slightest of taps. - // So, if we moved only a small distance, this could still be a tap - var xdiff = event.touches[0].pageX - firstTouch.pageX; - var ydiff = event.touches[0].pageY - firstTouch.pageY; - var touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); - - if (touchDistance > tapMovementThreshold) { - couldBeTap = false; - } - } - }); - - var noTap = function noTap() { - couldBeTap = false; - }; // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s - - - this.on('touchleave', noTap); - this.on('touchcancel', noTap); // When the touch ends, measure how long it took and trigger the appropriate - // event - - this.on('touchend', function (event) { - firstTouch = null; // Proceed only if the touchmove/leave/cancel event didn't happen - - if (couldBeTap === true) { - // Measure how long the touch lasted - var touchTime = window_1$1.performance.now() - touchStart; // Make sure the touch was less than the threshold to be considered a tap - - if (touchTime < touchTimeThreshold) { - // Don't let browser turn this into a click - event.preventDefault(); - /** - * Triggered when a `Component` is tapped. - * - * @event Component#tap - * @type {EventTarget~Event} - */ - - this.trigger('tap'); // It may be good to copy the touchend event object and change the - // type to tap, if the other event properties aren't exact after - // Events.fixEvent runs (e.g. event.target) - } - } - }); - } - /** - * This function reports user activity whenever touch events happen. This can get - * turned off by any sub-components that wants touch events to act another way. - * - * Report user touch activity when touch events occur. User activity gets used to - * determine when controls should show/hide. It is simple when it comes to mouse - * events, because any mouse event should show the controls. So we capture mouse - * events that bubble up to the player and report activity when that happens. - * With touch events it isn't as easy as `touchstart` and `touchend` toggle player - * controls. So touch events can't help us at the player level either. - * - * User activity gets checked asynchronously. So what could happen is a tap event - * on the video turns the controls off. Then the `touchend` event bubbles up to - * the player. Which, if it reported user activity, would turn the controls right - * back on. We also don't want to completely block touch events from bubbling up. - * Furthermore a `touchmove` event and anything other than a tap, should not turn - * controls back on. - * - * @listens Component#touchstart - * @listens Component#touchmove - * @listens Component#touchend - * @listens Component#touchcancel - */ - ; - - _proto.enableTouchActivity = function enableTouchActivity() { - // Don't continue if the root player doesn't support reporting user activity - if (!this.player() || !this.player().reportUserActivity) { - return; - } // listener for reporting that the user is active - - - var report = bind(this.player(), this.player().reportUserActivity); - var touchHolding; - this.on('touchstart', function () { - report(); // For as long as the they are touching the device or have their mouse down, - // we consider them active even if they're not moving their finger or mouse. - // So we want to continue to update that they are active - - this.clearInterval(touchHolding); // report at the same interval as activityCheck - - touchHolding = this.setInterval(report, 250); - }); - - var touchEnd = function touchEnd(event) { - report(); // stop the interval that maintains activity if the touch is holding - - this.clearInterval(touchHolding); - }; - - this.on('touchmove', report); - this.on('touchend', touchEnd); - this.on('touchcancel', touchEnd); - } - /** - * A callback that has no parameters and is bound into `Component`s context. - * - * @callback Component~GenericCallback - * @this Component - */ - - /** - * Creates a function that runs after an `x` millisecond timeout. This function is a - * wrapper around `window.setTimeout`. There are a few reasons to use this one - * instead though: - * 1. It gets cleared via {@link Component#clearTimeout} when - * {@link Component#dispose} gets called. - * 2. The function callback will gets turned into a {@link Component~GenericCallback} - * - * > Note: You can't use `window.clearTimeout` on the id returned by this function. This - * will cause its dispose listener not to get cleaned up! Please use - * {@link Component#clearTimeout} or {@link Component#dispose} instead. - * - * @param {Component~GenericCallback} fn - * The function that will be run after `timeout`. - * - * @param {number} timeout - * Timeout in milliseconds to delay before executing the specified function. - * - * @return {number} - * Returns a timeout ID that gets used to identify the timeout. It can also - * get used in {@link Component#clearTimeout} to clear the timeout that - * was set. - * - * @listens Component#dispose - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout} - */ - ; - - _proto.setTimeout = function setTimeout(fn, timeout) { - var _this2 = this; - - // declare as variables so they are properly available in timeout function - // eslint-disable-next-line - var timeoutId; - fn = bind(this, fn); - this.clearTimersOnDispose_(); - timeoutId = window_1$1.setTimeout(function () { - if (_this2.setTimeoutIds_.has(timeoutId)) { - _this2.setTimeoutIds_["delete"](timeoutId); - } - - fn(); - }, timeout); - this.setTimeoutIds_.add(timeoutId); - return timeoutId; - } - /** - * Clears a timeout that gets created via `window.setTimeout` or - * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout} - * use this function instead of `window.clearTimout`. If you don't your dispose - * listener will not get cleaned up until {@link Component#dispose}! - * - * @param {number} timeoutId - * The id of the timeout to clear. The return value of - * {@link Component#setTimeout} or `window.setTimeout`. - * - * @return {number} - * Returns the timeout id that was cleared. - * - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout} - */ - ; - - _proto.clearTimeout = function clearTimeout(timeoutId) { - if (this.setTimeoutIds_.has(timeoutId)) { - this.setTimeoutIds_["delete"](timeoutId); - window_1$1.clearTimeout(timeoutId); - } - - return timeoutId; - } - /** - * Creates a function that gets run every `x` milliseconds. This function is a wrapper - * around `window.setInterval`. There are a few reasons to use this one instead though. - * 1. It gets cleared via {@link Component#clearInterval} when - * {@link Component#dispose} gets called. - * 2. The function callback will be a {@link Component~GenericCallback} - * - * @param {Component~GenericCallback} fn - * The function to run every `x` seconds. - * - * @param {number} interval - * Execute the specified function every `x` milliseconds. - * - * @return {number} - * Returns an id that can be used to identify the interval. It can also be be used in - * {@link Component#clearInterval} to clear the interval. - * - * @listens Component#dispose - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval} - */ - ; - - _proto.setInterval = function setInterval(fn, interval) { - fn = bind(this, fn); - this.clearTimersOnDispose_(); - var intervalId = window_1$1.setInterval(fn, interval); - this.setIntervalIds_.add(intervalId); - return intervalId; - } - /** - * Clears an interval that gets created via `window.setInterval` or - * {@link Component#setInterval}. If you set an inteval via {@link Component#setInterval} - * use this function instead of `window.clearInterval`. If you don't your dispose - * listener will not get cleaned up until {@link Component#dispose}! - * - * @param {number} intervalId - * The id of the interval to clear. The return value of - * {@link Component#setInterval} or `window.setInterval`. - * - * @return {number} - * Returns the interval id that was cleared. - * - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval} - */ - ; - - _proto.clearInterval = function clearInterval(intervalId) { - if (this.setIntervalIds_.has(intervalId)) { - this.setIntervalIds_["delete"](intervalId); - window_1$1.clearInterval(intervalId); - } - - return intervalId; - } - /** - * Queues up a callback to be passed to requestAnimationFrame (rAF), but - * with a few extra bonuses: - * - * - Supports browsers that do not support rAF by falling back to - * {@link Component#setTimeout}. - * - * - The callback is turned into a {@link Component~GenericCallback} (i.e. - * bound to the component). - * - * - Automatic cancellation of the rAF callback is handled if the component - * is disposed before it is called. - * - * @param {Component~GenericCallback} fn - * A function that will be bound to this component and executed just - * before the browser's next repaint. - * - * @return {number} - * Returns an rAF ID that gets used to identify the timeout. It can - * also be used in {@link Component#cancelAnimationFrame} to cancel - * the animation frame callback. - * - * @listens Component#dispose - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame} - */ - ; - - _proto.requestAnimationFrame = function requestAnimationFrame(fn) { - var _this3 = this; - - // Fall back to using a timer. - if (!this.supportsRaf_) { - return this.setTimeout(fn, 1000 / 60); - } - - this.clearTimersOnDispose_(); // declare as variables so they are properly available in rAF function - // eslint-disable-next-line - - var id; - fn = bind(this, fn); - id = window_1$1.requestAnimationFrame(function () { - if (_this3.rafIds_.has(id)) { - _this3.rafIds_["delete"](id); - } - - fn(); - }); - this.rafIds_.add(id); - return id; - } - /** - * Request an animation frame, but only one named animation - * frame will be queued. Another will never be added until - * the previous one finishes. - * - * @param {string} name - * The name to give this requestAnimationFrame - * - * @param {Component~GenericCallback} fn - * A function that will be bound to this component and executed just - * before the browser's next repaint. - */ - ; - - _proto.requestNamedAnimationFrame = function requestNamedAnimationFrame(name, fn) { - var _this4 = this; - - if (this.namedRafs_.has(name)) { - return; - } - - this.clearTimersOnDispose_(); - fn = bind(this, fn); - var id = this.requestAnimationFrame(function () { - fn(); - - if (_this4.namedRafs_.has(name)) { - _this4.namedRafs_["delete"](name); - } - }); - this.namedRafs_.set(name, id); - return name; - } - /** - * Cancels a current named animation frame if it exists. - * - * @param {string} name - * The name of the requestAnimationFrame to cancel. - */ - ; - - _proto.cancelNamedAnimationFrame = function cancelNamedAnimationFrame(name) { - if (!this.namedRafs_.has(name)) { - return; - } - - this.cancelAnimationFrame(this.namedRafs_.get(name)); - this.namedRafs_["delete"](name); - } - /** - * Cancels a queued callback passed to {@link Component#requestAnimationFrame} - * (rAF). - * - * If you queue an rAF callback via {@link Component#requestAnimationFrame}, - * use this function instead of `window.cancelAnimationFrame`. If you don't, - * your dispose listener will not get cleaned up until {@link Component#dispose}! - * - * @param {number} id - * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}. - * - * @return {number} - * Returns the rAF ID that was cleared. - * - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame} - */ - ; - - _proto.cancelAnimationFrame = function cancelAnimationFrame(id) { - // Fall back to using a timer. - if (!this.supportsRaf_) { - return this.clearTimeout(id); - } - - if (this.rafIds_.has(id)) { - this.rafIds_["delete"](id); - window_1$1.cancelAnimationFrame(id); - } - - return id; - } - /** - * A function to setup `requestAnimationFrame`, `setTimeout`, - * and `setInterval`, clearing on dispose. - * - * > Previously each timer added and removed dispose listeners on it's own. - * For better performance it was decided to batch them all, and use `Set`s - * to track outstanding timer ids. - * - * @private - */ - ; - - _proto.clearTimersOnDispose_ = function clearTimersOnDispose_() { - var _this5 = this; - - if (this.clearingTimersOnDispose_) { - return; - } - - this.clearingTimersOnDispose_ = true; - this.one('dispose', function () { - [['namedRafs_', 'cancelNamedAnimationFrame'], ['rafIds_', 'cancelAnimationFrame'], ['setTimeoutIds_', 'clearTimeout'], ['setIntervalIds_', 'clearInterval']].forEach(function (_ref) { - var idName = _ref[0], - cancelName = _ref[1]; - - // for a `Set` key will actually be the value again - // so forEach((val, val) =>` but for maps we want to use - // the key. - _this5[idName].forEach(function (val, key) { - return _this5[cancelName](key); - }); - }); - _this5.clearingTimersOnDispose_ = false; - }); - } - /** - * Register a `Component` with `videojs` given the name and the component. - * - * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s - * should be registered using {@link Tech.registerTech} or - * {@link videojs:videojs.registerTech}. - * - * > NOTE: This function can also be seen on videojs as - * {@link videojs:videojs.registerComponent}. - * - * @param {string} name - * The name of the `Component` to register. - * - * @param {Component} ComponentToRegister - * The `Component` class to register. - * - * @return {Component} - * The `Component` that was registered. - */ - ; - - Component.registerComponent = function registerComponent(name, ComponentToRegister) { - if (typeof name !== 'string' || !name) { - throw new Error("Illegal component name, \"" + name + "\"; must be a non-empty string."); - } - - var Tech = Component.getComponent('Tech'); // We need to make sure this check is only done if Tech has been registered. - - var isTech = Tech && Tech.isTech(ComponentToRegister); - var isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype); - - if (isTech || !isComp) { - var reason; - - if (isTech) { - reason = 'techs must be registered using Tech.registerTech()'; - } else { - reason = 'must be a Component subclass'; - } - - throw new Error("Illegal component, \"" + name + "\"; " + reason + "."); - } - - name = toTitleCase(name); - - if (!Component.components_) { - Component.components_ = {}; - } - - var Player = Component.getComponent('Player'); - - if (name === 'Player' && Player && Player.players) { - var players = Player.players; - var playerNames = Object.keys(players); // If we have players that were disposed, then their name will still be - // in Players.players. So, we must loop through and verify that the value - // for each item is not null. This allows registration of the Player component - // after all players have been disposed or before any were created. - - if (players && playerNames.length > 0 && playerNames.map(function (pname) { - return players[pname]; - }).every(Boolean)) { - throw new Error('Can not register Player component after player has been created.'); - } - } - - Component.components_[name] = ComponentToRegister; - Component.components_[toLowerCase(name)] = ComponentToRegister; - return ComponentToRegister; - } - /** - * Get a `Component` based on the name it was registered with. - * - * @param {string} name - * The Name of the component to get. - * - * @return {Component} - * The `Component` that got registered under the given name. - * - * @deprecated In `videojs` 6 this will not return `Component`s that were not - * registered using {@link Component.registerComponent}. Currently we - * check the global `videojs` object for a `Component` name and - * return that if it exists. - */ - ; - - Component.getComponent = function getComponent(name) { - if (!name || !Component.components_) { - return; - } - - return Component.components_[name]; - }; - - return Component; -}(); -/** - * Whether or not this component supports `requestAnimationFrame`. - * - * This is exposed primarily for testing purposes. - * - * @private - * @type {Boolean} - */ - - -Component.prototype.supportsRaf_ = typeof window_1$1.requestAnimationFrame === 'function' && typeof window_1$1.cancelAnimationFrame === 'function'; -Component.registerComponent('Component', Component); - -/** - * @file browser.js - * @module browser - */ -var USER_AGENT = window_1$1.navigator && window_1$1.navigator.userAgent || ''; -var webkitVersionMap = /AppleWebKit\/([\d.]+)/i.exec(USER_AGENT); -var appleWebkitVersion = webkitVersionMap ? parseFloat(webkitVersionMap.pop()) : null; -/** - * Whether or not this device is an iPod. - * - * @static - * @const - * @type {Boolean} - */ - -var IS_IPOD = /iPod/i.test(USER_AGENT); -/** - * The detected iOS version - or `null`. - * - * @static - * @const - * @type {string|null} - */ - -var IOS_VERSION = function () { - var match = USER_AGENT.match(/OS (\d+)_/i); - - if (match && match[1]) { - return match[1]; - } - - return null; -}(); -/** - * Whether or not this is an Android device. - * - * @static - * @const - * @type {Boolean} - */ - -var IS_ANDROID = /Android/i.test(USER_AGENT); -/** - * The detected Android version - or `null`. - * - * @static - * @const - * @type {number|string|null} - */ - -var ANDROID_VERSION = function () { - // This matches Android Major.Minor.Patch versions - // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned - var match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); - - if (!match) { - return null; - } - - var major = match[1] && parseFloat(match[1]); - var minor = match[2] && parseFloat(match[2]); - - if (major && minor) { - return parseFloat(match[1] + '.' + match[2]); - } else if (major) { - return major; - } - - return null; -}(); -/** - * Whether or not this is a native Android browser. - * - * @static - * @const - * @type {Boolean} - */ - -var IS_NATIVE_ANDROID = IS_ANDROID && ANDROID_VERSION < 5 && appleWebkitVersion < 537; -/** - * Whether or not this is Mozilla Firefox. - * - * @static - * @const - * @type {Boolean} - */ - -var IS_FIREFOX = /Firefox/i.test(USER_AGENT); -/** - * Whether or not this is Microsoft Edge. - * - * @static - * @const - * @type {Boolean} - */ - -var IS_EDGE = /Edg/i.test(USER_AGENT); -/** - * Whether or not this is Google Chrome. - * - * This will also be `true` for Chrome on iOS, which will have different support - * as it is actually Safari under the hood. - * - * @static - * @const - * @type {Boolean} - */ - -var IS_CHROME = !IS_EDGE && (/Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT)); -/** - * The detected Google Chrome version - or `null`. - * - * @static - * @const - * @type {number|null} - */ - -var CHROME_VERSION = function () { - var match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/); - - if (match && match[2]) { - return parseFloat(match[2]); - } - - return null; -}(); -/** - * The detected Internet Explorer version - or `null`. - * - * @static - * @const - * @type {number|null} - */ - -var IE_VERSION = function () { - var result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT); - var version = result && parseFloat(result[1]); - - if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) { - // IE 11 has a different user agent string than other IE versions - version = 11.0; - } - - return version; -}(); -/** - * Whether or not this is desktop Safari. - * - * @static - * @const - * @type {Boolean} - */ - -var IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE; -/** - * Whether or not this is a Windows machine. - * - * @static - * @const - * @type {Boolean} - */ - -var IS_WINDOWS = /Windows/i.test(USER_AGENT); -/** - * Whether or not this device is touch-enabled. - * - * @static - * @const - * @type {Boolean} - */ - -var TOUCH_ENABLED = isReal() && ('ontouchstart' in window_1$1 || window_1$1.navigator.maxTouchPoints || window_1$1.DocumentTouch && window_1$1.document instanceof window_1$1.DocumentTouch); -/** - * Whether or not this device is an iPad. - * - * @static - * @const - * @type {Boolean} - */ - -var IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT); -/** - * Whether or not this device is an iPhone. - * - * @static - * @const - * @type {Boolean} - */ -// The Facebook app's UIWebView identifies as both an iPhone and iPad, so -// to identify iPhones, we need to exclude iPads. -// http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ - -var IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD; -/** - * Whether or not this is an iOS device. - * - * @static - * @const - * @type {Boolean} - */ - -var IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; -/** - * Whether or not this is any flavor of Safari - including iOS. - * - * @static - * @const - * @type {Boolean} - */ - -var IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME; - -var browser = /*#__PURE__*/Object.freeze({ - __proto__: null, - IS_IPOD: IS_IPOD, - IOS_VERSION: IOS_VERSION, - IS_ANDROID: IS_ANDROID, - ANDROID_VERSION: ANDROID_VERSION, - IS_NATIVE_ANDROID: IS_NATIVE_ANDROID, - IS_FIREFOX: IS_FIREFOX, - IS_EDGE: IS_EDGE, - IS_CHROME: IS_CHROME, - CHROME_VERSION: CHROME_VERSION, - IE_VERSION: IE_VERSION, - IS_SAFARI: IS_SAFARI, - IS_WINDOWS: IS_WINDOWS, - TOUCH_ENABLED: TOUCH_ENABLED, - IS_IPAD: IS_IPAD, - IS_IPHONE: IS_IPHONE, - IS_IOS: IS_IOS, - IS_ANY_SAFARI: IS_ANY_SAFARI -}); - -/** - * @file time-ranges.js - * @module time-ranges - */ - -/** - * Returns the time for the specified index at the start or end - * of a TimeRange object. - * - * @typedef {Function} TimeRangeIndex - * - * @param {number} [index=0] - * The range number to return the time for. - * - * @return {number} - * The time offset at the specified index. - * - * @deprecated The index argument must be provided. - * In the future, leaving it out will throw an error. - */ - -/** - * An object that contains ranges of time. - * - * @typedef {Object} TimeRange - * - * @property {number} length - * The number of time ranges represented by this object. - * - * @property {module:time-ranges~TimeRangeIndex} start - * Returns the time offset at which a specified time range begins. - * - * @property {module:time-ranges~TimeRangeIndex} end - * Returns the time offset at which a specified time range ends. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges - */ - -/** - * Check if any of the time ranges are over the maximum index. - * - * @private - * @param {string} fnName - * The function name to use for logging - * - * @param {number} index - * The index to check - * - * @param {number} maxIndex - * The maximum possible index - * - * @throws {Error} if the timeRanges provided are over the maxIndex - */ -function rangeCheck(fnName, index, maxIndex) { - if (typeof index !== 'number' || index < 0 || index > maxIndex) { - throw new Error("Failed to execute '" + fnName + "' on 'TimeRanges': The index provided (" + index + ") is non-numeric or out of bounds (0-" + maxIndex + ")."); - } -} -/** - * Get the time for the specified index at the start or end - * of a TimeRange object. - * - * @private - * @param {string} fnName - * The function name to use for logging - * - * @param {string} valueIndex - * The property that should be used to get the time. should be - * 'start' or 'end' - * - * @param {Array} ranges - * An array of time ranges - * - * @param {Array} [rangeIndex=0] - * The index to start the search at - * - * @return {number} - * The time that offset at the specified index. - * - * @deprecated rangeIndex must be set to a value, in the future this will throw an error. - * @throws {Error} if rangeIndex is more than the length of ranges - */ - - -function getRange(fnName, valueIndex, ranges, rangeIndex) { - rangeCheck(fnName, rangeIndex, ranges.length - 1); - return ranges[rangeIndex][valueIndex]; -} -/** - * Create a time range object given ranges of time. - * - * @private - * @param {Array} [ranges] - * An array of time ranges. - */ - - -function createTimeRangesObj(ranges) { - if (ranges === undefined || ranges.length === 0) { - return { - length: 0, - start: function start() { - throw new Error('This TimeRanges object is empty'); - }, - end: function end() { - throw new Error('This TimeRanges object is empty'); - } - }; - } - - return { - length: ranges.length, - start: getRange.bind(null, 'start', 0, ranges), - end: getRange.bind(null, 'end', 1, ranges) - }; -} -/** - * Create a `TimeRange` object which mimics an - * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}. - * - * @param {number|Array[]} start - * The start of a single range (a number) or an array of ranges (an - * array of arrays of two numbers each). - * - * @param {number} end - * The end of a single range. Cannot be used with the array form of - * the `start` argument. - */ - - -function createTimeRanges(start, end) { - if (Array.isArray(start)) { - return createTimeRangesObj(start); - } else if (start === undefined || end === undefined) { - return createTimeRangesObj(); - } - - return createTimeRangesObj([[start, end]]); -} - -/** - * @file buffer.js - * @module buffer - */ -/** - * Compute the percentage of the media that has been buffered. - * - * @param {TimeRange} buffered - * The current `TimeRange` object representing buffered time ranges - * - * @param {number} duration - * Total duration of the media - * - * @return {number} - * Percent buffered of the total duration in decimal form. - */ - -function bufferedPercent(buffered, duration) { - var bufferedDuration = 0; - var start; - var end; - - if (!duration) { - return 0; - } - - if (!buffered || !buffered.length) { - buffered = createTimeRanges(0, 0); - } - - for (var i = 0; i < buffered.length; i++) { - start = buffered.start(i); - end = buffered.end(i); // buffered end can be bigger than duration by a very small fraction - - if (end > duration) { - end = duration; - } - - bufferedDuration += end - start; - } - - return bufferedDuration / duration; -} - -/** - * @file fullscreen-api.js - * @module fullscreen-api - * @private - */ -/** - * Store the browser-specific methods for the fullscreen API. - * - * @type {Object} - * @see [Specification]{@link https://fullscreen.spec.whatwg.org} - * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js} - */ - -var FullscreenApi = { - prefixed: true -}; // browser API methods - -var apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror', 'fullscreen'], // WebKit -['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror', '-webkit-full-screen'], // Mozilla -['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozFullScreenEnabled', 'mozfullscreenchange', 'mozfullscreenerror', '-moz-full-screen'], // Microsoft -['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'msFullscreenEnabled', 'MSFullscreenChange', 'MSFullscreenError', '-ms-fullscreen']]; -var specApi = apiMap[0]; -var browserApi; // determine the supported set of functions - -for (var i = 0; i < apiMap.length; i++) { - // check for exitFullscreen function - if (apiMap[i][1] in document_1) { - browserApi = apiMap[i]; - break; - } -} // map the browser API names to the spec API names - - -if (browserApi) { - for (var _i = 0; _i < browserApi.length; _i++) { - FullscreenApi[specApi[_i]] = browserApi[_i]; - } - - FullscreenApi.prefixed = browserApi[0] !== specApi[0]; -} - -/** - * @file media-error.js - */ -/** - * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class. - * - * @param {number|string|Object|MediaError} value - * This can be of multiple types: - * - number: should be a standard error code - * - string: an error message (the code will be 0) - * - Object: arbitrary properties - * - `MediaError` (native): used to populate a video.js `MediaError` object - * - `MediaError` (video.js): will return itself if it's already a - * video.js `MediaError` object. - * - * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror} - * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes} - * - * @class MediaError - */ - -function MediaError(value) { - // Allow redundant calls to this constructor to avoid having `instanceof` - // checks peppered around the code. - if (value instanceof MediaError) { - return value; - } - - if (typeof value === 'number') { - this.code = value; - } else if (typeof value === 'string') { - // default code is zero, so this is a custom error - this.message = value; - } else if (isObject$1(value)) { - // We assign the `code` property manually because native `MediaError` objects - // do not expose it as an own/enumerable property of the object. - if (typeof value.code === 'number') { - this.code = value.code; - } - - assign(this, value); - } - - if (!this.message) { - this.message = MediaError.defaultMessages[this.code] || ''; - } -} -/** - * The error code that refers two one of the defined `MediaError` types - * - * @type {Number} - */ - - -MediaError.prototype.code = 0; -/** - * An optional message that to show with the error. Message is not part of the HTML5 - * video spec but allows for more informative custom errors. - * - * @type {String} - */ - -MediaError.prototype.message = ''; -/** - * An optional status code that can be set by plugins to allow even more detail about - * the error. For example a plugin might provide a specific HTTP status code and an - * error message for that code. Then when the plugin gets that error this class will - * know how to display an error message for it. This allows a custom message to show - * up on the `Player` error overlay. - * - * @type {Array} - */ - -MediaError.prototype.status = null; -/** - * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the - * specification listed under {@link MediaError} for more information. - * - * @enum {array} - * @readonly - * @property {string} 0 - MEDIA_ERR_CUSTOM - * @property {string} 1 - MEDIA_ERR_ABORTED - * @property {string} 2 - MEDIA_ERR_NETWORK - * @property {string} 3 - MEDIA_ERR_DECODE - * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED - * @property {string} 5 - MEDIA_ERR_ENCRYPTED - */ - -MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED']; -/** - * The default `MediaError` messages based on the {@link MediaError.errorTypes}. - * - * @type {Array} - * @constant - */ - -MediaError.defaultMessages = { - 1: 'You aborted the media playback', - 2: 'A network error caused the media download to fail part-way.', - 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.', - 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.', - 5: 'The media is encrypted and we do not have the keys to decrypt it.' -}; // Add types as properties on MediaError -// e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; - -for (var errNum = 0; errNum < MediaError.errorTypes.length; errNum++) { - MediaError[MediaError.errorTypes[errNum]] = errNum; // values should be accessible on both the class and instance - - MediaError.prototype[MediaError.errorTypes[errNum]] = errNum; -} // jsdocs for instance/static members added above - -/** - * Returns whether an object is `Promise`-like (i.e. has a `then` method). - * - * @param {Object} value - * An object that may or may not be `Promise`-like. - * - * @return {boolean} - * Whether or not the object is `Promise`-like. - */ -function isPromise(value) { - return value !== undefined && value !== null && typeof value.then === 'function'; -} -/** - * Silence a Promise-like object. - * - * This is useful for avoiding non-harmful, but potentially confusing "uncaught - * play promise" rejection error messages. - * - * @param {Object} value - * An object that may or may not be `Promise`-like. - */ - -function silencePromise(value) { - if (isPromise(value)) { - value.then(null, function (e) {}); - } -} - -/** - * @file text-track-list-converter.js Utilities for capturing text track state and - * re-creating tracks based on a capture. - * - * @module text-track-list-converter - */ - -/** - * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that - * represents the {@link TextTrack}'s state. - * - * @param {TextTrack} track - * The text track to query. - * - * @return {Object} - * A serializable javascript representation of the TextTrack. - * @private - */ -var trackToJson_ = function trackToJson_(track) { - var ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce(function (acc, prop, i) { - if (track[prop]) { - acc[prop] = track[prop]; - } - - return acc; - }, { - cues: track.cues && Array.prototype.map.call(track.cues, function (cue) { - return { - startTime: cue.startTime, - endTime: cue.endTime, - text: cue.text, - id: cue.id - }; - }) - }); - return ret; -}; -/** - * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the - * state of all {@link TextTrack}s currently configured. The return array is compatible with - * {@link text-track-list-converter:jsonToTextTracks}. - * - * @param {Tech} tech - * The tech object to query - * - * @return {Array} - * A serializable javascript representation of the {@link Tech}s - * {@link TextTrackList}. - */ - - -var textTracksToJson = function textTracksToJson(tech) { - var trackEls = tech.$$('track'); - var trackObjs = Array.prototype.map.call(trackEls, function (t) { - return t.track; - }); - var tracks = Array.prototype.map.call(trackEls, function (trackEl) { - var json = trackToJson_(trackEl.track); - - if (trackEl.src) { - json.src = trackEl.src; - } - - return json; - }); - return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) { - return trackObjs.indexOf(track) === -1; - }).map(trackToJson_)); -}; -/** - * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript - * object {@link TextTrack} representations. - * - * @param {Array} json - * An array of `TextTrack` representation objects, like those that would be - * produced by `textTracksToJson`. - * - * @param {Tech} tech - * The `Tech` to create the `TextTrack`s on. - */ - - -var jsonToTextTracks = function jsonToTextTracks(json, tech) { - json.forEach(function (track) { - var addedTrack = tech.addRemoteTextTrack(track).track; - - if (!track.src && track.cues) { - track.cues.forEach(function (cue) { - return addedTrack.addCue(cue); - }); - } - }); - return tech.textTracks(); -}; - -var textTrackConverter = { - textTracksToJson: textTracksToJson, - jsonToTextTracks: jsonToTextTracks, - trackToJson_: trackToJson_ -}; - -var MODAL_CLASS_NAME = 'vjs-modal-dialog'; -/** - * The `ModalDialog` displays over the video and its controls, which blocks - * interaction with the player until it is closed. - * - * Modal dialogs include a "Close" button and will close when that button - * is activated - or when ESC is pressed anywhere. - * - * @extends Component - */ - -var ModalDialog = /*#__PURE__*/function (_Component) { - inheritsLoose(ModalDialog, _Component); - - /** - * Create an instance of this class. - * - * @param {Player} player - * The `Player` that this class should be attached to. - * - * @param {Object} [options] - * The key/value store of player options. - * - * @param {Mixed} [options.content=undefined] - * Provide customized content for this modal. - * - * @param {string} [options.description] - * A text description for the modal, primarily for accessibility. - * - * @param {boolean} [options.fillAlways=false] - * Normally, modals are automatically filled only the first time - * they open. This tells the modal to refresh its content - * every time it opens. - * - * @param {string} [options.label] - * A text label for the modal, primarily for accessibility. - * - * @param {boolean} [options.pauseOnOpen=true] - * If `true`, playback will will be paused if playing when - * the modal opens, and resumed when it closes. - * - * @param {boolean} [options.temporary=true] - * If `true`, the modal can only be opened once; it will be - * disposed as soon as it's closed. - * - * @param {boolean} [options.uncloseable=false] - * If `true`, the user will not be able to close the modal - * through the UI in the normal ways. Programmatic closing is - * still possible. - */ - function ModalDialog(player, options) { - var _this; - - _this = _Component.call(this, player, options) || this; - _this.opened_ = _this.hasBeenOpened_ = _this.hasBeenFilled_ = false; - - _this.closeable(!_this.options_.uncloseable); - - _this.content(_this.options_.content); // Make sure the contentEl is defined AFTER any children are initialized - // because we only want the contents of the modal in the contentEl - // (not the UI elements like the close button). - - - _this.contentEl_ = createEl('div', { - className: MODAL_CLASS_NAME + "-content" - }, { - role: 'document' - }); - _this.descEl_ = createEl('p', { - className: MODAL_CLASS_NAME + "-description vjs-control-text", - id: _this.el().getAttribute('aria-describedby') - }); - textContent(_this.descEl_, _this.description()); - - _this.el_.appendChild(_this.descEl_); - - _this.el_.appendChild(_this.contentEl_); - - return _this; - } - /** - * Create the `ModalDialog`'s DOM element - * - * @return {Element} - * The DOM element that gets created. - */ - - - var _proto = ModalDialog.prototype; - - _proto.createEl = function createEl() { - return _Component.prototype.createEl.call(this, 'div', { - className: this.buildCSSClass(), - tabIndex: -1 - }, { - 'aria-describedby': this.id() + "_description", - 'aria-hidden': 'true', - 'aria-label': this.label(), - 'role': 'dialog' - }); - }; - - _proto.dispose = function dispose() { - this.contentEl_ = null; - this.descEl_ = null; - this.previouslyActiveEl_ = null; - - _Component.prototype.dispose.call(this); - } - /** - * Builds the default DOM `className`. - * - * @return {string} - * The DOM `className` for this object. - */ - ; - - _proto.buildCSSClass = function buildCSSClass() { - return MODAL_CLASS_NAME + " vjs-hidden " + _Component.prototype.buildCSSClass.call(this); - } - /** - * Returns the label string for this modal. Primarily used for accessibility. - * - * @return {string} - * the localized or raw label of this modal. - */ - ; - - _proto.label = function label() { - return this.localize(this.options_.label || 'Modal Window'); - } - /** - * Returns the description string for this modal. Primarily used for - * accessibility. - * - * @return {string} - * The localized or raw description of this modal. - */ - ; - - _proto.description = function description() { - var desc = this.options_.description || this.localize('This is a modal window.'); // Append a universal closeability message if the modal is closeable. - - if (this.closeable()) { - desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.'); - } - - return desc; - } - /** - * Opens the modal. - * - * @fires ModalDialog#beforemodalopen - * @fires ModalDialog#modalopen - */ - ; - - _proto.open = function open() { - if (!this.opened_) { - var player = this.player(); - /** - * Fired just before a `ModalDialog` is opened. - * - * @event ModalDialog#beforemodalopen - * @type {EventTarget~Event} - */ - - this.trigger('beforemodalopen'); - this.opened_ = true; // Fill content if the modal has never opened before and - // never been filled. - - if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) { - this.fill(); - } // If the player was playing, pause it and take note of its previously - // playing state. - - - this.wasPlaying_ = !player.paused(); - - if (this.options_.pauseOnOpen && this.wasPlaying_) { - player.pause(); - } - - this.on('keydown', this.handleKeyDown); // Hide controls and note if they were enabled. - - this.hadControls_ = player.controls(); - player.controls(false); - this.show(); - this.conditionalFocus_(); - this.el().setAttribute('aria-hidden', 'false'); - /** - * Fired just after a `ModalDialog` is opened. - * - * @event ModalDialog#modalopen - * @type {EventTarget~Event} - */ - - this.trigger('modalopen'); - this.hasBeenOpened_ = true; - } - } - /** - * If the `ModalDialog` is currently open or closed. - * - * @param {boolean} [value] - * If given, it will open (`true`) or close (`false`) the modal. - * - * @return {boolean} - * the current open state of the modaldialog - */ - ; - - _proto.opened = function opened(value) { - if (typeof value === 'boolean') { - this[value ? 'open' : 'close'](); - } - - return this.opened_; - } - /** - * Closes the modal, does nothing if the `ModalDialog` is - * not open. - * - * @fires ModalDialog#beforemodalclose - * @fires ModalDialog#modalclose - */ - ; - - _proto.close = function close() { - if (!this.opened_) { - return; - } - - var player = this.player(); - /** - * Fired just before a `ModalDialog` is closed. - * - * @event ModalDialog#beforemodalclose - * @type {EventTarget~Event} - */ - - this.trigger('beforemodalclose'); - this.opened_ = false; - - if (this.wasPlaying_ && this.options_.pauseOnOpen) { - player.play(); - } - - this.off('keydown', this.handleKeyDown); - - if (this.hadControls_) { - player.controls(true); - } - - this.hide(); - this.el().setAttribute('aria-hidden', 'true'); - /** - * Fired just after a `ModalDialog` is closed. - * - * @event ModalDialog#modalclose - * @type {EventTarget~Event} - */ - - this.trigger('modalclose'); - this.conditionalBlur_(); - - if (this.options_.temporary) { - this.dispose(); - } - } - /** - * Check to see if the `ModalDialog` is closeable via the UI. - * - * @param {boolean} [value] - * If given as a boolean, it will set the `closeable` option. - * - * @return {boolean} - * Returns the final value of the closable option. - */ - ; - - _proto.closeable = function closeable(value) { - if (typeof value === 'boolean') { - var closeable = this.closeable_ = !!value; - var close = this.getChild('closeButton'); // If this is being made closeable and has no close button, add one. - - if (closeable && !close) { - // The close button should be a child of the modal - not its - // content element, so temporarily change the content element. - var temp = this.contentEl_; - this.contentEl_ = this.el_; - close = this.addChild('closeButton', { - controlText: 'Close Modal Dialog' - }); - this.contentEl_ = temp; - this.on(close, 'close', this.close); - } // If this is being made uncloseable and has a close button, remove it. - - - if (!closeable && close) { - this.off(close, 'close', this.close); - this.removeChild(close); - close.dispose(); - } - } - - return this.closeable_; - } - /** - * Fill the modal's content element with the modal's "content" option. - * The content element will be emptied before this change takes place. - */ - ; - - _proto.fill = function fill() { - this.fillWith(this.content()); - } - /** - * Fill the modal's content element with arbitrary content. - * The content element will be emptied before this change takes place. - * - * @fires ModalDialog#beforemodalfill - * @fires ModalDialog#modalfill - * - * @param {Mixed} [content] - * The same rules apply to this as apply to the `content` option. - */ - ; - - _proto.fillWith = function fillWith(content) { - var contentEl = this.contentEl(); - var parentEl = contentEl.parentNode; - var nextSiblingEl = contentEl.nextSibling; - /** - * Fired just before a `ModalDialog` is filled with content. - * - * @event ModalDialog#beforemodalfill - * @type {EventTarget~Event} - */ - - this.trigger('beforemodalfill'); - this.hasBeenFilled_ = true; // Detach the content element from the DOM before performing - // manipulation to avoid modifying the live DOM multiple times. - - parentEl.removeChild(contentEl); - this.empty(); - insertContent(contentEl, content); - /** - * Fired just after a `ModalDialog` is filled with content. - * - * @event ModalDialog#modalfill - * @type {EventTarget~Event} - */ - - this.trigger('modalfill'); // Re-inject the re-filled content element. - - if (nextSiblingEl) { - parentEl.insertBefore(contentEl, nextSiblingEl); - } else { - parentEl.appendChild(contentEl); - } // make sure that the close button is last in the dialog DOM - - - var closeButton = this.getChild('closeButton'); - - if (closeButton) { - parentEl.appendChild(closeButton.el_); - } - } - /** - * Empties the content element. This happens anytime the modal is filled. - * - * @fires ModalDialog#beforemodalempty - * @fires ModalDialog#modalempty - */ - ; - - _proto.empty = function empty() { - /** - * Fired just before a `ModalDialog` is emptied. - * - * @event ModalDialog#beforemodalempty - * @type {EventTarget~Event} - */ - this.trigger('beforemodalempty'); - emptyEl(this.contentEl()); - /** - * Fired just after a `ModalDialog` is emptied. - * - * @event ModalDialog#modalempty - * @type {EventTarget~Event} - */ - - this.trigger('modalempty'); - } - /** - * Gets or sets the modal content, which gets normalized before being - * rendered into the DOM. - * - * This does not update the DOM or fill the modal, but it is called during - * that process. - * - * @param {Mixed} [value] - * If defined, sets the internal content value to be used on the - * next call(s) to `fill`. This value is normalized before being - * inserted. To "clear" the internal content value, pass `null`. - * - * @return {Mixed} - * The current content of the modal dialog - */ - ; - - _proto.content = function content(value) { - if (typeof value !== 'undefined') { - this.content_ = value; - } - - return this.content_; - } - /** - * conditionally focus the modal dialog if focus was previously on the player. - * - * @private - */ - ; - - _proto.conditionalFocus_ = function conditionalFocus_() { - var activeEl = document_1.activeElement; - var playerEl = this.player_.el_; - this.previouslyActiveEl_ = null; - - if (playerEl.contains(activeEl) || playerEl === activeEl) { - this.previouslyActiveEl_ = activeEl; - this.focus(); - } - } - /** - * conditionally blur the element and refocus the last focused element - * - * @private - */ - ; - - _proto.conditionalBlur_ = function conditionalBlur_() { - if (this.previouslyActiveEl_) { - this.previouslyActiveEl_.focus(); - this.previouslyActiveEl_ = null; - } - } - /** - * Keydown handler. Attached when modal is focused. - * - * @listens keydown - */ - ; - - _proto.handleKeyDown = function handleKeyDown(event) { - // Do not allow keydowns to reach out of the modal dialog. - event.stopPropagation(); - - if (keycode.isEventKey(event, 'Escape') && this.closeable()) { - event.preventDefault(); - this.close(); - return; - } // exit early if it isn't a tab key - - - if (!keycode.isEventKey(event, 'Tab')) { - return; - } - - var focusableEls = this.focusableEls_(); - var activeEl = this.el_.querySelector(':focus'); - var focusIndex; - - for (var i = 0; i < focusableEls.length; i++) { - if (activeEl === focusableEls[i]) { - focusIndex = i; - break; - } - } - - if (document_1.activeElement === this.el_) { - focusIndex = 0; - } - - if (event.shiftKey && focusIndex === 0) { - focusableEls[focusableEls.length - 1].focus(); - event.preventDefault(); - } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) { - focusableEls[0].focus(); - event.preventDefault(); - } - } - /** - * get all focusable elements - * - * @private - */ - ; - - _proto.focusableEls_ = function focusableEls_() { - var allChildren = this.el_.querySelectorAll('*'); - return Array.prototype.filter.call(allChildren, function (child) { - return (child instanceof window_1$1.HTMLAnchorElement || child instanceof window_1$1.HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window_1$1.HTMLInputElement || child instanceof window_1$1.HTMLSelectElement || child instanceof window_1$1.HTMLTextAreaElement || child instanceof window_1$1.HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window_1$1.HTMLIFrameElement || child instanceof window_1$1.HTMLObjectElement || child instanceof window_1$1.HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable'); - }); - }; - - return ModalDialog; -}(Component); -/** - * Default options for `ModalDialog` default options. - * - * @type {Object} - * @private - */ - - -ModalDialog.prototype.options_ = { - pauseOnOpen: true, - temporary: true -}; -Component.registerComponent('ModalDialog', ModalDialog); - -/** - * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and - * {@link VideoTrackList} - * - * @extends EventTarget - */ - -var TrackList = /*#__PURE__*/function (_EventTarget) { - inheritsLoose(TrackList, _EventTarget); - - /** - * Create an instance of this class - * - * @param {Track[]} tracks - * A list of tracks to initialize the list with. - * - * @abstract - */ - function TrackList(tracks) { - var _this; - - if (tracks === void 0) { - tracks = []; - } - - _this = _EventTarget.call(this) || this; - _this.tracks_ = []; - /** - * @memberof TrackList - * @member {number} length - * The current number of `Track`s in the this Trackist. - * @instance - */ - - Object.defineProperty(assertThisInitialized(_this), 'length', { - get: function get() { - return this.tracks_.length; - } - }); - - for (var i = 0; i < tracks.length; i++) { - _this.addTrack(tracks[i]); - } - - return _this; - } - /** - * Add a {@link Track} to the `TrackList` - * - * @param {Track} track - * The audio, video, or text track to add to the list. - * - * @fires TrackList#addtrack - */ - - - var _proto = TrackList.prototype; - - _proto.addTrack = function addTrack(track) { - var index = this.tracks_.length; - - if (!('' + index in this)) { - Object.defineProperty(this, index, { - get: function get() { - return this.tracks_[index]; - } - }); - } // Do not add duplicate tracks - - - if (this.tracks_.indexOf(track) === -1) { - this.tracks_.push(track); - /** - * Triggered when a track is added to a track list. - * - * @event TrackList#addtrack - * @type {EventTarget~Event} - * @property {Track} track - * A reference to track that was added. - */ - - this.trigger({ - track: track, - type: 'addtrack', - target: this - }); - } - } - /** - * Remove a {@link Track} from the `TrackList` - * - * @param {Track} rtrack - * The audio, video, or text track to remove from the list. - * - * @fires TrackList#removetrack - */ - ; - - _proto.removeTrack = function removeTrack(rtrack) { - var track; - - for (var i = 0, l = this.length; i < l; i++) { - if (this[i] === rtrack) { - track = this[i]; - - if (track.off) { - track.off(); - } - - this.tracks_.splice(i, 1); - break; - } - } - - if (!track) { - return; - } - /** - * Triggered when a track is removed from track list. - * - * @event TrackList#removetrack - * @type {EventTarget~Event} - * @property {Track} track - * A reference to track that was removed. - */ - - - this.trigger({ - track: track, - type: 'removetrack', - target: this - }); - } - /** - * Get a Track from the TrackList by a tracks id - * - * @param {string} id - the id of the track to get - * @method getTrackById - * @return {Track} - * @private - */ - ; - - _proto.getTrackById = function getTrackById(id) { - var result = null; - - for (var i = 0, l = this.length; i < l; i++) { - var track = this[i]; - - if (track.id === id) { - result = track; - break; - } - } - - return result; - }; - - return TrackList; -}(EventTarget); -/** - * Triggered when a different track is selected/enabled. - * - * @event TrackList#change - * @type {EventTarget~Event} - */ - -/** - * Events that can be called with on + eventName. See {@link EventHandler}. - * - * @property {Object} TrackList#allowedEvents_ - * @private - */ - - -TrackList.prototype.allowedEvents_ = { - change: 'change', - addtrack: 'addtrack', - removetrack: 'removetrack' -}; // emulate attribute EventHandler support to allow for feature detection - -for (var event in TrackList.prototype.allowedEvents_) { - TrackList.prototype['on' + event] = null; -} - -/** - * Anywhere we call this function we diverge from the spec - * as we only support one enabled audiotrack at a time - * - * @param {AudioTrackList} list - * list to work on - * - * @param {AudioTrack} track - * The track to skip - * - * @private - */ - -var disableOthers = function disableOthers(list, track) { - for (var i = 0; i < list.length; i++) { - if (!Object.keys(list[i]).length || track.id === list[i].id) { - continue; - } // another audio track is enabled, disable it - - - list[i].enabled = false; - } -}; -/** - * The current list of {@link AudioTrack} for a media file. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist} - * @extends TrackList - */ - - -var AudioTrackList = /*#__PURE__*/function (_TrackList) { - inheritsLoose(AudioTrackList, _TrackList); - - /** - * Create an instance of this class. - * - * @param {AudioTrack[]} [tracks=[]] - * A list of `AudioTrack` to instantiate the list with. - */ - function AudioTrackList(tracks) { - var _this; - - if (tracks === void 0) { - tracks = []; - } - - // make sure only 1 track is enabled - // sorted from last index to first index - for (var i = tracks.length - 1; i >= 0; i--) { - if (tracks[i].enabled) { - disableOthers(tracks, tracks[i]); - break; - } - } - - _this = _TrackList.call(this, tracks) || this; - _this.changing_ = false; - return _this; - } - /** - * Add an {@link AudioTrack} to the `AudioTrackList`. - * - * @param {AudioTrack} track - * The AudioTrack to add to the list - * - * @fires TrackList#addtrack - */ - - - var _proto = AudioTrackList.prototype; - - _proto.addTrack = function addTrack(track) { - var _this2 = this; - - if (track.enabled) { - disableOthers(this, track); - } - - _TrackList.prototype.addTrack.call(this, track); // native tracks don't have this - - - if (!track.addEventListener) { - return; - } - - track.enabledChange_ = function () { - // when we are disabling other tracks (since we don't support - // more than one track at a time) we will set changing_ - // to true so that we don't trigger additional change events - if (_this2.changing_) { - return; - } - - _this2.changing_ = true; - disableOthers(_this2, track); - _this2.changing_ = false; - - _this2.trigger('change'); - }; - /** - * @listens AudioTrack#enabledchange - * @fires TrackList#change - */ - - - track.addEventListener('enabledchange', track.enabledChange_); - }; - - _proto.removeTrack = function removeTrack(rtrack) { - _TrackList.prototype.removeTrack.call(this, rtrack); - - if (rtrack.removeEventListener && rtrack.enabledChange_) { - rtrack.removeEventListener('enabledchange', rtrack.enabledChange_); - rtrack.enabledChange_ = null; - } - }; - - return AudioTrackList; -}(TrackList); - -/** - * Un-select all other {@link VideoTrack}s that are selected. - * - * @param {VideoTrackList} list - * list to work on - * - * @param {VideoTrack} track - * The track to skip - * - * @private - */ - -var disableOthers$1 = function disableOthers(list, track) { - for (var i = 0; i < list.length; i++) { - if (!Object.keys(list[i]).length || track.id === list[i].id) { - continue; - } // another video track is enabled, disable it - - - list[i].selected = false; - } -}; -/** - * The current list of {@link VideoTrack} for a video. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist} - * @extends TrackList - */ - - -var VideoTrackList = /*#__PURE__*/function (_TrackList) { - inheritsLoose(VideoTrackList, _TrackList); - - /** - * Create an instance of this class. - * - * @param {VideoTrack[]} [tracks=[]] - * A list of `VideoTrack` to instantiate the list with. - */ - function VideoTrackList(tracks) { - var _this; - - if (tracks === void 0) { - tracks = []; - } - - // make sure only 1 track is enabled - // sorted from last index to first index - for (var i = tracks.length - 1; i >= 0; i--) { - if (tracks[i].selected) { - disableOthers$1(tracks, tracks[i]); - break; - } - } - - _this = _TrackList.call(this, tracks) || this; - _this.changing_ = false; - /** - * @member {number} VideoTrackList#selectedIndex - * The current index of the selected {@link VideoTrack`}. - */ - - Object.defineProperty(assertThisInitialized(_this), 'selectedIndex', { - get: function get() { - for (var _i = 0; _i < this.length; _i++) { - if (this[_i].selected) { - return _i; - } - } - - return -1; - }, - set: function set() {} - }); - return _this; - } - /** - * Add a {@link VideoTrack} to the `VideoTrackList`. - * - * @param {VideoTrack} track - * The VideoTrack to add to the list - * - * @fires TrackList#addtrack - */ - - - var _proto = VideoTrackList.prototype; - - _proto.addTrack = function addTrack(track) { - var _this2 = this; - - if (track.selected) { - disableOthers$1(this, track); - } - - _TrackList.prototype.addTrack.call(this, track); // native tracks don't have this - - - if (!track.addEventListener) { - return; - } - - track.selectedChange_ = function () { - if (_this2.changing_) { - return; - } - - _this2.changing_ = true; - disableOthers$1(_this2, track); - _this2.changing_ = false; - - _this2.trigger('change'); - }; - /** - * @listens VideoTrack#selectedchange - * @fires TrackList#change - */ - - - track.addEventListener('selectedchange', track.selectedChange_); - }; - - _proto.removeTrack = function removeTrack(rtrack) { - _TrackList.prototype.removeTrack.call(this, rtrack); - - if (rtrack.removeEventListener && rtrack.selectedChange_) { - rtrack.removeEventListener('selectedchange', rtrack.selectedChange_); - rtrack.selectedChange_ = null; - } - }; - - return VideoTrackList; -}(TrackList); - -/** - * The current list of {@link TextTrack} for a media file. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist} - * @extends TrackList - */ - -var TextTrackList = /*#__PURE__*/function (_TrackList) { - inheritsLoose(TextTrackList, _TrackList); - - function TextTrackList() { - return _TrackList.apply(this, arguments) || this; - } - - var _proto = TextTrackList.prototype; - - /** - * Add a {@link TextTrack} to the `TextTrackList` - * - * @param {TextTrack} track - * The text track to add to the list. - * - * @fires TrackList#addtrack - */ - _proto.addTrack = function addTrack(track) { - var _this = this; - - _TrackList.prototype.addTrack.call(this, track); - - if (!this.queueChange_) { - this.queueChange_ = function () { - return _this.queueTrigger('change'); - }; - } - - if (!this.triggerSelectedlanguagechange) { - this.triggerSelectedlanguagechange_ = function () { - return _this.trigger('selectedlanguagechange'); - }; - } - /** - * @listens TextTrack#modechange - * @fires TrackList#change - */ - - - track.addEventListener('modechange', this.queueChange_); - var nonLanguageTextTrackKind = ['metadata', 'chapters']; - - if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) { - track.addEventListener('modechange', this.triggerSelectedlanguagechange_); - } - }; - - _proto.removeTrack = function removeTrack(rtrack) { - _TrackList.prototype.removeTrack.call(this, rtrack); // manually remove the event handlers we added - - - if (rtrack.removeEventListener) { - if (this.queueChange_) { - rtrack.removeEventListener('modechange', this.queueChange_); - } - - if (this.selectedlanguagechange_) { - rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_); - } - } - }; - - return TextTrackList; -}(TrackList); - -/** - * @file html-track-element-list.js - */ - -/** - * The current list of {@link HtmlTrackElement}s. - */ -var HtmlTrackElementList = /*#__PURE__*/function () { - /** - * Create an instance of this class. - * - * @param {HtmlTrackElement[]} [tracks=[]] - * A list of `HtmlTrackElement` to instantiate the list with. - */ - function HtmlTrackElementList(trackElements) { - if (trackElements === void 0) { - trackElements = []; - } - - this.trackElements_ = []; - /** - * @memberof HtmlTrackElementList - * @member {number} length - * The current number of `Track`s in the this Trackist. - * @instance - */ - - Object.defineProperty(this, 'length', { - get: function get() { - return this.trackElements_.length; - } - }); - - for (var i = 0, length = trackElements.length; i < length; i++) { - this.addTrackElement_(trackElements[i]); - } - } - /** - * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList` - * - * @param {HtmlTrackElement} trackElement - * The track element to add to the list. - * - * @private - */ - - - var _proto = HtmlTrackElementList.prototype; - - _proto.addTrackElement_ = function addTrackElement_(trackElement) { - var index = this.trackElements_.length; - - if (!('' + index in this)) { - Object.defineProperty(this, index, { - get: function get() { - return this.trackElements_[index]; - } - }); - } // Do not add duplicate elements - - - if (this.trackElements_.indexOf(trackElement) === -1) { - this.trackElements_.push(trackElement); - } - } - /** - * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an - * {@link TextTrack}. - * - * @param {TextTrack} track - * The track associated with a track element. - * - * @return {HtmlTrackElement|undefined} - * The track element that was found or undefined. - * - * @private - */ - ; - - _proto.getTrackElementByTrack_ = function getTrackElementByTrack_(track) { - var trackElement_; - - for (var i = 0, length = this.trackElements_.length; i < length; i++) { - if (track === this.trackElements_[i].track) { - trackElement_ = this.trackElements_[i]; - break; - } - } - - return trackElement_; - } - /** - * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList` - * - * @param {HtmlTrackElement} trackElement - * The track element to remove from the list. - * - * @private - */ - ; - - _proto.removeTrackElement_ = function removeTrackElement_(trackElement) { - for (var i = 0, length = this.trackElements_.length; i < length; i++) { - if (trackElement === this.trackElements_[i]) { - if (this.trackElements_[i].track && typeof this.trackElements_[i].track.off === 'function') { - this.trackElements_[i].track.off(); - } - - if (typeof this.trackElements_[i].off === 'function') { - this.trackElements_[i].off(); - } - - this.trackElements_.splice(i, 1); - break; - } - } - }; - - return HtmlTrackElementList; -}(); - -/** - * @file text-track-cue-list.js - */ - -/** - * @typedef {Object} TextTrackCueList~TextTrackCue - * - * @property {string} id - * The unique id for this text track cue - * - * @property {number} startTime - * The start time for this text track cue - * - * @property {number} endTime - * The end time for this text track cue - * - * @property {boolean} pauseOnExit - * Pause when the end time is reached if true. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue} - */ - -/** - * A List of TextTrackCues. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist} - */ -var TextTrackCueList = /*#__PURE__*/function () { - /** - * Create an instance of this class.. - * - * @param {Array} cues - * A list of cues to be initialized with - */ - function TextTrackCueList(cues) { - TextTrackCueList.prototype.setCues_.call(this, cues); - /** - * @memberof TextTrackCueList - * @member {number} length - * The current number of `TextTrackCue`s in the TextTrackCueList. - * @instance - */ - - Object.defineProperty(this, 'length', { - get: function get() { - return this.length_; - } - }); - } - /** - * A setter for cues in this list. Creates getters - * an an index for the cues. - * - * @param {Array} cues - * An array of cues to set - * - * @private - */ - - - var _proto = TextTrackCueList.prototype; - - _proto.setCues_ = function setCues_(cues) { - var oldLength = this.length || 0; - var i = 0; - var l = cues.length; - this.cues_ = cues; - this.length_ = cues.length; - - var defineProp = function defineProp(index) { - if (!('' + index in this)) { - Object.defineProperty(this, '' + index, { - get: function get() { - return this.cues_[index]; - } - }); - } - }; - - if (oldLength < l) { - i = oldLength; - - for (; i < l; i++) { - defineProp.call(this, i); - } - } - } - /** - * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id. - * - * @param {string} id - * The id of the cue that should be searched for. - * - * @return {TextTrackCueList~TextTrackCue|null} - * A single cue or null if none was found. - */ - ; - - _proto.getCueById = function getCueById(id) { - var result = null; - - for (var i = 0, l = this.length; i < l; i++) { - var cue = this[i]; - - if (cue.id === id) { - result = cue; - break; - } - } - - return result; - }; - - return TextTrackCueList; -}(); - -/** - * @file track-kinds.js - */ - -/** - * All possible `VideoTrackKind`s - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind - * @typedef VideoTrack~Kind - * @enum - */ -var VideoTrackKind = { - alternative: 'alternative', - captions: 'captions', - main: 'main', - sign: 'sign', - subtitles: 'subtitles', - commentary: 'commentary' -}; -/** - * All possible `AudioTrackKind`s - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind - * @typedef AudioTrack~Kind - * @enum - */ - -var AudioTrackKind = { - 'alternative': 'alternative', - 'descriptions': 'descriptions', - 'main': 'main', - 'main-desc': 'main-desc', - 'translation': 'translation', - 'commentary': 'commentary' -}; -/** - * All possible `TextTrackKind`s - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind - * @typedef TextTrack~Kind - * @enum - */ - -var TextTrackKind = { - subtitles: 'subtitles', - captions: 'captions', - descriptions: 'descriptions', - chapters: 'chapters', - metadata: 'metadata' -}; -/** - * All possible `TextTrackMode`s - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode - * @typedef TextTrack~Mode - * @enum - */ - -var TextTrackMode = { - disabled: 'disabled', - hidden: 'hidden', - showing: 'showing' -}; - -/** - * A Track class that contains all of the common functionality for {@link AudioTrack}, - * {@link VideoTrack}, and {@link TextTrack}. - * - * > Note: This class should not be used directly - * - * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html} - * @extends EventTarget - * @abstract - */ - -var Track = /*#__PURE__*/function (_EventTarget) { - inheritsLoose(Track, _EventTarget); - - /** - * Create an instance of this class. - * - * @param {Object} [options={}] - * Object of option names and values - * - * @param {string} [options.kind=''] - * A valid kind for the track type you are creating. - * - * @param {string} [options.id='vjs_track_' + Guid.newGUID()] - * A unique id for this AudioTrack. - * - * @param {string} [options.label=''] - * The menu label for this track. - * - * @param {string} [options.language=''] - * A valid two character language code. - * - * @abstract - */ - function Track(options) { - var _this; - - if (options === void 0) { - options = {}; - } - - _this = _EventTarget.call(this) || this; - var trackProps = { - id: options.id || 'vjs_track_' + newGUID(), - kind: options.kind || '', - label: options.label || '', - language: options.language || '' - }; - /** - * @memberof Track - * @member {string} id - * The id of this track. Cannot be changed after creation. - * @instance - * - * @readonly - */ - - /** - * @memberof Track - * @member {string} kind - * The kind of track that this is. Cannot be changed after creation. - * @instance - * - * @readonly - */ - - /** - * @memberof Track - * @member {string} label - * The label of this track. Cannot be changed after creation. - * @instance - * - * @readonly - */ - - /** - * @memberof Track - * @member {string} language - * The two letter language code for this track. Cannot be changed after - * creation. - * @instance - * - * @readonly - */ - - var _loop = function _loop(key) { - Object.defineProperty(assertThisInitialized(_this), key, { - get: function get() { - return trackProps[key]; - }, - set: function set() {} - }); - }; - - for (var key in trackProps) { - _loop(key); - } - - return _this; - } - - return Track; -}(EventTarget); - -/** - * @file url.js - * @module url - */ -/** - * @typedef {Object} url:URLObject - * - * @property {string} protocol - * The protocol of the url that was parsed. - * - * @property {string} hostname - * The hostname of the url that was parsed. - * - * @property {string} port - * The port of the url that was parsed. - * - * @property {string} pathname - * The pathname of the url that was parsed. - * - * @property {string} search - * The search query of the url that was parsed. - * - * @property {string} hash - * The hash of the url that was parsed. - * - * @property {string} host - * The host of the url that was parsed. - */ - -/** - * Resolve and parse the elements of a URL. - * - * @function - * @param {String} url - * The url to parse - * - * @return {url:URLObject} - * An object of url details - */ - -var parseUrl = function parseUrl(url) { - var props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; // add the url to an anchor and let the browser parse the URL - - var a = document_1.createElement('a'); - a.href = url; // IE8 (and 9?) Fix - // ie8 doesn't parse the URL correctly until the anchor is actually - // added to the body, and an innerHTML is needed to trigger the parsing - - var addToBody = a.host === '' && a.protocol !== 'file:'; - var div; - - if (addToBody) { - div = document_1.createElement('div'); - div.innerHTML = ""; - a = div.firstChild; // prevent the div from affecting layout - - div.setAttribute('style', 'display:none; position:absolute;'); - document_1.body.appendChild(div); - } // Copy the specific URL properties to a new object - // This is also needed for IE8 because the anchor loses its - // properties when it's removed from the dom - - - var details = {}; - - for (var i = 0; i < props.length; i++) { - details[props[i]] = a[props[i]]; - } // IE9 adds the port to the host property unlike everyone else. If - // a port identifier is added for standard ports, strip it. - - - if (details.protocol === 'http:') { - details.host = details.host.replace(/:80$/, ''); - } - - if (details.protocol === 'https:') { - details.host = details.host.replace(/:443$/, ''); - } - - if (!details.protocol) { - details.protocol = window_1$1.location.protocol; - } - - if (addToBody) { - document_1.body.removeChild(div); - } - - return details; -}; -/** - * Get absolute version of relative URL. Used to tell Flash the correct URL. - * - * @function - * @param {string} url - * URL to make absolute - * - * @return {string} - * Absolute URL - * - * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue - */ - -var getAbsoluteURL = function getAbsoluteURL(url) { - // Check if absolute URL - if (!url.match(/^https?:\/\//)) { - // Convert to absolute URL. Flash hosted off-site needs an absolute URL. - var div = document_1.createElement('div'); - div.innerHTML = "x"; - url = div.firstChild.href; - } - - return url; -}; -/** - * Returns the extension of the passed file name. It will return an empty string - * if passed an invalid path. - * - * @function - * @param {string} path - * The fileName path like '/path/to/file.mp4' - * - * @return {string} - * The extension in lower case or an empty string if no - * extension could be found. - */ - -var getFileExtension = function getFileExtension(path) { - if (typeof path === 'string') { - var splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/; - var pathParts = splitPathRe.exec(path); - - if (pathParts) { - return pathParts.pop().toLowerCase(); - } - } - - return ''; -}; -/** - * Returns whether the url passed is a cross domain request or not. - * - * @function - * @param {string} url - * The url to check. - * - * @param {Object} [winLoc] - * the domain to check the url against, defaults to window.location - * - * @param {string} [winLoc.protocol] - * The window location protocol defaults to window.location.protocol - * - * @param {string} [winLoc.host] - * The window location host defaults to window.location.host - * - * @return {boolean} - * Whether it is a cross domain request or not. - */ - -var isCrossOrigin = function isCrossOrigin(url, winLoc) { - if (winLoc === void 0) { - winLoc = window_1$1.location; - } - - var urlInfo = parseUrl(url); // IE8 protocol relative urls will return ':' for protocol - - var srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol; // Check if url is for another domain/origin - // IE8 doesn't know location.origin, so we won't rely on it here - - var crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host; - return crossOrigin; -}; - -var Url = /*#__PURE__*/Object.freeze({ - __proto__: null, - parseUrl: parseUrl, - getAbsoluteURL: getAbsoluteURL, - getFileExtension: getFileExtension, - isCrossOrigin: isCrossOrigin -}); - -/** - * Takes a webvtt file contents and parses it into cues - * - * @param {string} srcContent - * webVTT file contents - * - * @param {TextTrack} track - * TextTrack to add cues to. Cues come from the srcContent. - * - * @private - */ - -var parseCues = function parseCues(srcContent, track) { - var parser = new window_1$1.WebVTT.Parser(window_1$1, window_1$1.vttjs, window_1$1.WebVTT.StringDecoder()); - var errors = []; - - parser.oncue = function (cue) { - track.addCue(cue); - }; - - parser.onparsingerror = function (error) { - errors.push(error); - }; - - parser.onflush = function () { - track.trigger({ - type: 'loadeddata', - target: track - }); - }; - - parser.parse(srcContent); - - if (errors.length > 0) { - if (window_1$1.console && window_1$1.console.groupCollapsed) { - window_1$1.console.groupCollapsed("Text Track parsing errors for " + track.src); - } - - errors.forEach(function (error) { - return log.error(error); - }); - - if (window_1$1.console && window_1$1.console.groupEnd) { - window_1$1.console.groupEnd(); - } - } - - parser.flush(); -}; -/** - * Load a `TextTrack` from a specified url. - * - * @param {string} src - * Url to load track from. - * - * @param {TextTrack} track - * Track to add cues to. Comes from the content at the end of `url`. - * - * @private - */ - - -var loadTrack = function loadTrack(src, track) { - var opts = { - uri: src - }; - var crossOrigin = isCrossOrigin(src); - - if (crossOrigin) { - opts.cors = crossOrigin; - } - - var withCredentials = track.tech_.crossOrigin() === 'use-credentials'; - - if (withCredentials) { - opts.withCredentials = withCredentials; - } - - xhr(opts, bind(this, function (err, response, responseBody) { - if (err) { - return log.error(err, response); - } - - track.loaded_ = true; // Make sure that vttjs has loaded, otherwise, wait till it finished loading - // NOTE: this is only used for the alt/video.novtt.js build - - if (typeof window_1$1.WebVTT !== 'function') { - if (track.tech_) { - // to prevent use before define eslint error, we define loadHandler - // as a let here - track.tech_.any(['vttjsloaded', 'vttjserror'], function (event) { - if (event.type === 'vttjserror') { - log.error("vttjs failed to load, stopping trying to process " + track.src); - return; - } - - return parseCues(responseBody, track); - }); - } - } else { - parseCues(responseBody, track); - } - })); -}; -/** - * A representation of a single `TextTrack`. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack} - * @extends Track - */ - - -var TextTrack = /*#__PURE__*/function (_Track) { - inheritsLoose(TextTrack, _Track); - - /** - * Create an instance of this class. - * - * @param {Object} options={} - * Object of option names and values - * - * @param {Tech} options.tech - * A reference to the tech that owns this TextTrack. - * - * @param {TextTrack~Kind} [options.kind='subtitles'] - * A valid text track kind. - * - * @param {TextTrack~Mode} [options.mode='disabled'] - * A valid text track mode. - * - * @param {string} [options.id='vjs_track_' + Guid.newGUID()] - * A unique id for this TextTrack. - * - * @param {string} [options.label=''] - * The menu label for this track. - * - * @param {string} [options.language=''] - * A valid two character language code. - * - * @param {string} [options.srclang=''] - * A valid two character language code. An alternative, but deprioritized - * version of `options.language` - * - * @param {string} [options.src] - * A url to TextTrack cues. - * - * @param {boolean} [options.default] - * If this track should default to on or off. - */ - function TextTrack(options) { - var _this; - - if (options === void 0) { - options = {}; - } - - if (!options.tech) { - throw new Error('A tech was not provided.'); - } - - var settings = mergeOptions(options, { - kind: TextTrackKind[options.kind] || 'subtitles', - language: options.language || options.srclang || '' - }); - var mode = TextTrackMode[settings.mode] || 'disabled'; - var default_ = settings["default"]; - - if (settings.kind === 'metadata' || settings.kind === 'chapters') { - mode = 'hidden'; - } - - _this = _Track.call(this, settings) || this; - _this.tech_ = settings.tech; - _this.cues_ = []; - _this.activeCues_ = []; - _this.preload_ = _this.tech_.preloadTextTracks !== false; - var cues = new TextTrackCueList(_this.cues_); - var activeCues = new TextTrackCueList(_this.activeCues_); - var changed = false; - var timeupdateHandler = bind(assertThisInitialized(_this), function () { - // Accessing this.activeCues for the side-effects of updating itself - // due to its nature as a getter function. Do not remove or cues will - // stop updating! - // Use the setter to prevent deletion from uglify (pure_getters rule) - this.activeCues = this.activeCues; - - if (changed) { - this.trigger('cuechange'); - changed = false; - } - }); - - if (mode !== 'disabled') { - _this.tech_.ready(function () { - _this.tech_.on('timeupdate', timeupdateHandler); - }, true); - } - - Object.defineProperties(assertThisInitialized(_this), { - /** - * @memberof TextTrack - * @member {boolean} default - * If this track was set to be on or off by default. Cannot be changed after - * creation. - * @instance - * - * @readonly - */ - "default": { - get: function get() { - return default_; - }, - set: function set() {} - }, - - /** - * @memberof TextTrack - * @member {string} mode - * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will - * not be set if setting to an invalid mode. - * @instance - * - * @fires TextTrack#modechange - */ - mode: { - get: function get() { - return mode; - }, - set: function set(newMode) { - var _this2 = this; - - if (!TextTrackMode[newMode]) { - return; - } - - mode = newMode; - - if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) { - // On-demand load. - loadTrack(this.src, this); - } - - if (mode !== 'disabled') { - this.tech_.ready(function () { - _this2.tech_.on('timeupdate', timeupdateHandler); - }, true); - } else { - this.tech_.off('timeupdate', timeupdateHandler); - } - /** - * An event that fires when mode changes on this track. This allows - * the TextTrackList that holds this track to act accordingly. - * - * > Note: This is not part of the spec! - * - * @event TextTrack#modechange - * @type {EventTarget~Event} - */ - - - this.trigger('modechange'); - } - }, - - /** - * @memberof TextTrack - * @member {TextTrackCueList} cues - * The text track cue list for this TextTrack. - * @instance - */ - cues: { - get: function get() { - if (!this.loaded_) { - return null; - } - - return cues; - }, - set: function set() {} - }, - - /** - * @memberof TextTrack - * @member {TextTrackCueList} activeCues - * The list text track cues that are currently active for this TextTrack. - * @instance - */ - activeCues: { - get: function get() { - if (!this.loaded_) { - return null; - } // nothing to do - - - if (this.cues.length === 0) { - return activeCues; - } - - var ct = this.tech_.currentTime(); - var active = []; - - for (var i = 0, l = this.cues.length; i < l; i++) { - var cue = this.cues[i]; - - if (cue.startTime <= ct && cue.endTime >= ct) { - active.push(cue); - } else if (cue.startTime === cue.endTime && cue.startTime <= ct && cue.startTime + 0.5 >= ct) { - active.push(cue); - } - } - - changed = false; - - if (active.length !== this.activeCues_.length) { - changed = true; - } else { - for (var _i = 0; _i < active.length; _i++) { - if (this.activeCues_.indexOf(active[_i]) === -1) { - changed = true; - } - } - } - - this.activeCues_ = active; - activeCues.setCues_(this.activeCues_); - return activeCues; - }, - // /!\ Keep this setter empty (see the timeupdate handler above) - set: function set() {} - } - }); - - if (settings.src) { - _this.src = settings.src; - - if (!_this.preload_) { - // Tracks will load on-demand. - // Act like we're loaded for other purposes. - _this.loaded_ = true; - } - - if (_this.preload_ || default_ || settings.kind !== 'subtitles' && settings.kind !== 'captions') { - loadTrack(_this.src, assertThisInitialized(_this)); - } - } else { - _this.loaded_ = true; - } - - return _this; - } - /** - * Add a cue to the internal list of cues. - * - * @param {TextTrack~Cue} cue - * The cue to add to our internal list - */ - - - var _proto = TextTrack.prototype; - - _proto.addCue = function addCue(originalCue) { - var cue = originalCue; - - if (window_1$1.vttjs && !(originalCue instanceof window_1$1.vttjs.VTTCue)) { - cue = new window_1$1.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text); - - for (var prop in originalCue) { - if (!(prop in cue)) { - cue[prop] = originalCue[prop]; - } - } // make sure that `id` is copied over - - - cue.id = originalCue.id; - cue.originalCue_ = originalCue; - } - - var tracks = this.tech_.textTracks(); - - for (var i = 0; i < tracks.length; i++) { - if (tracks[i] !== this) { - tracks[i].removeCue(cue); - } - } - - this.cues_.push(cue); - this.cues.setCues_(this.cues_); - } - /** - * Remove a cue from our internal list - * - * @param {TextTrack~Cue} removeCue - * The cue to remove from our internal list - */ - ; - - _proto.removeCue = function removeCue(_removeCue) { - var i = this.cues_.length; - - while (i--) { - var cue = this.cues_[i]; - - if (cue === _removeCue || cue.originalCue_ && cue.originalCue_ === _removeCue) { - this.cues_.splice(i, 1); - this.cues.setCues_(this.cues_); - break; - } - } - }; - - return TextTrack; -}(Track); -/** - * cuechange - One or more cues in the track have become active or stopped being active. - */ - - -TextTrack.prototype.allowedEvents_ = { - cuechange: 'cuechange' -}; - -/** - * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList} - * only one `AudioTrack` in the list will be enabled at a time. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack} - * @extends Track - */ - -var AudioTrack = /*#__PURE__*/function (_Track) { - inheritsLoose(AudioTrack, _Track); - - /** - * Create an instance of this class. - * - * @param {Object} [options={}] - * Object of option names and values - * - * @param {AudioTrack~Kind} [options.kind=''] - * A valid audio track kind - * - * @param {string} [options.id='vjs_track_' + Guid.newGUID()] - * A unique id for this AudioTrack. - * - * @param {string} [options.label=''] - * The menu label for this track. - * - * @param {string} [options.language=''] - * A valid two character language code. - * - * @param {boolean} [options.enabled] - * If this track is the one that is currently playing. If this track is part of - * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled. - */ - function AudioTrack(options) { - var _this; - - if (options === void 0) { - options = {}; - } - - var settings = mergeOptions(options, { - kind: AudioTrackKind[options.kind] || '' - }); - _this = _Track.call(this, settings) || this; - var enabled = false; - /** - * @memberof AudioTrack - * @member {boolean} enabled - * If this `AudioTrack` is enabled or not. When setting this will - * fire {@link AudioTrack#enabledchange} if the state of enabled is changed. - * @instance - * - * @fires VideoTrack#selectedchange - */ - - Object.defineProperty(assertThisInitialized(_this), 'enabled', { - get: function get() { - return enabled; - }, - set: function set(newEnabled) { - // an invalid or unchanged value - if (typeof newEnabled !== 'boolean' || newEnabled === enabled) { - return; - } - - enabled = newEnabled; - /** - * An event that fires when enabled changes on this track. This allows - * the AudioTrackList that holds this track to act accordingly. - * - * > Note: This is not part of the spec! Native tracks will do - * this internally without an event. - * - * @event AudioTrack#enabledchange - * @type {EventTarget~Event} - */ - - this.trigger('enabledchange'); - } - }); // if the user sets this track to selected then - // set selected to that true value otherwise - // we keep it false - - if (settings.enabled) { - _this.enabled = settings.enabled; - } - - _this.loaded_ = true; - return _this; - } - - return AudioTrack; -}(Track); - -/** - * A representation of a single `VideoTrack`. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack} - * @extends Track - */ - -var VideoTrack = /*#__PURE__*/function (_Track) { - inheritsLoose(VideoTrack, _Track); - - /** - * Create an instance of this class. - * - * @param {Object} [options={}] - * Object of option names and values - * - * @param {string} [options.kind=''] - * A valid {@link VideoTrack~Kind} - * - * @param {string} [options.id='vjs_track_' + Guid.newGUID()] - * A unique id for this AudioTrack. - * - * @param {string} [options.label=''] - * The menu label for this track. - * - * @param {string} [options.language=''] - * A valid two character language code. - * - * @param {boolean} [options.selected] - * If this track is the one that is currently playing. - */ - function VideoTrack(options) { - var _this; - - if (options === void 0) { - options = {}; - } - - var settings = mergeOptions(options, { - kind: VideoTrackKind[options.kind] || '' - }); - _this = _Track.call(this, settings) || this; - var selected = false; - /** - * @memberof VideoTrack - * @member {boolean} selected - * If this `VideoTrack` is selected or not. When setting this will - * fire {@link VideoTrack#selectedchange} if the state of selected changed. - * @instance - * - * @fires VideoTrack#selectedchange - */ - - Object.defineProperty(assertThisInitialized(_this), 'selected', { - get: function get() { - return selected; - }, - set: function set(newSelected) { - // an invalid or unchanged value - if (typeof newSelected !== 'boolean' || newSelected === selected) { - return; - } - - selected = newSelected; - /** - * An event that fires when selected changes on this track. This allows - * the VideoTrackList that holds this track to act accordingly. - * - * > Note: This is not part of the spec! Native tracks will do - * this internally without an event. - * - * @event VideoTrack#selectedchange - * @type {EventTarget~Event} - */ - - this.trigger('selectedchange'); - } - }); // if the user sets this track to selected then - // set selected to that true value otherwise - // we keep it false - - if (settings.selected) { - _this.selected = settings.selected; - } - - return _this; - } - - return VideoTrack; -}(Track); - -/** - * @memberof HTMLTrackElement - * @typedef {HTMLTrackElement~ReadyState} - * @enum {number} - */ - -var NONE = 0; -var LOADING = 1; -var LOADED = 2; -var ERROR = 3; -/** - * A single track represented in the DOM. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement} - * @extends EventTarget - */ - -var HTMLTrackElement = /*#__PURE__*/function (_EventTarget) { - inheritsLoose(HTMLTrackElement, _EventTarget); - - /** - * Create an instance of this class. - * - * @param {Object} options={} - * Object of option names and values - * - * @param {Tech} options.tech - * A reference to the tech that owns this HTMLTrackElement. - * - * @param {TextTrack~Kind} [options.kind='subtitles'] - * A valid text track kind. - * - * @param {TextTrack~Mode} [options.mode='disabled'] - * A valid text track mode. - * - * @param {string} [options.id='vjs_track_' + Guid.newGUID()] - * A unique id for this TextTrack. - * - * @param {string} [options.label=''] - * The menu label for this track. - * - * @param {string} [options.language=''] - * A valid two character language code. - * - * @param {string} [options.srclang=''] - * A valid two character language code. An alternative, but deprioritized - * vesion of `options.language` - * - * @param {string} [options.src] - * A url to TextTrack cues. - * - * @param {boolean} [options.default] - * If this track should default to on or off. - */ - function HTMLTrackElement(options) { - var _this; - - if (options === void 0) { - options = {}; - } - - _this = _EventTarget.call(this) || this; - var readyState; - var track = new TextTrack(options); - _this.kind = track.kind; - _this.src = track.src; - _this.srclang = track.language; - _this.label = track.label; - _this["default"] = track["default"]; - Object.defineProperties(assertThisInitialized(_this), { - /** - * @memberof HTMLTrackElement - * @member {HTMLTrackElement~ReadyState} readyState - * The current ready state of the track element. - * @instance - */ - readyState: { - get: function get() { - return readyState; - } - }, - - /** - * @memberof HTMLTrackElement - * @member {TextTrack} track - * The underlying TextTrack object. - * @instance - * - */ - track: { - get: function get() { - return track; - } - } - }); - readyState = NONE; - /** - * @listens TextTrack#loadeddata - * @fires HTMLTrackElement#load - */ - - track.addEventListener('loadeddata', function () { - readyState = LOADED; - - _this.trigger({ - type: 'load', - target: assertThisInitialized(_this) - }); - }); - return _this; - } - - return HTMLTrackElement; -}(EventTarget); - -HTMLTrackElement.prototype.allowedEvents_ = { - load: 'load' -}; -HTMLTrackElement.NONE = NONE; -HTMLTrackElement.LOADING = LOADING; -HTMLTrackElement.LOADED = LOADED; -HTMLTrackElement.ERROR = ERROR; - -/* - * This file contains all track properties that are used in - * player.js, tech.js, html5.js and possibly other techs in the future. - */ - -var NORMAL = { - audio: { - ListClass: AudioTrackList, - TrackClass: AudioTrack, - capitalName: 'Audio' - }, - video: { - ListClass: VideoTrackList, - TrackClass: VideoTrack, - capitalName: 'Video' - }, - text: { - ListClass: TextTrackList, - TrackClass: TextTrack, - capitalName: 'Text' - } -}; -Object.keys(NORMAL).forEach(function (type) { - NORMAL[type].getterName = type + "Tracks"; - NORMAL[type].privateName = type + "Tracks_"; -}); -var REMOTE = { - remoteText: { - ListClass: TextTrackList, - TrackClass: TextTrack, - capitalName: 'RemoteText', - getterName: 'remoteTextTracks', - privateName: 'remoteTextTracks_' - }, - remoteTextEl: { - ListClass: HtmlTrackElementList, - TrackClass: HTMLTrackElement, - capitalName: 'RemoteTextTrackEls', - getterName: 'remoteTextTrackEls', - privateName: 'remoteTextTrackEls_' - } -}; - -var ALL = _extends_1({}, NORMAL, REMOTE); - -REMOTE.names = Object.keys(REMOTE); -NORMAL.names = Object.keys(NORMAL); -ALL.names = [].concat(REMOTE.names).concat(NORMAL.names); - -/** - * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string - * that just contains the src url alone. - * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};` - * `var SourceString = 'http://example.com/some-video.mp4';` - * - * @typedef {Object|string} Tech~SourceObject - * - * @property {string} src - * The url to the source - * - * @property {string} type - * The mime type of the source - */ - -/** - * A function used by {@link Tech} to create a new {@link TextTrack}. - * - * @private - * - * @param {Tech} self - * An instance of the Tech class. - * - * @param {string} kind - * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) - * - * @param {string} [label] - * Label to identify the text track - * - * @param {string} [language] - * Two letter language abbreviation - * - * @param {Object} [options={}] - * An object with additional text track options - * - * @return {TextTrack} - * The text track that was created. - */ - -function createTrackHelper(self, kind, label, language, options) { - if (options === void 0) { - options = {}; - } - - var tracks = self.textTracks(); - options.kind = kind; - - if (label) { - options.label = label; - } - - if (language) { - options.language = language; - } - - options.tech = self; - var track = new ALL.text.TrackClass(options); - tracks.addTrack(track); - return track; -} -/** - * This is the base class for media playback technology controllers, such as - * {@link Flash} and {@link HTML5} - * - * @extends Component - */ - - -var Tech = /*#__PURE__*/function (_Component) { - inheritsLoose(Tech, _Component); - - /** - * Create an instance of this Tech. - * - * @param {Object} [options] - * The key/value store of player options. - * - * @param {Component~ReadyCallback} ready - * Callback function to call when the `HTML5` Tech is ready. - */ - function Tech(options, ready) { - var _this; - - if (options === void 0) { - options = {}; - } - - if (ready === void 0) { - ready = function ready() {}; - } - - // we don't want the tech to report user activity automatically. - // This is done manually in addControlsListeners - options.reportTouchActivity = false; - _this = _Component.call(this, null, options, ready) || this; // keep track of whether the current source has played at all to - // implement a very limited played() - - _this.hasStarted_ = false; - - _this.on('playing', function () { - this.hasStarted_ = true; - }); - - _this.on('loadstart', function () { - this.hasStarted_ = false; - }); - - ALL.names.forEach(function (name) { - var props = ALL[name]; - - if (options && options[props.getterName]) { - _this[props.privateName] = options[props.getterName]; - } - }); // Manually track progress in cases where the browser/flash player doesn't report it. - - if (!_this.featuresProgressEvents) { - _this.manualProgressOn(); - } // Manually track timeupdates in cases where the browser/flash player doesn't report it. - - - if (!_this.featuresTimeupdateEvents) { - _this.manualTimeUpdatesOn(); - } - - ['Text', 'Audio', 'Video'].forEach(function (track) { - if (options["native" + track + "Tracks"] === false) { - _this["featuresNative" + track + "Tracks"] = false; - } - }); - - if (options.nativeCaptions === false || options.nativeTextTracks === false) { - _this.featuresNativeTextTracks = false; - } else if (options.nativeCaptions === true || options.nativeTextTracks === true) { - _this.featuresNativeTextTracks = true; - } - - if (!_this.featuresNativeTextTracks) { - _this.emulateTextTracks(); - } - - _this.preloadTextTracks = options.preloadTextTracks !== false; - _this.autoRemoteTextTracks_ = new ALL.text.ListClass(); - - _this.initTrackListeners(); // Turn on component tap events only if not using native controls - - - if (!options.nativeControlsForTouch) { - _this.emitTapEvents(); - } - - if (_this.constructor) { - _this.name_ = _this.constructor.name || 'Unknown Tech'; - } - - return _this; - } - /** - * A special function to trigger source set in a way that will allow player - * to re-trigger if the player or tech are not ready yet. - * - * @fires Tech#sourceset - * @param {string} src The source string at the time of the source changing. - */ - - - var _proto = Tech.prototype; - - _proto.triggerSourceset = function triggerSourceset(src) { - var _this2 = this; - - if (!this.isReady_) { - // on initial ready we have to trigger source set - // 1ms after ready so that player can watch for it. - this.one('ready', function () { - return _this2.setTimeout(function () { - return _this2.triggerSourceset(src); - }, 1); - }); - } - /** - * Fired when the source is set on the tech causing the media element - * to reload. - * - * @see {@link Player#event:sourceset} - * @event Tech#sourceset - * @type {EventTarget~Event} - */ - - - this.trigger({ - src: src, - type: 'sourceset' - }); - } - /* Fallbacks for unsupported event types - ================================================================================ */ - - /** - * Polyfill the `progress` event for browsers that don't support it natively. - * - * @see {@link Tech#trackProgress} - */ - ; - - _proto.manualProgressOn = function manualProgressOn() { - this.on('durationchange', this.onDurationChange); - this.manualProgress = true; // Trigger progress watching when a source begins loading - - this.one('ready', this.trackProgress); - } - /** - * Turn off the polyfill for `progress` events that was created in - * {@link Tech#manualProgressOn} - */ - ; - - _proto.manualProgressOff = function manualProgressOff() { - this.manualProgress = false; - this.stopTrackingProgress(); - this.off('durationchange', this.onDurationChange); - } - /** - * This is used to trigger a `progress` event when the buffered percent changes. It - * sets an interval function that will be called every 500 milliseconds to check if the - * buffer end percent has changed. - * - * > This function is called by {@link Tech#manualProgressOn} - * - * @param {EventTarget~Event} event - * The `ready` event that caused this to run. - * - * @listens Tech#ready - * @fires Tech#progress - */ - ; - - _proto.trackProgress = function trackProgress(event) { - this.stopTrackingProgress(); - this.progressInterval = this.setInterval(bind(this, function () { - // Don't trigger unless buffered amount is greater than last time - var numBufferedPercent = this.bufferedPercent(); - - if (this.bufferedPercent_ !== numBufferedPercent) { - /** - * See {@link Player#progress} - * - * @event Tech#progress - * @type {EventTarget~Event} - */ - this.trigger('progress'); - } - - this.bufferedPercent_ = numBufferedPercent; - - if (numBufferedPercent === 1) { - this.stopTrackingProgress(); - } - }), 500); - } - /** - * Update our internal duration on a `durationchange` event by calling - * {@link Tech#duration}. - * - * @param {EventTarget~Event} event - * The `durationchange` event that caused this to run. - * - * @listens Tech#durationchange - */ - ; - - _proto.onDurationChange = function onDurationChange(event) { - this.duration_ = this.duration(); - } - /** - * Get and create a `TimeRange` object for buffering. - * - * @return {TimeRange} - * The time range object that was created. - */ - ; - - _proto.buffered = function buffered() { - return createTimeRanges(0, 0); - } - /** - * Get the percentage of the current video that is currently buffered. - * - * @return {number} - * A number from 0 to 1 that represents the decimal percentage of the - * video that is buffered. - * - */ - ; - - _proto.bufferedPercent = function bufferedPercent$1() { - return bufferedPercent(this.buffered(), this.duration_); - } - /** - * Turn off the polyfill for `progress` events that was created in - * {@link Tech#manualProgressOn} - * Stop manually tracking progress events by clearing the interval that was set in - * {@link Tech#trackProgress}. - */ - ; - - _proto.stopTrackingProgress = function stopTrackingProgress() { - this.clearInterval(this.progressInterval); - } - /** - * Polyfill the `timeupdate` event for browsers that don't support it. - * - * @see {@link Tech#trackCurrentTime} - */ - ; - - _proto.manualTimeUpdatesOn = function manualTimeUpdatesOn() { - this.manualTimeUpdates = true; - this.on('play', this.trackCurrentTime); - this.on('pause', this.stopTrackingCurrentTime); - } - /** - * Turn off the polyfill for `timeupdate` events that was created in - * {@link Tech#manualTimeUpdatesOn} - */ - ; - - _proto.manualTimeUpdatesOff = function manualTimeUpdatesOff() { - this.manualTimeUpdates = false; - this.stopTrackingCurrentTime(); - this.off('play', this.trackCurrentTime); - this.off('pause', this.stopTrackingCurrentTime); - } - /** - * Sets up an interval function to track current time and trigger `timeupdate` every - * 250 milliseconds. - * - * @listens Tech#play - * @triggers Tech#timeupdate - */ - ; - - _proto.trackCurrentTime = function trackCurrentTime() { - if (this.currentTimeInterval) { - this.stopTrackingCurrentTime(); - } - - this.currentTimeInterval = this.setInterval(function () { - /** - * Triggered at an interval of 250ms to indicated that time is passing in the video. - * - * @event Tech#timeupdate - * @type {EventTarget~Event} - */ - this.trigger({ - type: 'timeupdate', - target: this, - manuallyTriggered: true - }); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 - }, 250); - } - /** - * Stop the interval function created in {@link Tech#trackCurrentTime} so that the - * `timeupdate` event is no longer triggered. - * - * @listens {Tech#pause} - */ - ; - - _proto.stopTrackingCurrentTime = function stopTrackingCurrentTime() { - this.clearInterval(this.currentTimeInterval); // #1002 - if the video ends right before the next timeupdate would happen, - // the progress bar won't make it all the way to the end - - this.trigger({ - type: 'timeupdate', - target: this, - manuallyTriggered: true - }); - } - /** - * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList}, - * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech. - * - * @fires Component#dispose - */ - ; - - _proto.dispose = function dispose() { - // clear out all tracks because we can't reuse them between techs - this.clearTracks(NORMAL.names); // Turn off any manual progress or timeupdate tracking - - if (this.manualProgress) { - this.manualProgressOff(); - } - - if (this.manualTimeUpdates) { - this.manualTimeUpdatesOff(); - } - - _Component.prototype.dispose.call(this); - } - /** - * Clear out a single `TrackList` or an array of `TrackLists` given their names. - * - * > Note: Techs without source handlers should call this between sources for `video` - * & `audio` tracks. You don't want to use them between tracks! - * - * @param {string[]|string} types - * TrackList names to clear, valid names are `video`, `audio`, and - * `text`. - */ - ; - - _proto.clearTracks = function clearTracks(types) { - var _this3 = this; - - types = [].concat(types); // clear out all tracks because we can't reuse them between techs - - types.forEach(function (type) { - var list = _this3[type + "Tracks"]() || []; - var i = list.length; - - while (i--) { - var track = list[i]; - - if (type === 'text') { - _this3.removeRemoteTextTrack(track); - } - - list.removeTrack(track); - } - }); - } - /** - * Remove any TextTracks added via addRemoteTextTrack that are - * flagged for automatic garbage collection - */ - ; - - _proto.cleanupAutoTextTracks = function cleanupAutoTextTracks() { - var list = this.autoRemoteTextTracks_ || []; - var i = list.length; - - while (i--) { - var track = list[i]; - this.removeRemoteTextTrack(track); - } - } - /** - * Reset the tech, which will removes all sources and reset the internal readyState. - * - * @abstract - */ - ; - - _proto.reset = function reset() {} - /** - * Get the value of `crossOrigin` from the tech. - * - * @abstract - * - * @see {Html5#crossOrigin} - */ - ; - - _proto.crossOrigin = function crossOrigin() {} - /** - * Set the value of `crossOrigin` on the tech. - * - * @abstract - * - * @param {string} crossOrigin the crossOrigin value - * @see {Html5#setCrossOrigin} - */ - ; - - _proto.setCrossOrigin = function setCrossOrigin() {} - /** - * Get or set an error on the Tech. - * - * @param {MediaError} [err] - * Error to set on the Tech - * - * @return {MediaError|null} - * The current error object on the tech, or null if there isn't one. - */ - ; - - _proto.error = function error(err) { - if (err !== undefined) { - this.error_ = new MediaError(err); - this.trigger('error'); - } - - return this.error_; - } - /** - * Returns the `TimeRange`s that have been played through for the current source. - * - * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`. - * It only checks whether the source has played at all or not. - * - * @return {TimeRange} - * - A single time range if this video has played - * - An empty set of ranges if not. - */ - ; - - _proto.played = function played() { - if (this.hasStarted_) { - return createTimeRanges(0, 0); - } - - return createTimeRanges(); - } - /** - * Set whether we are scrubbing or not - * - * @abstract - * - * @see {Html5#setScrubbing} - */ - ; - - _proto.setScrubbing = function setScrubbing() {} - /** - * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was - * previously called. - * - * @fires Tech#timeupdate - */ - ; - - _proto.setCurrentTime = function setCurrentTime() { - // improve the accuracy of manual timeupdates - if (this.manualTimeUpdates) { - /** - * A manual `timeupdate` event. - * - * @event Tech#timeupdate - * @type {EventTarget~Event} - */ - this.trigger({ - type: 'timeupdate', - target: this, - manuallyTriggered: true - }); - } - } - /** - * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and - * {@link TextTrackList} events. - * - * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`. - * - * @fires Tech#audiotrackchange - * @fires Tech#videotrackchange - * @fires Tech#texttrackchange - */ - ; - - _proto.initTrackListeners = function initTrackListeners() { - var _this4 = this; - - /** - * Triggered when tracks are added or removed on the Tech {@link AudioTrackList} - * - * @event Tech#audiotrackchange - * @type {EventTarget~Event} - */ - - /** - * Triggered when tracks are added or removed on the Tech {@link VideoTrackList} - * - * @event Tech#videotrackchange - * @type {EventTarget~Event} - */ - - /** - * Triggered when tracks are added or removed on the Tech {@link TextTrackList} - * - * @event Tech#texttrackchange - * @type {EventTarget~Event} - */ - NORMAL.names.forEach(function (name) { - var props = NORMAL[name]; - - var trackListChanges = function trackListChanges() { - _this4.trigger(name + "trackchange"); - }; - - var tracks = _this4[props.getterName](); - - tracks.addEventListener('removetrack', trackListChanges); - tracks.addEventListener('addtrack', trackListChanges); - - _this4.on('dispose', function () { - tracks.removeEventListener('removetrack', trackListChanges); - tracks.removeEventListener('addtrack', trackListChanges); - }); - }); - } - /** - * Emulate TextTracks using vtt.js if necessary - * - * @fires Tech#vttjsloaded - * @fires Tech#vttjserror - */ - ; - - _proto.addWebVttScript_ = function addWebVttScript_() { - var _this5 = this; - - if (window_1$1.WebVTT) { - return; - } // Initially, Tech.el_ is a child of a dummy-div wait until the Component system - // signals that the Tech is ready at which point Tech.el_ is part of the DOM - // before inserting the WebVTT script - - - if (document_1.body.contains(this.el())) { - // load via require if available and vtt.js script location was not passed in - // as an option. novtt builds will turn the above require call into an empty object - // which will cause this if check to always fail. - if (!this.options_['vtt.js'] && isPlain(browserIndex) && Object.keys(browserIndex).length > 0) { - this.trigger('vttjsloaded'); - return; - } // load vtt.js via the script location option or the cdn of no location was - // passed in - - - var script = document_1.createElement('script'); - script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js'; - - script.onload = function () { - /** - * Fired when vtt.js is loaded. - * - * @event Tech#vttjsloaded - * @type {EventTarget~Event} - */ - _this5.trigger('vttjsloaded'); - }; - - script.onerror = function () { - /** - * Fired when vtt.js was not loaded due to an error - * - * @event Tech#vttjsloaded - * @type {EventTarget~Event} - */ - _this5.trigger('vttjserror'); - }; - - this.on('dispose', function () { - script.onload = null; - script.onerror = null; - }); // but have not loaded yet and we set it to true before the inject so that - // we don't overwrite the injected window.WebVTT if it loads right away - - window_1$1.WebVTT = true; - this.el().parentNode.appendChild(script); - } else { - this.ready(this.addWebVttScript_); - } - } - /** - * Emulate texttracks - * - */ - ; - - _proto.emulateTextTracks = function emulateTextTracks() { - var _this6 = this; - - var tracks = this.textTracks(); - var remoteTracks = this.remoteTextTracks(); - - var handleAddTrack = function handleAddTrack(e) { - return tracks.addTrack(e.track); - }; - - var handleRemoveTrack = function handleRemoveTrack(e) { - return tracks.removeTrack(e.track); - }; - - remoteTracks.on('addtrack', handleAddTrack); - remoteTracks.on('removetrack', handleRemoveTrack); - this.addWebVttScript_(); - - var updateDisplay = function updateDisplay() { - return _this6.trigger('texttrackchange'); - }; - - var textTracksChanges = function textTracksChanges() { - updateDisplay(); - - for (var i = 0; i < tracks.length; i++) { - var track = tracks[i]; - track.removeEventListener('cuechange', updateDisplay); - - if (track.mode === 'showing') { - track.addEventListener('cuechange', updateDisplay); - } - } - }; - - textTracksChanges(); - tracks.addEventListener('change', textTracksChanges); - tracks.addEventListener('addtrack', textTracksChanges); - tracks.addEventListener('removetrack', textTracksChanges); - this.on('dispose', function () { - remoteTracks.off('addtrack', handleAddTrack); - remoteTracks.off('removetrack', handleRemoveTrack); - tracks.removeEventListener('change', textTracksChanges); - tracks.removeEventListener('addtrack', textTracksChanges); - tracks.removeEventListener('removetrack', textTracksChanges); - - for (var i = 0; i < tracks.length; i++) { - var track = tracks[i]; - track.removeEventListener('cuechange', updateDisplay); - } - }); - } - /** - * Create and returns a remote {@link TextTrack} object. - * - * @param {string} kind - * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) - * - * @param {string} [label] - * Label to identify the text track - * - * @param {string} [language] - * Two letter language abbreviation - * - * @return {TextTrack} - * The TextTrack that gets created. - */ - ; - - _proto.addTextTrack = function addTextTrack(kind, label, language) { - if (!kind) { - throw new Error('TextTrack kind is required but was not provided'); - } - - return createTrackHelper(this, kind, label, language); - } - /** - * Create an emulated TextTrack for use by addRemoteTextTrack - * - * This is intended to be overridden by classes that inherit from - * Tech in order to create native or custom TextTracks. - * - * @param {Object} options - * The object should contain the options to initialize the TextTrack with. - * - * @param {string} [options.kind] - * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). - * - * @param {string} [options.label]. - * Label to identify the text track - * - * @param {string} [options.language] - * Two letter language abbreviation. - * - * @return {HTMLTrackElement} - * The track element that gets created. - */ - ; - - _proto.createRemoteTextTrack = function createRemoteTextTrack(options) { - var track = mergeOptions(options, { - tech: this - }); - return new REMOTE.remoteTextEl.TrackClass(track); - } - /** - * Creates a remote text track object and returns an html track element. - * - * > Note: This can be an emulated {@link HTMLTrackElement} or a native one. - * - * @param {Object} options - * See {@link Tech#createRemoteTextTrack} for more detailed properties. - * - * @param {boolean} [manualCleanup=true] - * - When false: the TextTrack will be automatically removed from the video - * element whenever the source changes - * - When True: The TextTrack will have to be cleaned up manually - * - * @return {HTMLTrackElement} - * An Html Track Element. - * - * @deprecated The default functionality for this function will be equivalent - * to "manualCleanup=false" in the future. The manualCleanup parameter will - * also be removed. - */ - ; - - _proto.addRemoteTextTrack = function addRemoteTextTrack(options, manualCleanup) { - var _this7 = this; - - if (options === void 0) { - options = {}; - } - - var htmlTrackElement = this.createRemoteTextTrack(options); - - if (manualCleanup !== true && manualCleanup !== false) { - // deprecation warning - log.warn('Calling addRemoteTextTrack without explicitly setting the "manualCleanup" parameter to `true` is deprecated and default to `false` in future version of video.js'); - manualCleanup = true; - } // store HTMLTrackElement and TextTrack to remote list - - - this.remoteTextTrackEls().addTrackElement_(htmlTrackElement); - this.remoteTextTracks().addTrack(htmlTrackElement.track); - - if (manualCleanup !== true) { - // create the TextTrackList if it doesn't exist - this.ready(function () { - return _this7.autoRemoteTextTracks_.addTrack(htmlTrackElement.track); - }); - } - - return htmlTrackElement; - } - /** - * Remove a remote text track from the remote `TextTrackList`. - * - * @param {TextTrack} track - * `TextTrack` to remove from the `TextTrackList` - */ - ; - - _proto.removeRemoteTextTrack = function removeRemoteTextTrack(track) { - var trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track); // remove HTMLTrackElement and TextTrack from remote list - - this.remoteTextTrackEls().removeTrackElement_(trackElement); - this.remoteTextTracks().removeTrack(track); - this.autoRemoteTextTracks_.removeTrack(track); - } - /** - * Gets available media playback quality metrics as specified by the W3C's Media - * Playback Quality API. - * - * @see [Spec]{@link https://wicg.github.io/media-playback-quality} - * - * @return {Object} - * An object with supported media playback quality metrics - * - * @abstract - */ - ; - - _proto.getVideoPlaybackQuality = function getVideoPlaybackQuality() { - return {}; - } - /** - * Attempt to create a floating video window always on top of other windows - * so that users may continue consuming media while they interact with other - * content sites, or applications on their device. - * - * @see [Spec]{@link https://wicg.github.io/picture-in-picture} - * - * @return {Promise|undefined} - * A promise with a Picture-in-Picture window if the browser supports - * Promises (or one was passed in as an option). It returns undefined - * otherwise. - * - * @abstract - */ - ; - - _proto.requestPictureInPicture = function requestPictureInPicture() { - var PromiseClass = this.options_.Promise || window_1$1.Promise; - - if (PromiseClass) { - return PromiseClass.reject(); - } - } - /** - * A method to check for the value of the 'disablePictureInPicture'