From f8068826aba1502b95ef8d11231c56fee6cda3bf Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Wed, 30 Sep 2020 17:26:07 -0700 Subject: [PATCH 01/30] Add script to build and bundle admin --- scripts/bundleAdmin.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100755 scripts/bundleAdmin.sh diff --git a/scripts/bundleAdmin.sh b/scripts/bundleAdmin.sh new file mode 100755 index 000000000..b5c06dc87 --- /dev/null +++ b/scripts/bundleAdmin.sh @@ -0,0 +1,13 @@ +PROJECT_SOURCE_DIR=$(pwd) +mkdir $TMPDIR/admin 2> /dev/null +cd $TMPDIR/admin +git clone --depth 1 https://github.com/owncast/owncast-admin 2> /dev/null +cd owncast-admin +npm --silent install 2> /dev/null +(node_modules/.bin/next build && node_modules/.bin/next export) | grep info +ADMIN_BUILD_DIR=$(pwd) +cd $PROJECT_SOURCE_DIR +mkdir webroot/admin 2> /dev/null +cd webroot/admin +cp -R ${ADMIN_BUILD_DIR}/out/* . +rm -rf $TMPDIR/admin From 334a69386d54da3505b5e725c44559c5f4303f53 Mon Sep 17 00:00:00 2001 From: Ahmad Karlam Date: Thu, 1 Oct 2020 16:11:43 +0700 Subject: [PATCH 02/30] Add timestamp to title chat --- webroot/js/components/chat/message.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webroot/js/components/chat/message.js b/webroot/js/components/chat/message.js index e208a72dd..3fa246bcf 100644 --- a/webroot/js/components/chat/message.js +++ b/webroot/js/components/chat/message.js @@ -13,7 +13,7 @@ export default class Message extends Component { const { type } = message; if (type === SOCKET_MESSAGE_TYPES.CHAT) { - const { image, author, body } = message; + const { image, author, body, timestamp } = message; const formattedMessage = formatMessageText(body, username); const avatar = image || generateAvatar(author); @@ -35,6 +35,7 @@ export default class Message extends Component {
Date: Thu, 1 Oct 2020 18:16:58 -0700 Subject: [PATCH 03/30] Disconnect stream Admin API + HTTP Basic Auth (#204) * Create http auth middleware * Add support for ending the inbound stream. Closes #191 * Add a simple success response to API requests --- controllers/admin.go | 19 +++++++++++++++++++ controllers/controllers.go | 11 +++++++++++ core/rtmp/rtmp.go | 13 +++++++++++++ models/baseAPIResponse.go | 7 +++++++ router/middleware/auth.go | 34 ++++++++++++++++++++++++++++++++++ router/router.go | 6 ++++++ 6 files changed, 90 insertions(+) create mode 100644 controllers/admin.go create mode 100644 models/baseAPIResponse.go create mode 100644 router/middleware/auth.go diff --git a/controllers/admin.go b/controllers/admin.go new file mode 100644 index 000000000..106768301 --- /dev/null +++ b/controllers/admin.go @@ -0,0 +1,19 @@ +package controllers + +import ( + "net/http" + + "github.com/gabek/owncast/core" + "github.com/gabek/owncast/core/rtmp" +) + +// DisconnectInboundConnection will force-disconnect an inbound stream +func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) { + if !core.GetStatus().Online { + writeSimpleResponse(w, false, "no inbound stream connected") + return + } + + rtmp.Disconnect() + writeSimpleResponse(w, true, "inbound stream disconnected") +} diff --git a/controllers/controllers.go b/controllers/controllers.go index 62dc91191..7a54a3f09 100644 --- a/controllers/controllers.go +++ b/controllers/controllers.go @@ -3,6 +3,8 @@ package controllers import ( "encoding/json" "net/http" + + "github.com/gabek/owncast/models" ) type j map[string]interface{} @@ -24,3 +26,12 @@ func badRequestHandler(w http.ResponseWriter, err error) { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(j{"error": err.Error()}) } + +func writeSimpleResponse(w http.ResponseWriter, success bool, message string) { + response := models.BaseAPIResponse{ + Success: success, + Message: message, + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} diff --git a/core/rtmp/rtmp.go b/core/rtmp/rtmp.go index 64840a4f8..ddab33e5a 100644 --- a/core/rtmp/rtmp.go +++ b/core/rtmp/rtmp.go @@ -28,6 +28,7 @@ var ( var _transcoder ffmpeg.Transcoder var _pipe *os.File +var _rtmpConnection net.Conn //Start starts the rtmp service, listening on port 1935 func Start() { @@ -92,6 +93,7 @@ func HandleConn(c *rtmp.Conn, nc net.Conn) { _isConnected = true core.SetStreamAsConnected() + _rtmpConnection = nc f, err := os.OpenFile(pipePath, os.O_RDWR, os.ModeNamedPipe) _pipe = f @@ -121,9 +123,20 @@ func handleDisconnect(conn net.Conn) { _pipe.Close() _isConnected = false _transcoder.Stop() + _rtmpConnection = nil core.SetStreamAsDisconnected() } +// Disconnect will force disconnect the current inbound RTMP connection. +func Disconnect() { + if _rtmpConnection == nil { + return + } + + log.Infoln("Inbound stream disconnect requested.") + handleDisconnect(_rtmpConnection) +} + //IsConnected gets whether there is an rtmp connection or not //this is only a getter since it is controlled by the rtmp handler func IsConnected() bool { diff --git a/models/baseAPIResponse.go b/models/baseAPIResponse.go new file mode 100644 index 000000000..8c643f272 --- /dev/null +++ b/models/baseAPIResponse.go @@ -0,0 +1,7 @@ +package models + +// BaseAPIResponse is a simple response to API requests. +type BaseAPIResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} diff --git a/router/middleware/auth.go b/router/middleware/auth.go new file mode 100644 index 000000000..e168b0ac9 --- /dev/null +++ b/router/middleware/auth.go @@ -0,0 +1,34 @@ +package middleware + +import ( + "crypto/subtle" + "net/http" + + "github.com/gabek/owncast/config" + log "github.com/sirupsen/logrus" +) + +// RequireAdminAuth wraps a handler requiring HTTP basic auth for it using the given +// the stream key as the password and and a hardcoded "admin" for username. +func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc { + username := "admin" + password := config.Config.VideoSettings.StreamingKey + + return func(w http.ResponseWriter, r *http.Request) { + + user, pass, ok := r.BasicAuth() + realm := "Owncast Authenticated Request" + + // Failed + if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 { + w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + log.Warnln("Failed authentication for", r.URL.Path, "from", r.RemoteAddr, r.UserAgent()) + return + } + + // Success + log.Traceln("Authenticated request OK for", r.URL.Path, "from", r.RemoteAddr, r.UserAgent()) + handler(w, r) + } +} diff --git a/router/router.go b/router/router.go index 1c0f74ac9..70ac3afec 100644 --- a/router/router.go +++ b/router/router.go @@ -10,6 +10,7 @@ import ( "github.com/gabek/owncast/controllers" "github.com/gabek/owncast/core/chat" "github.com/gabek/owncast/core/rtmp" + "github.com/gabek/owncast/router/middleware" ) //Start starts the router for the http, ws, and rtmp @@ -43,6 +44,11 @@ func Start() error { http.HandleFunc("/embed/video", controllers.GetVideoEmbed) } + // Authenticated admin requests + + // Disconnect inbound stream + http.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(controllers.DisconnectInboundConnection)) + port := config.Config.GetPublicWebServerPort() log.Infof("Web server running on port: %d", port) From 48c8cf5ed2b659dc98245deb45a019c9057e8390 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Thu, 1 Oct 2020 23:17:27 -0700 Subject: [PATCH 04/30] Admin API to change in-memory streaming key. Closes #212 --- controllers/admin/changeStreamKey.go | 35 +++++++++++++++++++ controllers/{admin.go => admin/disconnect.go} | 8 +++-- controllers/controllers.go | 2 +- router/router.go | 7 +++- 4 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 controllers/admin/changeStreamKey.go rename controllers/{admin.go => admin/disconnect.go} (60%) diff --git a/controllers/admin/changeStreamKey.go b/controllers/admin/changeStreamKey.go new file mode 100644 index 000000000..394998858 --- /dev/null +++ b/controllers/admin/changeStreamKey.go @@ -0,0 +1,35 @@ +package admin + +import ( + "encoding/json" + "net/http" + + "github.com/gabek/owncast/config" + "github.com/gabek/owncast/controllers" + + log "github.com/sirupsen/logrus" +) + +// ChangeStreamKey will change the stream key (in memory) +func ChangeStreamKey(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + controllers.WriteSimpleResponse(w, false, r.Method+" not supported") + return + } + + decoder := json.NewDecoder(r.Body) + var request changeStreamKeyRequest + err := decoder.Decode(&request) + if err != nil { + log.Errorln(err) + controllers.WriteSimpleResponse(w, false, "") + return + } + + config.Config.VideoSettings.StreamingKey = request.Key + controllers.WriteSimpleResponse(w, true, "changed") +} + +type changeStreamKeyRequest struct { + Key string `json:"key"` +} diff --git a/controllers/admin.go b/controllers/admin/disconnect.go similarity index 60% rename from controllers/admin.go rename to controllers/admin/disconnect.go index 106768301..08fbc4f30 100644 --- a/controllers/admin.go +++ b/controllers/admin/disconnect.go @@ -1,19 +1,21 @@ -package controllers +package admin import ( "net/http" + "github.com/gabek/owncast/controllers" "github.com/gabek/owncast/core" + "github.com/gabek/owncast/core/rtmp" ) // DisconnectInboundConnection will force-disconnect an inbound stream func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) { if !core.GetStatus().Online { - writeSimpleResponse(w, false, "no inbound stream connected") + controllers.WriteSimpleResponse(w, false, "no inbound stream connected") return } rtmp.Disconnect() - writeSimpleResponse(w, true, "inbound stream disconnected") + controllers.WriteSimpleResponse(w, true, "inbound stream disconnected") } diff --git a/controllers/controllers.go b/controllers/controllers.go index 7a54a3f09..bd9c75634 100644 --- a/controllers/controllers.go +++ b/controllers/controllers.go @@ -27,7 +27,7 @@ func badRequestHandler(w http.ResponseWriter, err error) { json.NewEncoder(w).Encode(j{"error": err.Error()}) } -func writeSimpleResponse(w http.ResponseWriter, success bool, message string) { +func WriteSimpleResponse(w http.ResponseWriter, success bool, message string) { response := models.BaseAPIResponse{ Success: success, Message: message, diff --git a/router/router.go b/router/router.go index 70ac3afec..5643d5d4d 100644 --- a/router/router.go +++ b/router/router.go @@ -8,6 +8,8 @@ import ( "github.com/gabek/owncast/config" "github.com/gabek/owncast/controllers" + "github.com/gabek/owncast/controllers/admin" + "github.com/gabek/owncast/core/chat" "github.com/gabek/owncast/core/rtmp" "github.com/gabek/owncast/router/middleware" @@ -47,7 +49,10 @@ func Start() error { // Authenticated admin requests // Disconnect inbound stream - http.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(controllers.DisconnectInboundConnection)) + http.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(admin.DisconnectInboundConnection)) + + // Change the current streaming key in memory + http.HandleFunc("/api/admin/changekey", middleware.RequireAdminAuth(admin.ChangeStreamKey)) port := config.Config.GetPublicWebServerPort() From 6946d4b3ea484bd7fe4089f580b2ed2e78194b82 Mon Sep 17 00:00:00 2001 From: Anoop <46913894+anoopmsivadas@users.noreply.github.com> Date: Fri, 2 Oct 2020 12:04:29 +0530 Subject: [PATCH 05/30] Make video segment filename unique (#214) * Make video segment filename unique * fix typo * Remove type casting function --- core/ffmpeg/transcoder.go | 13 ++++++++++++- core/ffmpeg/transcoder_test.go | 3 ++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/core/ffmpeg/transcoder.go b/core/ffmpeg/transcoder.go index 9a03649d0..d320cdb96 100644 --- a/core/ffmpeg/transcoder.go +++ b/core/ffmpeg/transcoder.go @@ -8,6 +8,7 @@ import ( "strings" log "github.com/sirupsen/logrus" + "github.com/teris-io/shortid" "github.com/gabek/owncast/config" "github.com/gabek/owncast/utils" @@ -25,6 +26,7 @@ type Transcoder struct { segmentLengthSeconds int appendToStream bool ffmpegPath string + segmentIdentifier string } // HLSVariant is a combination of settings that results in a single HLS stream @@ -103,6 +105,10 @@ func (t *Transcoder) getString() string { hlsOptionFlags = append(hlsOptionFlags, "append_list") } + if t.segmentIdentifier == "" { + t.segmentIdentifier = shortid.MustGenerate() + } + ffmpegFlags := []string{ "cat", t.input, "|", t.ffmpegPath, @@ -125,7 +131,7 @@ func (t *Transcoder) getString() string { // 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.ts"), // Each segment's filename + "-hls_segment_filename", path.Join(t.segmentOutputPath, "/%v/stream-%s-"+t.segmentIdentifier+".ts"), // Each segment's filename "-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", @@ -353,3 +359,8 @@ func (t *Transcoder) SetSegmentLength(seconds int) { func (t *Transcoder) SetAppendToStream(append bool) { t.appendToStream = append } + +// SetIdentifer enables appending a unique identifier to segment file name +func (t *Transcoder) SetIdentifier(output string) { + t.segmentIdentifier = output +} diff --git a/core/ffmpeg/transcoder_test.go b/core/ffmpeg/transcoder_test.go index 58e0f7cbb..322406683 100644 --- a/core/ffmpeg/transcoder_test.go +++ b/core/ffmpeg/transcoder_test.go @@ -11,6 +11,7 @@ func TestFFmpegCommand(t *testing.T) { transcoder.SetInput("fakecontent.flv") transcoder.SetOutputPath("fakeOutput") transcoder.SetHLSPlaylistLength(10) + transcoder.SetIdentifier("jdofFGg") variant := HLSVariant{} variant.videoBitrate = 1200 @@ -22,7 +23,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.ts -max_muxing_queue_size 400 fakeOutput/%v/stream.m3u8 2> transcoder.log` + 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` if cmd != expected { t.Errorf("ffmpeg command does not match expected. Got %s, want: %s", cmd, expected) From 9b7784634b448f21c2f452da804a56e45a554401 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Thu, 1 Oct 2020 23:55:38 -0700 Subject: [PATCH 06/30] First pass at YP registration/configuration (#209) * Spike: Ping YP service with instance details * WIP: Add to the config to support YP * Add YP response endpoint * Handle YP errors. Use config. Off by default * Show message about YP support on launch * Add animated gif preview when generating thumb * Increase quality of preview gif and only create it if YP is enabled * Do not allow re-registration by clearing the key * Make large and small logos actually structured * Change log level * Fix default YP service URL * Point to default hostname * Set default value for YP to false --- .gitignore | 2 + config-example.yaml | 6 ++ config/config.go | 44 ++++++++-- config/defaults.go | 2 + controllers/index.go | 2 +- core/chat/server.go | 2 +- core/core.go | 8 ++ core/ffmpeg/thumbnailGenerator.go | 27 +++++- core/status.go | 8 ++ go.mod | 1 + go.sum | 2 + router/router.go | 3 + yp/README.md | 0 yp/api.go | 46 ++++++++++ yp/yp.go | 139 ++++++++++++++++++++++++++++++ 15 files changed, 278 insertions(+), 14 deletions(-) create mode 100644 yp/README.md create mode 100644 yp/api.go create mode 100644 yp/yp.go diff --git a/.gitignore b/.gitignore index f3e4888a0..f717b674f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,11 @@ vendor/ /stats.json owncast webroot/thumbnail.jpg +webroot/preview.gif webroot/hls webroot/static/content.md hls/ dist/ transcoder.log chat.db +.yp.key diff --git a/config-example.yaml b/config-example.yaml index 61e8692e2..aaa975b0d 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -25,3 +25,9 @@ instanceDetails: videoSettings: # Change this value and keep it secure. Treat it like a password to your live stream. streamingKey: abc123 + +yp: + # Enable YP to be listed in the Owncast directory and let people discover your instance. + enabled: false + # You must specify the public URL of your host that you want the directory to link to. + instanceURL: http://yourhost.com diff --git a/config/config.go b/config/config.go index 2ed4abe66..46d564572 100644 --- a/config/config.go +++ b/config/config.go @@ -26,18 +26,25 @@ type config struct { VersionInfo string `yaml:"-"` VideoSettings videoSettings `yaml:"videoSettings"` WebServerPort int `yaml:"webServerPort"` + YP yp `yaml:"yp"` } // InstanceDetails defines the user-visible information about this particular instance. type InstanceDetails struct { - Name string `yaml:"name" json:"name"` - Title string `yaml:"title" json:"title"` - Summary string `yaml:"summary" json:"summary"` - Logo map[string]string `yaml:"logo" json:"logo"` - Tags []string `yaml:"tags" json:"tags"` - SocialHandles []socialHandle `yaml:"socialHandles" json:"socialHandles"` - ExtraInfoFile string `yaml:"extraUserInfoFileName" json:"extraUserInfoFileName"` - Version string `json:"version"` + Name string `yaml:"name" json:"name"` + Title string `yaml:"title" json:"title"` + Summary string `yaml:"summary" json:"summary"` + Logo logo `yaml:"logo" json:"logo"` + Tags []string `yaml:"tags" json:"tags"` + SocialHandles []socialHandle `yaml:"socialHandles" json:"socialHandles"` + ExtraInfoFile string `yaml:"extraUserInfoFileName" json:"extraUserInfoFileName"` + Version string `json:"version"` + NSFW bool `yaml:"nsfw" json:"nsfw"` +} + +type logo struct { + Large string `yaml:"large" json:"large"` + Small string `yaml:"small" json:"small"` } type socialHandle struct { @@ -50,7 +57,14 @@ type videoSettings struct { StreamingKey string `yaml:"streamingKey"` StreamQualities []StreamQuality `yaml:"streamQualities"` OfflineContent string `yaml:"offlineContent"` - HighestQualityStreamIndex int `yaml"-"` + HighestQualityStreamIndex int `yaml:"-"` +} + +// Registration to the central Owncast YP (Yellow pages) service operating as a directory. +type yp struct { + Enabled bool `yaml:"enabled"` + InstanceURL string `yaml:"instanceURL"` // The public URL the directory should link to + YPServiceURL string `yaml:"ypServiceURL"` // The base URL to the YP API to register with (optional) } // StreamQuality defines the specifics of a single HLS stream variant. @@ -129,6 +143,10 @@ func (c *config) verifySettings() error { } } + if c.YP.Enabled && c.YP.InstanceURL == "" { + return errors.New("YP is enabled but instance url is not set") + } + return nil } @@ -189,6 +207,14 @@ func (c *config) GetFFMpegPath() string { return _default.FFMpegPath } +func (c *config) GetYPServiceHost() string { + if c.YP.YPServiceURL != "" { + return c.YP.YPServiceURL + } + + return _default.YP.YPServiceURL +} + func (c *config) GetVideoStreamQualities() []StreamQuality { if len(c.VideoSettings.StreamQualities) > 0 { return c.VideoSettings.StreamQualities diff --git a/config/defaults.go b/config/defaults.go index 0c89e5353..66a654e41 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -16,6 +16,8 @@ func getDefaults() config { defaults.PrivateHLSPath = "hls" defaults.VideoSettings.OfflineContent = "static/offline.m4v" defaults.InstanceDetails.ExtraInfoFile = "/static/content.md" + defaults.YP.Enabled = false + defaults.YP.YPServiceURL = "https://yp.owncast.online" defaultQuality := StreamQuality{ IsAudioPassthrough: true, diff --git a/controllers/index.go b/controllers/index.go index 9fb591568..32f8edc41 100644 --- a/controllers/index.go +++ b/controllers/index.go @@ -69,7 +69,7 @@ func handleScraperMetadataPage(w http.ResponseWriter, r *http.Request) { tmpl := template.Must(template.ParseFiles(path.Join("static", "metadata.html"))) fullURL, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, r.URL.Path)) - imageURL, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, config.Config.InstanceDetails.Logo["large"])) + imageURL, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, config.Config.InstanceDetails.Logo.Large)) status := core.GetStatus() diff --git a/core/chat/server.go b/core/chat/server.go index 8779f79df..1ff81c39a 100644 --- a/core/chat/server.go +++ b/core/chat/server.go @@ -133,7 +133,7 @@ func (s *server) sendWelcomeMessageToClient(c *Client) { time.Sleep(7 * time.Second) initialChatMessageText := fmt.Sprintf("Welcome to %s! %s", config.Config.InstanceDetails.Title, config.Config.InstanceDetails.Summary) - initialMessage := models.ChatMessage{"owncast-server", config.Config.InstanceDetails.Name, initialChatMessageText, config.Config.InstanceDetails.Logo["small"], "initial-message-1", "CHAT", true, time.Now()} + initialMessage := models.ChatMessage{"owncast-server", config.Config.InstanceDetails.Name, initialChatMessageText, config.Config.InstanceDetails.Logo.Small, "initial-message-1", "CHAT", true, time.Now()} c.Write(initialMessage) }() diff --git a/core/core.go b/core/core.go index be3fd4e04..72d220047 100644 --- a/core/core.go +++ b/core/core.go @@ -13,12 +13,14 @@ import ( "github.com/gabek/owncast/core/ffmpeg" "github.com/gabek/owncast/models" "github.com/gabek/owncast/utils" + "github.com/gabek/owncast/yp" ) var ( _stats *models.Stats _storage models.ChunkStorageProvider _cleanupTimer *time.Timer + _yp *yp.YP ) //Start starts up the core processing @@ -40,6 +42,12 @@ func Start() error { return err } + if config.Config.YP.Enabled { + _yp = yp.NewYP(GetStatus) + } else { + yp.DisplayInstructions() + } + chat.Setup(ChatListenerImpl{}) return nil diff --git a/core/ffmpeg/thumbnailGenerator.go b/core/ffmpeg/thumbnailGenerator.go index 388458a8b..892748d91 100644 --- a/core/ffmpeg/thumbnailGenerator.go +++ b/core/ffmpeg/thumbnailGenerator.go @@ -40,6 +40,7 @@ func StartThumbnailGenerator(chunkPath string, variantIndex int) { func fireThumbnailGenerator(chunkPath string, variantIndex int) error { // JPG takes less time to encode than PNG outputFile := path.Join("webroot", "thumbnail.jpg") + previewGifFile := path.Join("webroot", "preview.gif") framePath := path.Join(chunkPath, strconv.Itoa(variantIndex)) files, err := ioutil.ReadDir(framePath) @@ -83,12 +84,32 @@ func fireThumbnailGenerator(chunkPath string, variantIndex int) error { } ffmpegCmd := strings.Join(thumbnailCmdFlags, " ") - - // fmt.Println(ffmpegCmd) - if _, err := exec.Command("sh", "-c", ffmpegCmd).Output(); err != nil { return err } + // If YP support is enabled also create an animated GIF preview + if config.Config.YP.Enabled { + makeAnimatedGifPreview(mostRecentFile, previewGifFile) + } + return nil } + +func makeAnimatedGifPreview(sourceFile string, outputFile string) { + // Filter is pulled from https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/ + animatedGifFlags := []string{ + config.Config.GetFFMpegPath(), + "-y", // Overwrite file + "-threads 1", // Low priority processing + "-i", sourceFile, // Input + "-t 1", // Output is one second in length + "-filter_complex", "\"[0:v] fps=8,scale=w=480:h=-1:flags=lanczos,split [a][b];[a] palettegen=stats_mode=full [p];[b][p] paletteuse=new=1\"", + outputFile, + } + + ffmpegCmd := strings.Join(animatedGifFlags, " ") + if _, err := exec.Command("sh", "-c", ffmpegCmd).Output(); err != nil { + log.Errorln(err) + } +} diff --git a/core/status.go b/core/status.go index cd720667c..4eef1831b 100644 --- a/core/status.go +++ b/core/status.go @@ -38,6 +38,10 @@ func SetStreamAsConnected() { chunkPath = config.Config.GetPrivateHLSSavePath() } + if _yp != nil { + _yp.Start() + } + ffmpeg.StartThumbnailGenerator(chunkPath, config.Config.VideoSettings.HighestQualityStreamIndex) } @@ -46,6 +50,10 @@ func SetStreamAsDisconnected() { _stats.StreamConnected = false _stats.LastDisconnectTime = utils.NullTime{time.Now(), true} + if _yp != nil { + _yp.Stop() + } + ffmpeg.ShowStreamOfflineState() startCleanupTimer() } diff --git a/go.mod b/go.mod index 883c3e402..b10a47304 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/mssola/user_agent v0.5.2 github.com/nareix/joy5 v0.0.0-20200712071056-a55089207c88 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1 github.com/radovskyb/watcher v1.0.7 github.com/shirou/gopsutil v2.20.7+incompatible github.com/sirupsen/logrus v1.6.0 diff --git a/go.sum b/go.sum index 679481303..28ccd019c 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1 h1:CskT+S6Ay54OwxBGB0R3Rsx4Muto6UnEYTyKJbyRIAI= +github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= github.com/shirou/gopsutil v2.20.7+incompatible h1:Ymv4OD12d6zm+2yONe39VSmp2XooJe8za7ngOLW/o/w= diff --git a/router/router.go b/router/router.go index 5643d5d4d..8504fba1f 100644 --- a/router/router.go +++ b/router/router.go @@ -13,6 +13,7 @@ import ( "github.com/gabek/owncast/core/chat" "github.com/gabek/owncast/core/rtmp" "github.com/gabek/owncast/router/middleware" + "github.com/gabek/owncast/yp" ) //Start starts the router for the http, ws, and rtmp @@ -44,6 +45,8 @@ func Start() error { // video embed http.HandleFunc("/embed/video", controllers.GetVideoEmbed) + + http.HandleFunc("/api/yp", yp.GetYPResponse) } // Authenticated admin requests diff --git a/yp/README.md b/yp/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/yp/api.go b/yp/api.go new file mode 100644 index 000000000..57af412a8 --- /dev/null +++ b/yp/api.go @@ -0,0 +1,46 @@ +package yp + +import ( + "encoding/json" + "net/http" + + "github.com/gabek/owncast/config" + "github.com/gabek/owncast/utils" +) + +type ypDetailsResponse struct { + Name string `json:"name"` + Description string `json:"description"` + Logo string `json:"logo"` + NSFW bool `json:"nsfw"` + Tags []string `json:"tags"` + Online bool `json:"online"` + ViewerCount int `json:"viewerCount"` + OverallMaxViewerCount int `json:"overallMaxViewerCount"` + SessionMaxViewerCount int `json:"sessionMaxViewerCount"` + + LastConnectTime utils.NullTime `json:"lastConnectTime"` +} + +//GetYPResponse gets the status of the server for YP purposes +func GetYPResponse(w http.ResponseWriter, r *http.Request) { + status := getStatus() + + response := ypDetailsResponse{ + Name: config.Config.InstanceDetails.Name, + Description: config.Config.InstanceDetails.Summary, + Logo: config.Config.InstanceDetails.Logo.Large, + NSFW: config.Config.InstanceDetails.NSFW, + Tags: config.Config.InstanceDetails.Tags, + Online: status.Online, + ViewerCount: status.ViewerCount, + OverallMaxViewerCount: status.OverallMaxViewerCount, + SessionMaxViewerCount: status.SessionMaxViewerCount, + LastConnectTime: status.LastConnectTime, + } + + w.Header().Set("Content-Type", "application/json") + + json.NewEncoder(w).Encode(response) + +} diff --git a/yp/yp.go b/yp/yp.go new file mode 100644 index 000000000..9dd5b1f02 --- /dev/null +++ b/yp/yp.go @@ -0,0 +1,139 @@ +package yp + +import ( + "bytes" + "io/ioutil" + "net/http" + "os" + "time" + + "encoding/json" + + "github.com/gabek/owncast/config" + "github.com/gabek/owncast/models" + + log "github.com/sirupsen/logrus" +) + +const pingInterval = 4 * time.Minute + +var getStatus func() models.Status + +//YP is a service for handling listing in the Owncast directory. +type YP struct { + timer *time.Ticker +} + +type ypPingResponse struct { + Key string `json:"key"` + Success bool `json:"success"` + Error string `json:"error"` + ErrorCode int `json:"errorCode"` +} + +type ypPingRequest struct { + Key string `json:"key"` + URL string `json:"url"` +} + +// NewYP creates a new instance of the YP service handler +func NewYP(getStatusFunc func() models.Status) *YP { + getStatus = getStatusFunc + return &YP{} +} + +// Start is run when a live stream begins to start pinging YP +func (yp *YP) Start() { + yp.timer = time.NewTicker(pingInterval) + + go func() { + for { + select { + case <-yp.timer.C: + yp.ping() + } + } + }() + + yp.ping() +} + +// Stop stops the pinging of YP +func (yp *YP) Stop() { + yp.timer.Stop() +} + +func (yp *YP) ping() { + myInstanceURL := config.Config.YP.InstanceURL + key := yp.getSavedKey() + + log.Traceln("Pinging YP as: ", config.Config.InstanceDetails.Name) + + request := ypPingRequest{ + Key: key, + URL: myInstanceURL, + } + + req, err := json.Marshal(request) + if err != nil { + log.Errorln(err) + return + } + + pingURL := config.Config.GetYPServiceHost() + "/ping" + resp, err := http.Post(pingURL, "application/json", bytes.NewBuffer(req)) + if err != nil { + log.Errorln(err) + return + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Errorln(err) + } + + pingResponse := ypPingResponse{} + json.Unmarshal(body, &pingResponse) + + if !pingResponse.Success { + log.Debugln("YP Ping error returned from service:", pingResponse.Error) + return + } + + if pingResponse.Key != key { + yp.writeSavedKey(pingResponse.Key) + } +} + +func (yp *YP) writeSavedKey(key string) { + f, err := os.Create(".yp.key") + defer f.Close() + + if err != nil { + log.Errorln(err) + return + } + + _, err = f.WriteString(key) + if err != nil { + log.Errorln(err) + return + } +} + +func (yp *YP) getSavedKey() string { + fileBytes, err := ioutil.ReadFile(".yp.key") + if err != nil { + return "" + } + + return string(fileBytes) +} + +// DisplayInstructions will let the user know they are not in the directory by default and +// how they can enable the feature. +func DisplayInstructions() { + text := "Your instance can be listed on the Owncast directory at http://something.something by enabling YP in your config. Learn more at http://something.something." + log.Debugln(text) +} From d8c43d2c56a1f1f3b29a6ca2998db43d7715a7de Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Fri, 2 Oct 2020 00:02:42 -0700 Subject: [PATCH 07/30] Add server config admin endpoint (#207) * Add support for ending the inbound stream. Closes #191 * Add a simple success response to API requests * Add server config admin endpoint --- config/config.go | 38 +++++++++++++++++------------------ config/configUtils.go | 17 +++++++++++++++- controllers/serverConfig.go | 40 +++++++++++++++++++++++++++++++++++++ router/router.go | 3 +++ 4 files changed, 78 insertions(+), 20 deletions(-) create mode 100644 controllers/serverConfig.go diff --git a/config/config.go b/config/config.go index 46d564572..1f80003b2 100644 --- a/config/config.go +++ b/config/config.go @@ -22,7 +22,7 @@ type config struct { InstanceDetails InstanceDetails `yaml:"instanceDetails"` PrivateHLSPath string `yaml:"privateHLSPath"` PublicHLSPath string `yaml:"publicHLSPath"` - S3 s3 `yaml:"s3"` + S3 S3 `yaml:"s3"` VersionInfo string `yaml:"-"` VideoSettings videoSettings `yaml:"videoSettings"` WebServerPort int `yaml:"webServerPort"` @@ -72,35 +72,35 @@ type StreamQuality struct { // Enable passthrough to copy the video and/or audio directly from the // incoming stream and disable any transcoding. It will ignore any of // the below settings. - IsVideoPassthrough bool `yaml:"videoPassthrough"` - IsAudioPassthrough bool `yaml:"audioPassthrough"` + IsVideoPassthrough bool `yaml:"videoPassthrough" json:"videoPassthrough"` + IsAudioPassthrough bool `yaml:"audioPassthrough" json:"audioPassthrough"` - VideoBitrate int `yaml:"videoBitrate"` - AudioBitrate int `yaml:"audioBitrate"` + VideoBitrate int `yaml:"videoBitrate" json:"videoBitrate"` + AudioBitrate int `yaml:"audioBitrate" json:"audioBitrate"` // Set only one of these in order to keep your current aspect ratio. // Or set neither to not scale the video. - ScaledWidth int `yaml:"scaledWidth"` - ScaledHeight int `yaml:"scaledHeight"` + ScaledWidth int `yaml:"scaledWidth" json:"scaledWidth,omitempty"` + ScaledHeight int `yaml:"scaledHeight" json:"scaledHeight,omitempty"` - Framerate int `yaml:"framerate"` - EncoderPreset string `yaml:"encoderPreset"` + Framerate int `yaml:"framerate" json:"framerate"` + EncoderPreset string `yaml:"encoderPreset" json:"encoderPreset"` } type files struct { MaxNumberInPlaylist int `yaml:"maxNumberInPlaylist"` } -//s3 is for configuring the s3 integration -type s3 struct { - Enabled bool `yaml:"enabled"` - Endpoint string `yaml:"endpoint"` - ServingEndpoint string `yaml:"servingEndpoint"` - AccessKey string `yaml:"accessKey"` - Secret string `yaml:"secret"` - Bucket string `yaml:"bucket"` - Region string `yaml:"region"` - ACL string `yaml:"acl"` +//S3 is for configuring the S3 integration +type S3 struct { + Enabled bool `yaml:"enabled" json:"enabled"` + Endpoint string `yaml:"endpoint" json:"endpoint,omitempty"` + ServingEndpoint string `yaml:"servingEndpoint" json:"servingEndpoint,omitempty"` + AccessKey string `yaml:"accessKey" json:"accessKey,omitempty"` + Secret string `yaml:"secret" json:"secret,omitempty"` + Bucket string `yaml:"bucket" json:"bucket,omitempty"` + Region string `yaml:"region" json:"region,omitempty"` + ACL string `yaml:"acl" json:"acl,omitempty"` } func (c *config) load(filePath string) error { diff --git a/config/configUtils.go b/config/configUtils.go index f15a443a0..5b9fa60ae 100644 --- a/config/configUtils.go +++ b/config/configUtils.go @@ -1,6 +1,9 @@ package config -import "sort" +import ( + "encoding/json" + "sort" +) func findHighestQuality(qualities []StreamQuality) int { type IndexedQuality struct { @@ -32,3 +35,15 @@ func findHighestQuality(qualities []StreamQuality) int { return indexedQualities[0].index } + +// MarshalJSON is a custom JSON marshal function for video stream qualities +func (q *StreamQuality) MarshalJSON() ([]byte, error) { + type Alias StreamQuality + return json.Marshal(&struct { + Framerate int `json:"framerate"` + *Alias + }{ + Framerate: q.GetFramerate(), + Alias: (*Alias)(q), + }) +} diff --git a/controllers/serverConfig.go b/controllers/serverConfig.go new file mode 100644 index 000000000..bfc7bb36c --- /dev/null +++ b/controllers/serverConfig.go @@ -0,0 +1,40 @@ +package controllers + +import ( + "encoding/json" + "net/http" + + "github.com/gabek/owncast/config" +) + +// GetServerConfig gets the config details of the server +func GetServerConfig(w http.ResponseWriter, r *http.Request) { + response := serverConfigAdminResponse{ + InstanceDetails: config.Config.InstanceDetails, + FFmpegPath: config.Config.GetFFMpegPath(), + WebServerPort: config.Config.GetPublicWebServerPort(), + VideoSettings: videoSettings{ + VideoQualityVariants: config.Config.GetVideoStreamQualities(), + SegmentLengthSeconds: config.Config.GetVideoSegmentSecondsLength(), + NumberOfPlaylistItems: config.Config.GetMaxNumberOfReferencedSegmentsInPlaylist(), + }, + S3: config.Config.S3, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +type serverConfigAdminResponse struct { + InstanceDetails config.InstanceDetails `json:"instanceDetails"` + FFmpegPath string `json:"ffmpegPath"` + WebServerPort int `json:"webServerPort"` + S3 config.S3 `json:"s3"` + VideoSettings videoSettings `json:"videoSettings"` +} + +type videoSettings struct { + VideoQualityVariants []config.StreamQuality `json:"videoQualityVariants"` + SegmentLengthSeconds int `json:"segmentLengthSeconds"` + NumberOfPlaylistItems int `json:"numberOfPlaylistItems"` +} diff --git a/router/router.go b/router/router.go index 8504fba1f..f80c81599 100644 --- a/router/router.go +++ b/router/router.go @@ -57,6 +57,9 @@ func Start() error { // Change the current streaming key in memory http.HandleFunc("/api/admin/changekey", middleware.RequireAdminAuth(admin.ChangeStreamKey)) + // Server config + http.HandleFunc("/api/admin/serverconfig", middleware.RequireAdminAuth(controllers.GetServerConfig)) + port := config.Config.GetPublicWebServerPort() log.Infof("Web server running on port: %d", port) From 236f25b772b2c50546ee6297c0aaa0aa123415a2 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Fri, 2 Oct 2020 00:06:14 -0700 Subject: [PATCH 08/30] Viewer metrics api (#208) * Add support for ending the inbound stream. Closes #191 * Add a simple success response to API requests * Add viewers over time API * Move controllers to admin directory --- controllers/{ => admin}/serverConfig.go | 2 +- controllers/admin/viewers.go | 15 ++++++++++++ metrics/alerting.go | 2 +- metrics/hardware.go | 4 ++-- metrics/metrics.go | 11 ++++----- metrics/timestampedValue.go | 8 +++++++ metrics/viewers.go | 31 +++++++++++++++++++++++++ router/router.go | 5 +++- 8 files changed, 66 insertions(+), 12 deletions(-) rename controllers/{ => admin}/serverConfig.go (98%) create mode 100644 controllers/admin/viewers.go create mode 100644 metrics/timestampedValue.go create mode 100644 metrics/viewers.go diff --git a/controllers/serverConfig.go b/controllers/admin/serverConfig.go similarity index 98% rename from controllers/serverConfig.go rename to controllers/admin/serverConfig.go index bfc7bb36c..39771314c 100644 --- a/controllers/serverConfig.go +++ b/controllers/admin/serverConfig.go @@ -1,4 +1,4 @@ -package controllers +package admin import ( "encoding/json" diff --git a/controllers/admin/viewers.go b/controllers/admin/viewers.go new file mode 100644 index 000000000..fe88e59a5 --- /dev/null +++ b/controllers/admin/viewers.go @@ -0,0 +1,15 @@ +package admin + +import ( + "encoding/json" + "net/http" + + "github.com/gabek/owncast/metrics" +) + +// GetViewersOverTime will return the number of viewers at points in time +func GetViewersOverTime(w http.ResponseWriter, r *http.Request) { + viewersOverTime := metrics.Metrics.Viewers + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(viewersOverTime) +} diff --git a/metrics/alerting.go b/metrics/alerting.go index 15a79a9cd..b8e7107d0 100644 --- a/metrics/alerting.go +++ b/metrics/alerting.go @@ -36,6 +36,6 @@ func handleRAMAlerting() { } } -func recentAverage(values []value) int { +func recentAverage(values []timestampedValue) int { return int((values[len(values)-1].Value + values[len(values)-2].Value) / 2) } diff --git a/metrics/hardware.go b/metrics/hardware.go index 7c0f9b8ae..4c0687c21 100644 --- a/metrics/hardware.go +++ b/metrics/hardware.go @@ -20,7 +20,7 @@ func collectCPUUtilization() { panic(err) } - metricValue := value{time.Now(), int(v[0])} + metricValue := timestampedValue{time.Now(), int(v[0])} Metrics.CPUUtilizations = append(Metrics.CPUUtilizations, metricValue) } @@ -30,6 +30,6 @@ func collectRAMUtilization() { } memoryUsage, _ := mem.VirtualMemory() - metricValue := value{time.Now(), int(memoryUsage.UsedPercent)} + metricValue := timestampedValue{time.Now(), int(memoryUsage.UsedPercent)} Metrics.RAMUtilizations = append(Metrics.RAMUtilizations, metricValue) } diff --git a/metrics/metrics.go b/metrics/metrics.go index 210fe92b7..4c00594b7 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -7,14 +7,10 @@ import ( // How often we poll for updates const metricsPollingInterval = 15 * time.Second -type value struct { - Time time.Time - Value int -} - type metrics struct { - CPUUtilizations []value - RAMUtilizations []value + CPUUtilizations []timestampedValue + RAMUtilizations []timestampedValue + Viewers []timestampedValue } // Metrics is the shared Metrics instance @@ -23,6 +19,7 @@ var Metrics *metrics // Start will begin the metrics collection and alerting func Start() { Metrics = new(metrics) + startViewerCollectionMetrics() for range time.Tick(metricsPollingInterval) { handlePolling() diff --git a/metrics/timestampedValue.go b/metrics/timestampedValue.go new file mode 100644 index 000000000..35e560141 --- /dev/null +++ b/metrics/timestampedValue.go @@ -0,0 +1,8 @@ +package metrics + +import "time" + +type timestampedValue struct { + Time time.Time `json:"time"` + Value int `json:"value"` +} diff --git a/metrics/viewers.go b/metrics/viewers.go new file mode 100644 index 000000000..6acc0abc5 --- /dev/null +++ b/metrics/viewers.go @@ -0,0 +1,31 @@ +package metrics + +import ( + "time" + + "github.com/gabek/owncast/core" +) + +// How often we poll for updates +const viewerMetricsPollingInterval = 5 * time.Minute + +func startViewerCollectionMetrics() { + collectViewerCount() + + for range time.Tick(viewerMetricsPollingInterval) { + collectViewerCount() + } +} + +func collectViewerCount() { + if len(Metrics.Viewers) > maxCollectionValues { + Metrics.Viewers = Metrics.Viewers[1:] + } + + count := core.GetStatus().ViewerCount + value := timestampedValue{ + Value: count, + Time: time.Now(), + } + Metrics.Viewers = append(Metrics.Viewers, value) +} diff --git a/router/router.go b/router/router.go index f80c81599..cf1ec759d 100644 --- a/router/router.go +++ b/router/router.go @@ -58,7 +58,10 @@ func Start() error { http.HandleFunc("/api/admin/changekey", middleware.RequireAdminAuth(admin.ChangeStreamKey)) // Server config - http.HandleFunc("/api/admin/serverconfig", middleware.RequireAdminAuth(controllers.GetServerConfig)) + http.HandleFunc("/api/admin/serverconfig", middleware.RequireAdminAuth(admin.GetServerConfig)) + + // Get viewer count over time + http.HandleFunc("/api/admin/viewersOverTime", middleware.RequireAdminAuth(admin.GetViewersOverTime)) port := config.Config.GetPublicWebServerPort() From f4fdc6c9519ec0bc3eca823fc55d93b4489b66d6 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Fri, 2 Oct 2020 00:12:47 -0700 Subject: [PATCH 09/30] Current broadcaster details admin api (#206) * Add support for ending the inbound stream. Closes #191 * Add a simple success response to API requests * Store inbound broadcast details for admin purposes * Add /api/admin/broadcaster endpoint * Reset broadcaster on disconnect * Move controller to admin directory --- .../admin/inboundBroadcasterDetails.go | 38 +++++++++++ core/core.go | 1 + core/rtmp/rtmp.go | 27 ++++++++ core/rtmp/utils.go | 64 +++++++++++++++++++ core/status.go | 10 +++ models/broadcaster.go | 33 ++++++++++ router/router.go | 3 + 7 files changed, 176 insertions(+) create mode 100644 controllers/admin/inboundBroadcasterDetails.go create mode 100644 core/rtmp/utils.go create mode 100644 models/broadcaster.go diff --git a/controllers/admin/inboundBroadcasterDetails.go b/controllers/admin/inboundBroadcasterDetails.go new file mode 100644 index 000000000..fa20198f3 --- /dev/null +++ b/controllers/admin/inboundBroadcasterDetails.go @@ -0,0 +1,38 @@ +package admin + +import ( + "encoding/json" + "net/http" + + "github.com/gabek/owncast/controllers" + "github.com/gabek/owncast/core" + "github.com/gabek/owncast/models" + "github.com/gabek/owncast/router/middleware" +) + +// GetInboundBroadasterDetails gets the details of the inbound broadcaster +func GetInboundBroadasterDetails(w http.ResponseWriter, r *http.Request) { + middleware.EnableCors(&w) + + broadcaster := core.GetBroadcaster() + if broadcaster == nil { + controllers.WriteSimpleResponse(w, false, "no broadcaster connected") + return + } + + response := inboundBroadasterDetailsResponse{ + models.BaseAPIResponse{ + true, + "", + }, + broadcaster, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +type inboundBroadasterDetailsResponse struct { + models.BaseAPIResponse + Broadcaster *models.Broadcaster `json:"broadcaster"` +} diff --git a/core/core.go b/core/core.go index 72d220047..b0c304549 100644 --- a/core/core.go +++ b/core/core.go @@ -21,6 +21,7 @@ var ( _storage models.ChunkStorageProvider _cleanupTimer *time.Timer _yp *yp.YP + _broadcaster *models.Broadcaster ) //Start starts up the core processing diff --git a/core/rtmp/rtmp.go b/core/rtmp/rtmp.go index ddab33e5a..53703b8a6 100644 --- a/core/rtmp/rtmp.go +++ b/core/rtmp/rtmp.go @@ -17,6 +17,7 @@ import ( "github.com/gabek/owncast/config" "github.com/gabek/owncast/core" "github.com/gabek/owncast/core/ffmpeg" + "github.com/gabek/owncast/models" "github.com/gabek/owncast/utils" "github.com/nareix/joy5/format/rtmp" ) @@ -62,10 +63,36 @@ func Start() { } } +func setCurrentBroadcasterInfo(t flvio.Tag, remoteAddr string) { + data, err := getInboundDetailsFromMetadata(t.DebugFields()) + if err != nil { + log.Errorln(err) + return + } + + broadcaster := models.Broadcaster{ + RemoteAddr: remoteAddr, + Time: time.Now(), + StreamDetails: models.InboundStreamDetails{ + Width: data.Width, + Height: data.Height, + VideoBitrate: int(data.VideoBitrate), + VideoCodec: getVideoCodec(data.VideoCodec), + VideoFramerate: data.VideoFramerate, + AudioBitrate: int(data.AudioBitrate), + AudioCodec: getAudioCodec(data.AudioCodec), + Encoder: data.Encoder, + }, + } + + core.SetBroadcaster(broadcaster) +} + func HandleConn(c *rtmp.Conn, nc net.Conn) { c.LogTagEvent = func(isRead bool, t flvio.Tag) { if t.Type == flvio.TAG_AMF0 { log.Tracef("%+v\n", t.DebugFields()) + setCurrentBroadcasterInfo(t, nc.RemoteAddr().String()) } } diff --git a/core/rtmp/utils.go b/core/rtmp/utils.go new file mode 100644 index 000000000..00580c01f --- /dev/null +++ b/core/rtmp/utils.go @@ -0,0 +1,64 @@ +package rtmp + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" + + "github.com/gabek/owncast/models" + "github.com/nareix/joy5/format/flv/flvio" +) + +func getInboundDetailsFromMetadata(metadata []interface{}) (models.RTMPStreamMetadata, error) { + metadataComponentsString := fmt.Sprintf("%+v", metadata) + re := regexp.MustCompile(`\{(.*?)\}`) + submatchall := re.FindAllString(metadataComponentsString, 1) + + if len(submatchall) == 0 { + return models.RTMPStreamMetadata{}, errors.New("unable to parse inbound metadata") + } + + metadataJSONString := submatchall[0] + var details models.RTMPStreamMetadata + json.Unmarshal([]byte(metadataJSONString), &details) + return details, nil +} + +func getAudioCodec(codec interface{}) string { + var codecID float64 + if assertedCodecID, ok := codec.(float64); ok { + codecID = assertedCodecID + } else { + return codec.(string) + } + + switch codecID { + case flvio.SOUND_MP3: + return "MP3" + case flvio.SOUND_AAC: + return "AAC" + case flvio.SOUND_SPEEX: + return "Speex" + } + + return "Unknown" +} + +func getVideoCodec(codec interface{}) string { + var codecID float64 + if assertedCodecID, ok := codec.(float64); ok { + codecID = assertedCodecID + } else { + return codec.(string) + } + + switch codecID { + case flvio.VIDEO_H264: + return "H.264" + case flvio.VIDEO_H265: + return "H.265" + } + + return "Unknown" +} diff --git a/core/status.go b/core/status.go index 4eef1831b..9a517182e 100644 --- a/core/status.go +++ b/core/status.go @@ -49,6 +49,7 @@ func SetStreamAsConnected() { func SetStreamAsDisconnected() { _stats.StreamConnected = false _stats.LastDisconnectTime = utils.NullTime{time.Now(), true} + _broadcaster = nil if _yp != nil { _yp.Stop() @@ -57,3 +58,12 @@ func SetStreamAsDisconnected() { ffmpeg.ShowStreamOfflineState() startCleanupTimer() } + +// SetBroadcaster will store the current inbound broadcasting details +func SetBroadcaster(broadcaster models.Broadcaster) { + _broadcaster = &broadcaster +} + +func GetBroadcaster() *models.Broadcaster { + return _broadcaster +} diff --git a/models/broadcaster.go b/models/broadcaster.go new file mode 100644 index 000000000..a32db1bdf --- /dev/null +++ b/models/broadcaster.go @@ -0,0 +1,33 @@ +package models + +import "time" + +// Broadcaster represents the details around the inbound broadcasting connection. +type Broadcaster struct { + RemoteAddr string `json:"remoteAddr"` + StreamDetails InboundStreamDetails `json:"streamDetails"` + Time time.Time `json:"time"` +} + +type InboundStreamDetails struct { + Width int `json:"width"` + Height int `json:"height"` + VideoFramerate int `json:"framerate"` + VideoBitrate int `json:"videoBitrate"` + VideoCodec string `json:"videoCodec"` + AudioBitrate int `json:"audioBitrate"` + AudioCodec string `json:"audioCodec"` + Encoder string `json:"encoder"` +} + +// RTMPStreamMetadata is the raw metadata that comes in with a RTMP connection +type RTMPStreamMetadata struct { + Width int `json:"width"` + Height int `json:"height"` + VideoBitrate float32 `json:"videodatarate"` + VideoCodec interface{} `json:"videocodecid"` + VideoFramerate int `json:"framerate"` + AudioBitrate float32 `json:"audiodatarate"` + AudioCodec interface{} `json:"audiocodecid"` + Encoder string `json:"encoder"` +} diff --git a/router/router.go b/router/router.go index cf1ec759d..6f42f5928 100644 --- a/router/router.go +++ b/router/router.go @@ -51,6 +51,9 @@ func Start() error { // Authenticated admin requests + // Current inbound broadcaster + http.HandleFunc("/api/admin/broadcaster", middleware.RequireAdminAuth(admin.GetInboundBroadasterDetails)) + // Disconnect inbound stream http.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(admin.DisconnectInboundConnection)) From e042c85f8898ce534ea5282fc0234a90fcdf9ed9 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Fri, 2 Oct 2020 12:18:08 -0700 Subject: [PATCH 10/30] Hardware status admin api (#218) * Add metrics for disk usage * Add admin API for hardware metrics * Fix error message alert --- controllers/admin/hardware.go | 16 ++++++++++++++++ metrics/alerting.go | 20 +++++++++++++++++--- metrics/hardware.go | 13 +++++++++++++ metrics/metrics.go | 21 +++++++++++++-------- router/router.go | 3 +++ 5 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 controllers/admin/hardware.go diff --git a/controllers/admin/hardware.go b/controllers/admin/hardware.go new file mode 100644 index 000000000..267c48bc4 --- /dev/null +++ b/controllers/admin/hardware.go @@ -0,0 +1,16 @@ +package admin + +import ( + "encoding/json" + "net/http" + + "github.com/gabek/owncast/metrics" +) + +// GetHardwareStats will return hardware utilization over time +func GetHardwareStats(w http.ResponseWriter, r *http.Request) { + metrics := metrics.Metrics + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(metrics) +} diff --git a/metrics/alerting.go b/metrics/alerting.go index b8e7107d0..08994cbeb 100644 --- a/metrics/alerting.go +++ b/metrics/alerting.go @@ -6,12 +6,14 @@ import ( const maxCPUAlertingThresholdPCT = 95 const maxRAMAlertingThresholdPCT = 95 +const maxDiskAlertingThresholdPCT = 95 -const alertingError = "The %s utilization of %d%% is higher than the alerting threshold of %d%%. This can cause issues with video generation and delivery. Please visit the documentation at http://owncast.online/docs/troubleshooting/ to help troubleshoot this issue." +const alertingError = "The %s utilization of %d%% can cause issues with video generation and delivery. Please visit the documentation at http://owncast.online/docs/troubleshooting/ to help troubleshoot this issue." func handleAlerting() { handleCPUAlerting() handleRAMAlerting() + handleDiskAlerting() } func handleCPUAlerting() { @@ -21,7 +23,7 @@ func handleCPUAlerting() { avg := recentAverage(Metrics.CPUUtilizations) if avg > maxCPUAlertingThresholdPCT { - log.Errorf(alertingError, "CPU", avg, maxCPUAlertingThresholdPCT) + log.Errorf(alertingError, "CPU", maxCPUAlertingThresholdPCT) } } @@ -32,7 +34,19 @@ func handleRAMAlerting() { avg := recentAverage(Metrics.RAMUtilizations) if avg > maxRAMAlertingThresholdPCT { - log.Errorf(alertingError, "memory", avg, maxRAMAlertingThresholdPCT) + log.Errorf(alertingError, "memory", maxRAMAlertingThresholdPCT) + } +} + +func handleDiskAlerting() { + if len(Metrics.DiskUtilizations) < 2 { + return + } + + avg := recentAverage(Metrics.DiskUtilizations) + + if avg > maxDiskAlertingThresholdPCT { + log.Errorf(alertingError, "disk", maxRAMAlertingThresholdPCT) } } diff --git a/metrics/hardware.go b/metrics/hardware.go index 4c0687c21..92ee9a175 100644 --- a/metrics/hardware.go +++ b/metrics/hardware.go @@ -4,6 +4,7 @@ import ( "time" "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" "github.com/shirou/gopsutil/mem" ) @@ -33,3 +34,15 @@ func collectRAMUtilization() { metricValue := timestampedValue{time.Now(), int(memoryUsage.UsedPercent)} Metrics.RAMUtilizations = append(Metrics.RAMUtilizations, metricValue) } + +func collectDiskUtilization() { + path := "./" + diskUse, _ := disk.Usage(path) + + if len(Metrics.DiskUtilizations) > maxCollectionValues { + Metrics.DiskUtilizations = Metrics.DiskUtilizations[1:] + } + + metricValue := timestampedValue{time.Now(), int(diskUse.UsedPercent)} + Metrics.DiskUtilizations = append(Metrics.DiskUtilizations, metricValue) +} diff --git a/metrics/metrics.go b/metrics/metrics.go index 4c00594b7..a32345a6d 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -5,21 +5,25 @@ import ( ) // How often we poll for updates -const metricsPollingInterval = 15 * time.Second +const metricsPollingInterval = 1 * time.Minute -type metrics struct { - CPUUtilizations []timestampedValue - RAMUtilizations []timestampedValue - Viewers []timestampedValue +// CollectedMetrics stores different collected + timestamped values +type CollectedMetrics struct { + CPUUtilizations []timestampedValue `json:"cpu"` + RAMUtilizations []timestampedValue `json:"memory"` + DiskUtilizations []timestampedValue `json:"disk"` + + Viewers []timestampedValue `json:"-"` } // Metrics is the shared Metrics instance -var Metrics *metrics +var Metrics *CollectedMetrics // Start will begin the metrics collection and alerting func Start() { - Metrics = new(metrics) - startViewerCollectionMetrics() + Metrics = new(CollectedMetrics) + go startViewerCollectionMetrics() + handlePolling() for range time.Tick(metricsPollingInterval) { handlePolling() @@ -30,6 +34,7 @@ func handlePolling() { // Collect hardware stats collectCPUUtilization() collectRAMUtilization() + collectDiskUtilization() // Alerting handleAlerting() diff --git a/router/router.go b/router/router.go index 6f42f5928..4acae1821 100644 --- a/router/router.go +++ b/router/router.go @@ -66,6 +66,9 @@ func Start() error { // Get viewer count over time http.HandleFunc("/api/admin/viewersOverTime", middleware.RequireAdminAuth(admin.GetViewersOverTime)) + // Get hardware stats + http.HandleFunc("/api/admin/hardwarestats", middleware.RequireAdminAuth(admin.GetHardwareStats)) + port := config.Config.GetPublicWebServerPort() log.Infof("Web server running on port: %d", port) From 63a757ef2326b08307c3f2db6fc54d81d0cd8fb0 Mon Sep 17 00:00:00 2001 From: Ahmad Karlam Date: Sat, 3 Oct 2020 20:29:29 +0700 Subject: [PATCH 11/30] Use moment js for diff and format date --- webroot/index.html | 2 ++ webroot/js/components/chat/message.js | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/webroot/index.html b/webroot/index.html index 04eb34aba..231b710fb 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -35,6 +35,8 @@ + + diff --git a/webroot/js/components/chat/message.js b/webroot/js/components/chat/message.js index 3fa246bcf..bd1d0ca8d 100644 --- a/webroot/js/components/chat/message.js +++ b/webroot/js/components/chat/message.js @@ -8,6 +8,16 @@ import { generateAvatar } from '../../utils/helpers.js'; import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; export default class Message extends Component { + formatTimestamp(sentAt) { + sentAt = moment(sentAt); + + if (moment().diff(sentAt, 'days') >= 1) { + return `${sentAt.format('MMM D HH:mm:ss')}` + } + + return sentAt.format("HH:mm:ss"); + } + render(props) { const { message, username } = props; const { type } = message; @@ -20,6 +30,7 @@ export default class Message extends Component { const authorColor = messageBubbleColorForString(author); const avatarBgColor = { backgroundColor: authorColor }; const authorTextColor = { color: authorColor }; + return ( html`
@@ -35,7 +46,7 @@ export default class Message extends Component {
Date: Sat, 3 Oct 2020 13:42:48 -0700 Subject: [PATCH 12/30] apply scrollbar tricks to moz browser; make scroll colors on emoji picker easier to see; make emoji hover color easier to see --- webroot/js/app.js | 2 +- webroot/styles/app.css | 4 ++++ webroot/styles/chat.css | 9 +++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/webroot/js/app.js b/webroot/js/app.js index 9fe34ca40..96eea8520 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -554,7 +554,7 @@ export default class App extends Component { websocket=${websocket} username=${username} userAvatarImage=${userAvatarImage} - chatInputEnabled=${chatInputEnabled} + chatInputEnabled=${true||chatInputEnabled} />
`; diff --git a/webroot/styles/app.css b/webroot/styles/app.css index 30774ee2b..d3fa92a3e 100644 --- a/webroot/styles/app.css +++ b/webroot/styles/app.css @@ -14,6 +14,7 @@ May have overrides for other components with own stylesheets. html { font-size: 14px; + scrollbar-width: none; } a:hover { @@ -24,6 +25,9 @@ a:hover { width: 0px; background: transparent; } +.scrollbar-hidden { + scrollbar-width: none; /* moz only */ +} #app-container * { diff --git a/webroot/styles/chat.css b/webroot/styles/chat.css index eab6d9932..362c2c7ac 100644 --- a/webroot/styles/chat.css +++ b/webroot/styles/chat.css @@ -80,6 +80,7 @@ .emoji-picker.owncast { --secondary-text-color: rgba(255,255,255,.5); --category-button-color: rgba(255,255,255,.5); + --hover-color: rgba(255,255,255,.25); background: rgba(26,32,44,1); /* tailwind bg-gray-900 */ color: rgba(226,232,240,1); /* tailwind text-gray-300 */ @@ -101,15 +102,19 @@ } .emoji-picker__emojis::-webkit-scrollbar-track { border-radius: 8px; - background-color: rgba(0,0,0,.2); + background-color: black; box-shadow: inset 0 0 3px rgba(0,0,0,0.3); } .emoji-picker__emojis::-webkit-scrollbar-thumb { - background-color: rgba(0,0,0,.45); + background-color: var(--category-button-color); border-radius: 8px; } +.emoji-picker__emojis { + scrollbar-color: var(--category-button-color) black; +} + /******************************/ From ff3a50dc039b46aab16dcec05c67c6c7719bd7a5 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Sat, 3 Oct 2020 13:50:08 -0700 Subject: [PATCH 13/30] linty stuff --- webroot/index.html | 5 +++-- webroot/js/app.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/webroot/index.html b/webroot/index.html index 04eb34aba..d2a0ff9a5 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -1,6 +1,7 @@ - + + Owncast @@ -77,7 +78,7 @@

- This Owncast stream requires Javascript to play. + This Owncast stream requires Javascript to play.

diff --git a/webroot/js/app.js b/webroot/js/app.js index 96eea8520..9fe34ca40 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -554,7 +554,7 @@ export default class App extends Component { websocket=${websocket} username=${username} userAvatarImage=${userAvatarImage} - chatInputEnabled=${true||chatInputEnabled} + chatInputEnabled=${chatInputEnabled} /> `; From 922dfec77a92acc5363d1b59fff1ea16151325d8 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Sat, 3 Oct 2020 14:35:03 -0700 Subject: [PATCH 14/30] Remove custom paths and add constants (#216) --- config/config.go | 21 +-------------------- config/constants.go | 12 ++++++++++++ config/defaults.go | 2 -- controllers/emoji.go | 3 ++- controllers/index.go | 12 +++--------- core/core.go | 23 ++++++++++++----------- core/ffmpeg/thumbnailGenerator.go | 4 ++-- core/ffmpeg/transcoder.go | 6 +++--- core/playlist/monitor.go | 12 +++++------- core/status.go | 4 ++-- core/storageproviders/s3Storage.go | 4 ++-- router/router.go | 24 +++++++++++------------- 12 files changed, 55 insertions(+), 72 deletions(-) create mode 100644 config/constants.go diff --git a/config/config.go b/config/config.go index 1f80003b2..5793ddcba 100644 --- a/config/config.go +++ b/config/config.go @@ -15,15 +15,12 @@ var _default config type config struct { ChatDatabaseFilePath string `yaml:"chatDatabaseFile"` - DisableWebFeatures bool `yaml:"disableWebFeatures"` EnableDebugFeatures bool `yaml:"-"` FFMpegPath string `yaml:"ffmpegPath"` Files files `yaml:"files"` InstanceDetails InstanceDetails `yaml:"instanceDetails"` - PrivateHLSPath string `yaml:"privateHLSPath"` - PublicHLSPath string `yaml:"publicHLSPath"` S3 S3 `yaml:"s3"` - VersionInfo string `yaml:"-"` + VersionInfo string `yaml:"-"` // For storing the version/build number VideoSettings videoSettings `yaml:"videoSettings"` WebServerPort int `yaml:"webServerPort"` YP yp `yaml:"yp"` @@ -158,22 +155,6 @@ func (c *config) GetVideoSegmentSecondsLength() int { return _default.GetVideoSegmentSecondsLength() } -func (c *config) GetPublicHLSSavePath() string { - if c.PublicHLSPath != "" { - return c.PublicHLSPath - } - - return _default.PublicHLSPath -} - -func (c *config) GetPrivateHLSSavePath() string { - if c.PrivateHLSPath != "" { - return c.PrivateHLSPath - } - - return _default.PrivateHLSPath -} - func (c *config) GetPublicWebServerPort() int { if c.WebServerPort != 0 { return c.WebServerPort diff --git a/config/constants.go b/config/constants.go new file mode 100644 index 000000000..66bded1b7 --- /dev/null +++ b/config/constants.go @@ -0,0 +1,12 @@ +package config + +import "path/filepath" + +const ( + WebRoot = "webroot" + PrivateHLSStoragePath = "hls" +) + +var ( + PublicHLSStoragePath = filepath.Join(WebRoot, "hls") +) diff --git a/config/defaults.go b/config/defaults.go index 66a654e41..87d170eb4 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -12,8 +12,6 @@ func getDefaults() config { defaults.FFMpegPath = getDefaultFFMpegPath() defaults.VideoSettings.ChunkLengthInSeconds = 4 defaults.Files.MaxNumberInPlaylist = 5 - defaults.PublicHLSPath = "webroot/hls" - defaults.PrivateHLSPath = "hls" defaults.VideoSettings.OfflineContent = "static/offline.m4v" defaults.InstanceDetails.ExtraInfoFile = "/static/content.md" defaults.YP.Enabled = false diff --git a/controllers/emoji.go b/controllers/emoji.go index 3c2f64af1..808787e1a 100644 --- a/controllers/emoji.go +++ b/controllers/emoji.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" + "github.com/gabek/owncast/config" "github.com/gabek/owncast/models" log "github.com/sirupsen/logrus" ) @@ -20,7 +21,7 @@ const emojiPath = "/img/emoji" // Relative to webroot func GetCustomEmoji(w http.ResponseWriter, r *http.Request) { emojiList := make([]models.CustomEmoji, 0) - fullPath := filepath.Join("webroot", emojiPath) + fullPath := filepath.Join(config.WebRoot, emojiPath) files, err := ioutil.ReadDir(fullPath) if err != nil { log.Errorln(err) diff --git a/controllers/index.go b/controllers/index.go index 32f8edc41..f0a659f2a 100644 --- a/controllers/index.go +++ b/controllers/index.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "path" + "path/filepath" "strings" "text/template" @@ -30,13 +31,6 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) { isIndexRequest := r.URL.Path == "/" || r.URL.Path == "/index.html" - // Reject requests for the web UI if it's disabled. - if isIndexRequest && config.Config.DisableWebFeatures { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("404 - y u ask 4 this? If this is an error let us know: https://github.com/owncast/owncast/issues")) - return - } - // For search engine bots and social scrapers return a special // server-rendered page. if utils.IsUserAgentABot(r.UserAgent()) && isIndexRequest { @@ -60,7 +54,7 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) { // Set a cache control max-age header middleware.SetCachingHeaders(w, r) - http.ServeFile(w, r, path.Join("webroot", r.URL.Path)) + http.ServeFile(w, r, path.Join(config.WebRoot, r.URL.Path)) } // Return a basic HTML page with server-rendered metadata from the config file @@ -75,7 +69,7 @@ func handleScraperMetadataPage(w http.ResponseWriter, r *http.Request) { // If the thumbnail does not exist or we're offline then just use the logo image var thumbnailURL string - if status.Online && utils.DoesFileExists("webroot/thumbnail.jpg") { + if status.Online && utils.DoesFileExists(filepath.Join(config.WebRoot, "thumbnail.jpg")) { thumbnail, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, "/thumbnail.jpg")) if err != nil { log.Errorln(err) diff --git a/core/core.go b/core/core.go index b0c304549..5b173b89b 100644 --- a/core/core.go +++ b/core/core.go @@ -3,6 +3,7 @@ package core import ( "os" "path" + "path/filepath" "strconv" "time" @@ -56,8 +57,8 @@ func Start() error { func createInitialOfflineState() error { // Provide default files - if !utils.DoesFileExists("webroot/thumbnail.jpg") { - if err := utils.Copy("static/logo.png", "webroot/thumbnail.jpg"); err != nil { + if !utils.DoesFileExists(filepath.Join(config.WebRoot, "thumbnail.jpg")) { + if err := utils.Copy("static/logo.png", filepath.Join(config.WebRoot, "thumbnail.jpg")); err != nil { return err } } @@ -93,22 +94,22 @@ func resetDirectories() { log.Trace("Resetting file directories to a clean slate.") // Wipe the public, web-accessible hls data directory - os.RemoveAll(config.Config.GetPublicHLSSavePath()) - os.RemoveAll(config.Config.GetPrivateHLSSavePath()) - os.MkdirAll(config.Config.GetPublicHLSSavePath(), 0777) - os.MkdirAll(config.Config.GetPrivateHLSSavePath(), 0777) + os.RemoveAll(config.PublicHLSStoragePath) + os.RemoveAll(config.PrivateHLSStoragePath) + os.MkdirAll(config.PublicHLSStoragePath, 0777) + os.MkdirAll(config.PrivateHLSStoragePath, 0777) // Remove the previous thumbnail - os.Remove("webroot/thumbnail.jpg") + os.Remove(filepath.Join(config.WebRoot, "thumbnail.jpg")) // Create private hls data dirs if len(config.Config.VideoSettings.StreamQualities) != 0 { for index := range config.Config.VideoSettings.StreamQualities { - os.MkdirAll(path.Join(config.Config.GetPrivateHLSSavePath(), strconv.Itoa(index)), 0777) - os.MkdirAll(path.Join(config.Config.GetPublicHLSSavePath(), strconv.Itoa(index)), 0777) + os.MkdirAll(path.Join(config.PrivateHLSStoragePath, strconv.Itoa(index)), 0777) + os.MkdirAll(path.Join(config.PublicHLSStoragePath, strconv.Itoa(index)), 0777) } } else { - os.MkdirAll(path.Join(config.Config.GetPrivateHLSSavePath(), strconv.Itoa(0)), 0777) - os.MkdirAll(path.Join(config.Config.GetPublicHLSSavePath(), strconv.Itoa(0)), 0777) + os.MkdirAll(path.Join(config.PrivateHLSStoragePath, strconv.Itoa(0)), 0777) + os.MkdirAll(path.Join(config.PublicHLSStoragePath, strconv.Itoa(0)), 0777) } } diff --git a/core/ffmpeg/thumbnailGenerator.go b/core/ffmpeg/thumbnailGenerator.go index 892748d91..121c53900 100644 --- a/core/ffmpeg/thumbnailGenerator.go +++ b/core/ffmpeg/thumbnailGenerator.go @@ -39,8 +39,8 @@ func StartThumbnailGenerator(chunkPath string, variantIndex int) { func fireThumbnailGenerator(chunkPath string, variantIndex int) error { // JPG takes less time to encode than PNG - outputFile := path.Join("webroot", "thumbnail.jpg") - previewGifFile := path.Join("webroot", "preview.gif") + outputFile := path.Join(config.WebRoot, "thumbnail.jpg") + previewGifFile := path.Join(config.WebRoot, "preview.gif") framePath := path.Join(chunkPath, strconv.Itoa(variantIndex)) files, err := ioutil.ReadDir(framePath) diff --git a/core/ffmpeg/transcoder.go b/core/ffmpeg/transcoder.go index d320cdb96..6eb874623 100644 --- a/core/ffmpeg/transcoder.go +++ b/core/ffmpeg/transcoder.go @@ -188,15 +188,15 @@ func NewTranscoder() Transcoder { var outputPath string if config.Config.S3.Enabled { // Segments are not available via the local HTTP server - outputPath = config.Config.GetPrivateHLSSavePath() + outputPath = config.PrivateHLSStoragePath } else { // Segments are available via the local HTTP server - outputPath = config.Config.GetPublicHLSSavePath() + outputPath = config.PublicHLSStoragePath } transcoder.segmentOutputPath = outputPath // Playlists are available via the local HTTP server - transcoder.playlistOutputPath = config.Config.GetPublicHLSSavePath() + transcoder.playlistOutputPath = config.PublicHLSStoragePath transcoder.input = utils.GetTemporaryPipePath() transcoder.segmentLengthSeconds = config.Config.GetVideoSegmentSecondsLength() diff --git a/core/playlist/monitor.go b/core/playlist/monitor.go index cba85dc89..78f986f25 100644 --- a/core/playlist/monitor.go +++ b/core/playlist/monitor.go @@ -26,7 +26,7 @@ var ( func StartVideoContentMonitor(storage models.ChunkStorageProvider) error { _storage = storage - pathToMonitor := config.Config.GetPrivateHLSSavePath() + 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)) @@ -63,11 +63,9 @@ func StartVideoContentMonitor(storage models.ChunkStorageProvider) error { continue } - // fmt.Println(event.Op, relativePath) - // Handle updates to the master playlist by copying it to webroot - if relativePath == path.Join(config.Config.GetPrivateHLSSavePath(), "stream.m3u8") { - utils.Copy(event.Path, path.Join(config.Config.GetPublicHLSSavePath(), "stream.m3u8")) + 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 @@ -82,7 +80,7 @@ func StartVideoContentMonitor(storage models.ChunkStorageProvider) error { newObjectPathChannel := make(chan string, 1) go func() { - newObjectPath, err := storage.Save(path.Join(config.Config.GetPrivateHLSSavePath(), segment.RelativeUploadPath), 0) + 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) } @@ -155,5 +153,5 @@ func updateVariantPlaylist(fullPath string) error { playlistString := string(playlistBytes) playlistString = _storage.GenerateRemotePlaylist(playlistString, variant) - return WritePlaylist(playlistString, path.Join(config.Config.GetPublicHLSSavePath(), relativePath)) + return WritePlaylist(playlistString, path.Join(config.PublicHLSStoragePath, relativePath)) } diff --git a/core/status.go b/core/status.go index 9a517182e..758ff4619 100644 --- a/core/status.go +++ b/core/status.go @@ -33,9 +33,9 @@ func SetStreamAsConnected() { _stats.LastConnectTime = utils.NullTime{time.Now(), true} _stats.LastDisconnectTime = utils.NullTime{time.Now(), false} - chunkPath := config.Config.GetPublicHLSSavePath() + chunkPath := config.PublicHLSStoragePath if usingExternalStorage { - chunkPath = config.Config.GetPrivateHLSSavePath() + chunkPath = config.PrivateHLSStoragePath } if _yp != nil { diff --git a/core/storageproviders/s3Storage.go b/core/storageproviders/s3Storage.go index 77d61a7a5..d437db45a 100644 --- a/core/storageproviders/s3Storage.go +++ b/core/storageproviders/s3Storage.go @@ -28,7 +28,7 @@ type S3Storage struct { s3Bucket string s3AccessKey string s3Secret string - s3ACL string + s3ACL string } //Setup sets up the s3 storage for saving the video to s3 @@ -94,7 +94,7 @@ func (s *S3Storage) GenerateRemotePlaylist(playlist string, variant models.Varia if fullRemotePath == nil { line = "" } else if s.s3ServingEndpoint != "" { - line = fmt.Sprintf("%s/%s/%s", s.s3ServingEndpoint, config.Config.GetPrivateHLSSavePath(), fullRemotePath.RelativeUploadPath) + line = fmt.Sprintf("%s/%s/%s", s.s3ServingEndpoint, config.PrivateHLSStoragePath, fullRemotePath.RelativeUploadPath) } else { line = fullRemotePath.RemoteID } diff --git a/router/router.go b/router/router.go index 4acae1821..64ad1016f 100644 --- a/router/router.go +++ b/router/router.go @@ -30,24 +30,22 @@ func Start() error { // custom emoji supported in the chat http.HandleFunc("/api/emoji", controllers.GetCustomEmoji) - if !config.Config.DisableWebFeatures { - // websocket chat server - go chat.Start() + // websocket chat server + go chat.Start() - // chat rest api - http.HandleFunc("/api/chat", controllers.GetChatMessages) + // chat rest api + http.HandleFunc("/api/chat", controllers.GetChatMessages) - // web config api - http.HandleFunc("/api/config", controllers.GetWebConfig) + // web config api + http.HandleFunc("/api/config", controllers.GetWebConfig) - // chat embed - http.HandleFunc("/embed/chat", controllers.GetChatEmbed) + // chat embed + http.HandleFunc("/embed/chat", controllers.GetChatEmbed) - // video embed - http.HandleFunc("/embed/video", controllers.GetVideoEmbed) + // video embed + http.HandleFunc("/embed/video", controllers.GetVideoEmbed) - http.HandleFunc("/api/yp", yp.GetYPResponse) - } + http.HandleFunc("/api/yp", yp.GetYPResponse) // Authenticated admin requests From d27d4a798f259030e8bb27fc716c429e0d495a64 Mon Sep 17 00:00:00 2001 From: Ahmad Karlam Date: Sun, 4 Oct 2020 09:01:46 +0700 Subject: [PATCH 15/30] Remove moment js and use standard library date from javascript --- webroot/index.html | 3 --- webroot/js/components/chat/message.js | 23 ++++++++++++----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/webroot/index.html b/webroot/index.html index 231b710fb..79c199c64 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -35,9 +35,6 @@ - - - diff --git a/webroot/js/components/chat/message.js b/webroot/js/components/chat/message.js index bd1d0ca8d..2e5fc7695 100644 --- a/webroot/js/components/chat/message.js +++ b/webroot/js/components/chat/message.js @@ -1,21 +1,23 @@ -import { h, Component } from 'https://unpkg.com/preact?module'; +import {Component, h} from 'https://unpkg.com/preact?module'; import htm from 'https://unpkg.com/htm?module'; -const html = htm.bind(h); +import {messageBubbleColorForString} from '../../utils/user-colors.js'; +import {formatMessageText} from '../../utils/chat.js'; +import {generateAvatar} from '../../utils/helpers.js'; +import {SOCKET_MESSAGE_TYPES} from '../../utils/websocket.js'; -import { messageBubbleColorForString } from '../../utils/user-colors.js'; -import { formatMessageText } from '../../utils/chat.js'; -import { generateAvatar } from '../../utils/helpers.js'; -import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; +const html = htm.bind(h); export default class Message extends Component { formatTimestamp(sentAt) { - sentAt = moment(sentAt); + sentAt = new Date(sentAt); - if (moment().diff(sentAt, 'days') >= 1) { - return `${sentAt.format('MMM D HH:mm:ss')}` + let diffInDays = ((new Date()) - sentAt) / (24 * 3600 * 1000); + if (diffInDays >= -1) { + return `${sentAt.toLocaleDateString('en-US', {dateStyle: 'medium'})} at ` + + sentAt.toLocaleTimeString(); } - return sentAt.format("HH:mm:ss"); + return sentAt.toLocaleTimeString(); } render(props) { @@ -30,7 +32,6 @@ export default class Message extends Component { const authorColor = messageBubbleColorForString(author); const avatarBgColor = { backgroundColor: authorColor }; const authorTextColor = { color: authorColor }; - return ( html`
From 2abde9186c87cf7c691b07e9241f783c9a568570 Mon Sep 17 00:00:00 2001 From: Ahmad Karlam Date: Sun, 4 Oct 2020 09:08:05 +0700 Subject: [PATCH 16/30] refactoring --- webroot/js/components/chat/message.js | 26 +++++++------------------- webroot/js/utils/chat.js | 12 ++++++++++++ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/webroot/js/components/chat/message.js b/webroot/js/components/chat/message.js index 2e5fc7695..317a9006a 100644 --- a/webroot/js/components/chat/message.js +++ b/webroot/js/components/chat/message.js @@ -1,25 +1,13 @@ -import {Component, h} from 'https://unpkg.com/preact?module'; +import { h, Component } from 'https://unpkg.com/preact?module'; import htm from 'https://unpkg.com/htm?module'; -import {messageBubbleColorForString} from '../../utils/user-colors.js'; -import {formatMessageText} from '../../utils/chat.js'; -import {generateAvatar} from '../../utils/helpers.js'; -import {SOCKET_MESSAGE_TYPES} from '../../utils/websocket.js'; - const html = htm.bind(h); +import { messageBubbleColorForString } from '../../utils/user-colors.js'; +import { formatMessageText, formatTimestamp } from '../../utils/chat.js'; +import { generateAvatar } from '../../utils/helpers.js'; +import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; + export default class Message extends Component { - formatTimestamp(sentAt) { - sentAt = new Date(sentAt); - - let diffInDays = ((new Date()) - sentAt) / (24 * 3600 * 1000); - if (diffInDays >= -1) { - return `${sentAt.toLocaleDateString('en-US', {dateStyle: 'medium'})} at ` + - sentAt.toLocaleTimeString(); - } - - return sentAt.toLocaleTimeString(); - } - render(props) { const { message, username } = props; const { type } = message; @@ -47,7 +35,7 @@ export default class Message extends Component {
= -1) { + return `${sentAt.toLocaleDateString('en-US', {dateStyle: 'medium'})} at ` + + sentAt.toLocaleTimeString(); + } + + return sentAt.toLocaleTimeString(); +} From 1c03e83c3143e6819f7197d2c9d37795697ba7dd Mon Sep 17 00:00:00 2001 From: Ahmad Karlam Date: Sun, 4 Oct 2020 09:08:40 +0700 Subject: [PATCH 17/30] Typo --- webroot/js/utils/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js index 60a43be5a..e09bbdd48 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -283,7 +283,7 @@ export function formatTimestamp(sentAt) { sentAt = new Date(sentAt); let diffInDays = ((new Date()) - sentAt) / (24 * 3600 * 1000); - if (diffInDays >= -1) { + if (diffInDays >= 1) { return `${sentAt.toLocaleDateString('en-US', {dateStyle: 'medium'})} at ` + sentAt.toLocaleTimeString(); } From 8c380f118af3dec5675d27e2a78b84afa0e0b2e2 Mon Sep 17 00:00:00 2001 From: Ahmad Karlam Date: Sun, 4 Oct 2020 10:57:45 +0700 Subject: [PATCH 18/30] refactor: declare format timestamp as variable for consistency --- webroot/js/components/chat/message.js | 3 ++- webroot/js/utils/chat.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/webroot/js/components/chat/message.js b/webroot/js/components/chat/message.js index 317a9006a..c57515088 100644 --- a/webroot/js/components/chat/message.js +++ b/webroot/js/components/chat/message.js @@ -16,6 +16,7 @@ export default class Message extends Component { const { image, author, body, timestamp } = message; const formattedMessage = formatMessageText(body, username); const avatar = image || generateAvatar(author); + const formattedTimestamp = formatTimestamp(timestamp); const authorColor = messageBubbleColorForString(author); const avatarBgColor = { backgroundColor: authorColor }; @@ -35,7 +36,7 @@ export default class Message extends Component {
= 1) { - return `${sentAt.toLocaleDateString('en-US', {dateStyle: 'medium'})} at ` + + return `Sent at ${sentAt.toLocaleDateString('en-US', {dateStyle: 'medium'})} at ` + sentAt.toLocaleTimeString(); } - return sentAt.toLocaleTimeString(); + return `Sent at ${sentAt.toLocaleTimeString()}`; } From 29ef90e38413aac4c9ced199840b69687c833453 Mon Sep 17 00:00:00 2001 From: Ahmad Karlam Date: Sun, 4 Oct 2020 10:58:02 +0700 Subject: [PATCH 19/30] fix: check if date is invalid --- webroot/js/utils/chat.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js index 638f3964c..f63890ac6 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -281,6 +281,9 @@ export function convertOnPaste( event = { preventDefault() {} }) { export function formatTimestamp(sentAt) { sentAt = new Date(sentAt); + if (isNaN(sentAt)) { + return ''; + } let diffInDays = ((new Date()) - sentAt) / (24 * 3600 * 1000); if (diffInDays >= 1) { From bb9c788306c72eacb0338411703fe3e9043393fe Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Sat, 3 Oct 2020 23:06:48 -0700 Subject: [PATCH 20/30] Support CORS+Basic auth together --- controllers/admin/inboundBroadcasterDetails.go | 3 --- router/middleware/auth.go | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/controllers/admin/inboundBroadcasterDetails.go b/controllers/admin/inboundBroadcasterDetails.go index fa20198f3..7abfe0e5c 100644 --- a/controllers/admin/inboundBroadcasterDetails.go +++ b/controllers/admin/inboundBroadcasterDetails.go @@ -7,13 +7,10 @@ import ( "github.com/gabek/owncast/controllers" "github.com/gabek/owncast/core" "github.com/gabek/owncast/models" - "github.com/gabek/owncast/router/middleware" ) // GetInboundBroadasterDetails gets the details of the inbound broadcaster func GetInboundBroadasterDetails(w http.ResponseWriter, r *http.Request) { - middleware.EnableCors(&w) - broadcaster := core.GetBroadcaster() if broadcaster == nil { controllers.WriteSimpleResponse(w, false, "no broadcaster connected") diff --git a/router/middleware/auth.go b/router/middleware/auth.go index e168b0ac9..13974ab1f 100644 --- a/router/middleware/auth.go +++ b/router/middleware/auth.go @@ -13,11 +13,24 @@ import ( func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc { username := "admin" password := config.Config.VideoSettings.StreamingKey + realm := "Owncast Authenticated Request" return func(w http.ResponseWriter, r *http.Request) { + // The following line is kind of a work around. + // If you want HTTP Basic Auth + Cors it requires _explicit_ origins to be provided in the + // Access-Control-Allow-Origin header. So we just pull out the origin header and specify it. + // If we want to lock down admin APIs to not be CORS accessible for anywhere, this is where we would do that. + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization") + + // For request needing CORS, send a 200. + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } user, pass, ok := r.BasicAuth() - realm := "Owncast Authenticated Request" // Failed if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 { From 27a4c8c895c5529c2a442ca203a7c527d5494381 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Sun, 4 Oct 2020 18:43:31 -0700 Subject: [PATCH 21/30] Move all remote dependencies to be local (#220) * Experiment with javascript bundling * Experiment with snowpack. Making progress * Success! Uses local js modules and assets and no cdns * Missing local css --- build/javascript/package-lock.json | 3225 +++++++++++ build/javascript/package.json | 40 + webroot/index-standalone-chat.html | 11 +- webroot/index-video-only.html | 15 +- webroot/index.html | 25 +- webroot/js/app-standalone-chat.js | 4 +- webroot/js/app-video-only.js | 4 +- webroot/js/app.js | 4 +- webroot/js/components/chat/chat-input.js | 7 +- webroot/js/components/chat/chat.js | 4 +- .../js/components/chat/content-editable.js | 2 +- webroot/js/components/chat/message.js | 4 +- webroot/js/components/chat/username.js | 4 +- webroot/js/components/player.js | 20 +- webroot/js/components/social.js | 4 +- webroot/js/utils/chat.js | 1 + .../web_modules/@joeattardi/emoji-button.js | 3 + .../@justinribeiro/lite-youtube.js | 301 + .../@videojs/themes/fantasy/index.css | 116 + .../common/_commonjsHelpers-37fa8da4.js | 25 + .../js/web_modules/common/window-2f8a9a85.js | 44 + webroot/js/web_modules/htm.js | 3 + webroot/js/web_modules/import-map.json | 14 + webroot/js/web_modules/preact.js | 3 + webroot/js/web_modules/showdown.js | 5044 +++++++++++++++++ 25 files changed, 8870 insertions(+), 57 deletions(-) create mode 100644 build/javascript/package-lock.json create mode 100644 build/javascript/package.json create mode 100644 webroot/js/web_modules/@joeattardi/emoji-button.js create mode 100644 webroot/js/web_modules/@justinribeiro/lite-youtube.js create mode 100644 webroot/js/web_modules/@videojs/themes/fantasy/index.css create mode 100644 webroot/js/web_modules/common/_commonjsHelpers-37fa8da4.js create mode 100644 webroot/js/web_modules/common/window-2f8a9a85.js create mode 100644 webroot/js/web_modules/htm.js create mode 100644 webroot/js/web_modules/import-map.json create mode 100644 webroot/js/web_modules/preact.js create mode 100644 webroot/js/web_modules/showdown.js diff --git a/build/javascript/package-lock.json b/build/javascript/package-lock.json new file mode 100644 index 000000000..300c0645c --- /dev/null +++ b/build/javascript/package-lock.json @@ -0,0 +1,3225 @@ +{ + "name": "owncast-dependencies", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/runtime": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@fortawesome/fontawesome-common-types": { + "version": "0.2.31", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.31.tgz", + "integrity": "sha512-xfnPyH6NN5r/h1/qDYoGB0BlHSID902UkQzxR8QsoKDh55KAPr8ruAoie12WQEEQT8lRE2wtV7LoUllJ1HqCag==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "1.2.31", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.31.tgz", + "integrity": "sha512-lqUWRK+ylHQJG5Kiez4XrAZAfc7snxCc+X59quL3xPfMnxzfyf1lt+/hD7X1ZL4KlzAH2KFzMuEVrolo/rAkog==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.31" + } + }, + "@fortawesome/free-regular-svg-icons": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.0.tgz", + "integrity": "sha512-kgdP2N5sFjzs5+XFZTsz8KNJjXm/851Gtjh8FcG+gpzN4weN/pd9S1jrf1e+2naWughHUzo7AKAzFB2CodpJ3g==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.31" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.0.tgz", + "integrity": "sha512-4dGRsOnGBPM7c0fd3LuiU6LgRSLn01rw1LJ370yC2iFMLUcLCLLynZhQbMhsiJmMwQM/YmPQblAdyHKVCgsIAA==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.31" + } + }, + "@fullhuman/postcss-purgecss": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-2.3.0.tgz", + "integrity": "sha512-qnKm5dIOyPGJ70kPZ5jiz0I9foVOic0j+cOzNDoo8KoCf6HjicIZ99UfO2OmE7vCYSKAAepEwJtNzpiiZAh9xw==", + "requires": { + "postcss": "7.0.32", + "purgecss": "^2.3.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", + "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "@joeattardi/emoji-button": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@joeattardi/emoji-button/-/emoji-button-4.2.0.tgz", + "integrity": "sha512-naSbg20LEyenfeeUwPMUScwXzQkSM42hrEgLrQCyq01PP8uTZ30gnRByUkg475jUxUuOmf9G4fv0XJdFsNP2tw==", + "requires": { + "@fortawesome/fontawesome-svg-core": "^1.2.28", + "@fortawesome/free-regular-svg-icons": "^5.13.0", + "@fortawesome/free-solid-svg-icons": "^5.13.0", + "@popperjs/core": "^2.4.0", + "focus-trap": "^5.1.0", + "fuzzysort": "^1.1.4", + "tiny-emitter": "^2.1.0", + "tslib": "^2.0.0", + "twemoji": "^13.0.0" + } + }, + "@justinribeiro/lite-youtube": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@justinribeiro/lite-youtube/-/lite-youtube-0.9.0.tgz", + "integrity": "sha512-qh6WCBLr7thO2BSC29oSCbzJCEGM7Pbke/yFBFT4oxZNx/GiukWifaigVkatoqEx03PnQ/T0Iy3X7p9AZPHOEw==" + }, + "@npmcli/move-file": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", + "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + } + } + }, + "@popperjs/core": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.5.3.tgz", + "integrity": "sha512-RFwCobxsvZ6j7twS7dHIZQZituMIDJJNHS/qY6iuthVebxS3zhRY+jaC2roEKiAYaVuTcGmX6Luc6YBcf6zJVg==" + }, + "@rollup/plugin-alias": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-3.1.1.tgz", + "integrity": "sha512-hNcQY4bpBUIvxekd26DBPgF7BT4mKVNDF5tBG4Zi+3IgwLxGYRY0itHs9D0oLVwXM5pvJDWJlBQro+au8WaUWw==", + "dev": true, + "requires": { + "slash": "^3.0.0" + } + }, + "@rollup/plugin-commonjs": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-15.1.0.tgz", + "integrity": "sha512-xCQqz4z/o0h2syQ7d9LskIMvBSH4PX5PjYdpSSvgS+pQik3WahkQVNWg3D8XJeYjZoVWnIUQYDghuEMRGrmQYQ==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "commondir": "^1.0.1", + "estree-walker": "^2.0.1", + "glob": "^7.1.6", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7", + "resolve": "^1.17.0" + } + }, + "@rollup/plugin-inject": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-4.0.2.tgz", + "integrity": "sha512-TSLMA8waJ7Dmgmoc8JfPnwUwVZgLjjIAM6MqeIFqPO2ODK36JqE0Cf2F54UTgCUuW8da93Mvoj75a6KAVWgylw==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.0.4", + "estree-walker": "^1.0.1", + "magic-string": "^0.25.5" + }, + "dependencies": { + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + } + } + }, + "@rollup/plugin-json": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz", + "integrity": "sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.0.8" + } + }, + "@rollup/plugin-node-resolve": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-9.0.0.tgz", + "integrity": "sha512-gPz+utFHLRrd41WMP13Jq5mqqzHL3OXrfj3/MkSyB6UBIcuNt9j60GCbarzMzdf1VHFpOxfQh/ez7wyadLMqkg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.17.0" + } + }, + "@rollup/plugin-replace": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.3.3.tgz", + "integrity": "sha512-XPmVXZ7IlaoWaJLkSCDaa0Y6uVo5XQYHhiMFzOd5qSv5rE+t/UJToPIOE56flKIxBFQI27ONsxb7dqHnwSsjKQ==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.0.8", + "magic-string": "^0.25.5" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + } + } + }, + "@sindresorhus/is": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-3.1.2.tgz", + "integrity": "sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ==", + "dev": true + }, + "@snowpack/plugin-build-script": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@snowpack/plugin-build-script/-/plugin-build-script-2.0.7.tgz", + "integrity": "sha512-f1fZN0Blsfk25oMBf62wvhHm6v7DUG0W85+JFKzYI6SHOIHrYL37o9xaJX7RJpn2kEKCRymWsQo4PclQdnL77w==", + "dev": true, + "requires": { + "execa": "^4.0.3", + "npm-run-path": "^4.0.1" + } + }, + "@snowpack/plugin-run-script": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@snowpack/plugin-run-script/-/plugin-run-script-2.1.3.tgz", + "integrity": "sha512-DhV7KBHnUULtP7eQGHTAGdYsjI1q+vMuX07X8h/v2JnFVaZqowOBjnjQDfbY9oGtw/PZqiY4KEegAh3qxFqODQ==", + "dev": true, + "requires": { + "execa": "^4.0.3", + "npm-run-path": "^4.0.1" + } + }, + "@szmarczak/http-timer": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", + "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@types/cacheable-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", + "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "dev": true, + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==", + "dev": true + }, + "@types/keyv": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", + "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "14.11.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz", + "integrity": "sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@videojs/http-streaming": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.2.0.tgz", + "integrity": "sha512-0LlotccmEt19Z03rI5busKGBvtpYNPHsnLNz0wl6zVEpx8trzYxTQSwKDCUD994hfzLeikQTHJc8BqMrfs1OgA==", + "requires": { + "@babel/runtime": "^7.5.5", + "@videojs/vhs-utils": "^2.2.0", + "aes-decrypter": "3.0.2", + "global": "^4.3.2", + "m3u8-parser": "4.4.3", + "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": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@videojs/themes/-/themes-1.0.0.tgz", + "integrity": "sha512-DbRFpO7O0xPLRL4v7dJCcsfdP8Bpwd7jADzIKh/jpGViCl4u6ECmLquBi2OjGLWrqBv5uXMkKtK7RumrHeEyfw==", + "requires": { + "postcss-inline-svg": "^4.1.0" + } + }, + "@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==", + "requires": { + "@babel/runtime": "^7.5.5", + "global": "^4.3.2", + "url-toolkit": "^2.1.6" + } + }, + "@videojs/xhr": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.5.1.tgz", + "integrity": "sha512-wV9nGESHseSK+S9ePEru2+OJZ1jq/ZbbzniGQ4weAmTIepuBMSYPx5zrxxQA0E786T5ykpO8ts+LayV+3/oI2w==", + "requires": { + "@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": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==" + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" + }, + "address": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", + "integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==", + "dev": true + }, + "aes-decrypter": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.0.2.tgz", + "integrity": "sha512-SBAfPQpGTbHbAR6qSybPjMio+MYuZwdD/a/ltOq6hj53vK94dphAm88rR3FNaZyEa/x3SobYmdT5f46qUwroLQ==", + "requires": { + "@babel/runtime": "^7.5.5", + "@videojs/vhs-utils": "^1.0.0", + "global": "^4.3.2", + "pkcs7": "^1.0.4" + }, + "dependencies": { + "@videojs/vhs-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-1.3.0.tgz", + "integrity": "sha512-oiqXDtHQqDPun7JseWkirUHGrgdYdeF12goUut5z7vwAj4DmUufEPFJ4xK5hYGXGFDyDhk2rSFOR122Ze6qXyQ==", + "requires": { + "@babel/runtime": "^7.5.5", + "global": "^4.3.2", + "url-toolkit": "^2.1.6" + } + } + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "autoprefixer": { + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", + "requires": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browserslist": { + "version": "4.14.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.5.tgz", + "integrity": "sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==", + "requires": { + "caniuse-lite": "^1.0.30001135", + "electron-to-chromium": "^1.3.571", + "escalade": "^3.1.0", + "node-releases": "^1.1.61" + } + }, + "builtin-modules": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", + "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", + "dev": true + }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "cacheable-lookup": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz", + "integrity": "sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==", + "dev": true + }, + "cacheable-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", + "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^2.0.0" + } + }, + "cachedir": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", + "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" + }, + "caniuse-lite": { + "version": "1.0.30001143", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001143.tgz", + "integrity": "sha512-p/PO5YbwmCpBJPxjOiKBvAlUPgF8dExhfEpnsH+ys4N/791WHrYrGg0cyHiAURl5hSbx5vIcjKmQAP6sHDYH3w==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "chokidar": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", + "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", + "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==" + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "css-modules-loader-core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz", + "integrity": "sha1-WQhmgpShvs0mGuCkziGwtVHyHRY=", + "dev": true, + "requires": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.1", + "postcss-modules-extract-imports": "1.1.0", + "postcss-modules-local-by-default": "1.2.0", + "postcss-modules-scope": "1.1.0", + "postcss-modules-values": "1.3.0" + } + }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-selector-tokenizer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" + }, + "css-what": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.1.tgz", + "integrity": "sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g==" + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + } + } + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "defer-to-connect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", + "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==", + "dev": true + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, + "detect-port": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.3.0.tgz", + "integrity": "sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ==", + "dev": true, + "requires": { + "address": "^1.0.1", + "debug": "^2.6.0" + } + }, + "detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "requires": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "electron-to-chromium": { + "version": "1.3.576", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.576.tgz", + "integrity": "sha512-uSEI0XZ//5ic+0NdOqlxp0liCD44ck20OAGyLMSymIWTEAtHKVJi6JM18acOnRgUgX7Q65QqnI+sNncNvIy8ew==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-module-lexer": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.3.25.tgz", + "integrity": "sha512-H9VoFD5H9zEfiOX2LeTWDwMvAbLqcAyA2PIb40TOAvGpScOjit02oTGWgIh+M0rx2eJOKyJVM9wtpKFVgnyC3A==", + "dev": true + }, + "esbuild": { + "version": "0.6.34", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.6.34.tgz", + "integrity": "sha512-InRdL/Q96pUucPqovJzvuLhquZr6jOn81FDVwFjCKz1rYKIm9OdOC+7Fs4vr6x48vKBl5LzKgtjU39BUpO636A==", + "dev": true + }, + "escalade": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz", + "integrity": "sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esinstall": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/esinstall/-/esinstall-0.2.0.tgz", + "integrity": "sha512-jLmMiMlKNpZr4Yr0T+3eNvYP2F2FpkfwPKVBmEFzlvKXoWhpvARUTuqv4TRkG3ZbgrhoPcwD0V2sh92ii8T0AA==", + "dev": true, + "requires": { + "@rollup/plugin-alias": "^3.0.1", + "@rollup/plugin-commonjs": "^15.0.0", + "@rollup/plugin-inject": "^4.0.2", + "@rollup/plugin-json": "^4.0.0", + "@rollup/plugin-node-resolve": "^9.0.0", + "@rollup/plugin-replace": "^2.3.3", + "es-module-lexer": "^0.3.24", + "is-builtin-module": "^3.0.0", + "kleur": "^4.1.1", + "mkdirp": "^1.0.3", + "rimraf": "^3.0.0", + "rollup": "^2.23.0", + "rollup-plugin-node-polyfills": "^0.2.1", + "validate-npm-package-name": "^3.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "estree-walker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.1.tgz", + "integrity": "sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "execa": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", + "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "focus-trap": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-5.1.0.tgz", + "integrity": "sha512-CkB/nrO55069QAUjWFBpX6oc+9V90Qhgpe6fBWApzruMq5gnlh90Oo7iSSDK7pKiV5ugG6OY2AXM5mxcmL3lwQ==", + "requires": { + "tabbable": "^4.0.0", + "xtend": "^4.0.1" + } + }, + "follow-redirects": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "dependencies": { + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + } + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "fuzzysort": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-1.1.4.tgz", + "integrity": "sha512-JzK/lHjVZ6joAg3OnCjylwYXYVjRiwTY6Yb25LvfpJHK8bjisfnZJ5bY8aVWwTwCXgxPNgLAtmHL+Hs5q1ddLQ==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "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" + }, + "dependencies": { + "process": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", + "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" + } + } + }, + "got": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/got/-/got-11.7.0.tgz", + "integrity": "sha512-7en2XwH2MEqOsrK0xaKhbWibBoZqy+f1RSUoIeF1BLcnf+pyQdDsljWMfmOh+QKJwuvDIiKx38GtPh5wFdGGjg==", + "dev": true, + "requires": { + "@sindresorhus/is": "^3.1.1", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "htm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.0.4.tgz", + "integrity": "sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ==" + }, + "html-tags": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", + "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==" + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http2-wrapper": { + "version": "1.0.0-beta.5.2", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz", + "integrity": "sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==", + "dev": true, + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" + }, + "individual": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz", + "integrity": "sha1-gzsJfa0jKU52EXqY+zjg2a1hu5c=" + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-builtin-module": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.0.0.tgz", + "integrity": "sha512-/93sDihsAD652hrMEbJGbMAVBf1qc96kyThHQ0CAOONHaE3aROLpTjDe4WQ5aoC5ITHFxEq1z8XqSU7km+8amw==", + "dev": true, + "requires": { + "builtin-modules": "^3.0.0" + } + }, + "is-docker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "isbinaryfile": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.6.tgz", + "integrity": "sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "jsonfile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz", + "integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^0.1.2" + } + }, + "jsonschema": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.8.tgz", + "integrity": "sha512-HZrVvnv9Ri3WK3t53Anu55eS+IYiQm+UcGE23oBEYi3gD1qODW+I7y4R28q2FyVhzGTDhxAEqTjbe5+jNkqmeQ==", + "dev": true + }, + "keycode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz", + "integrity": "sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ=" + }, + "keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "kleur": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.3.tgz", + "integrity": "sha512-H1tr8QP2PxFTNwAFM74Mui2b6ovcY9FoxJefgrwxY+OCJcq01k5nvhf4M/KnizzrJvLRap5STUy7dgDV35iUBw==", + "dev": true + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, + "m3u8-parser": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.4.3.tgz", + "integrity": "sha512-o6Z2I0lVeL95gE3BHPB5mSACAA6PTubYCFKlbdQmGNH+Ij0A0oEW8N4V5kqTPgYVnuLcb5UdqNiP7V+xEORhYA==", + "requires": { + "@babel/runtime": "^7.5.5", + "@videojs/vhs-utils": "^1.1.0", + "global": "^4.3.2" + }, + "dependencies": { + "@videojs/vhs-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-1.3.0.tgz", + "integrity": "sha512-oiqXDtHQqDPun7JseWkirUHGrgdYdeF12goUut5z7vwAj4DmUufEPFJ4xK5hYGXGFDyDhk2rSFOR122Ze6qXyQ==", + "requires": { + "@babel/runtime": "^7.5.5", + "global": "^4.3.2", + "url-toolkit": "^2.1.6" + } + } + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "mime-db": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", + "dev": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dev": true, + "requires": { + "mime-db": "1.44.0" + }, + "dependencies": { + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true + } + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "requires": { + "dom-walk": "^0.1.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "mpd-parser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.12.0.tgz", + "integrity": "sha512-Ov5Oz9bw5X/G8V/6PlO+rHuqKywYYjQ6USyv8fqFMs413HkrzlpDjgUKSBD7C+/J19ID5mWtxzrpMf4Yp++iZg==", + "requires": { + "@babel/runtime": "^7.5.5", + "@videojs/vhs-utils": "^1.1.0", + "global": "^4.3.2", + "xmldom": "^0.1.27" + }, + "dependencies": { + "@videojs/vhs-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-1.3.0.tgz", + "integrity": "sha512-oiqXDtHQqDPun7JseWkirUHGrgdYdeF12goUut5z7vwAj4DmUufEPFJ4xK5hYGXGFDyDhk2rSFOR122Ze6qXyQ==", + "requires": { + "@babel/runtime": "^7.5.5", + "global": "^4.3.2", + "url-toolkit": "^2.1.6" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "mux.js": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.6.1.tgz", + "integrity": "sha512-iIE3EJURbrPZ9Y4i9ADKTIvxGUcAEBOFhwWUOZGCiKlpXDZrqDgcJLDrOa0PenLhw6WYkOyl18kHFEvwm9JSpg==" + }, + "node-emoji": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", + "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", + "requires": { + "lodash.toarray": "^4.4.0" + } + }, + "node-releases": { + "version": "1.1.61", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.61.tgz", + "integrity": "sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true + }, + "normalize.css": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", + "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + }, + "dependencies": { + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + } + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-hash": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz", + "integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.3.0.tgz", + "integrity": "sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "dependencies": { + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + } + } + }, + "p-cancelable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-queue": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.1.tgz", + "integrity": "sha512-miQiSxLYPYBxGkrldecZC18OTLjdUqnlRebGzPRiVxB8mco7usCmm7hFuxiTvp93K18JnLtE4KMMycjAu/cQQg==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.1.0" + } + }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "requires": { + "@babel/runtime": "^7.5.5" + } + }, + "postcss": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.1.tgz", + "integrity": "sha1-AA29H47vIXqjaLmiEsX8QLKo8/I=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-functions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz", + "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=", + "requires": { + "glob": "^7.1.2", + "object-assign": "^4.1.1", + "postcss": "^6.0.9", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-inline-svg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-inline-svg/-/postcss-inline-svg-4.1.0.tgz", + "integrity": "sha512-0pYBJyoQ9/sJViYRc1cNOOTM7DYh0/rmASB0TBeRmWkG8YFK2tmgdkfjHkbRma1iFtBFKFHZFsHwRTDZTMKzSQ==", + "requires": { + "css-select": "^2.0.2", + "dom-serializer": "^0.1.1", + "htmlparser2": "^3.10.1", + "postcss": "^7.0.17", + "postcss-value-parser": "^4.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "postcss-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-2.0.3.tgz", + "integrity": "sha512-zS59pAk3deu6dVHyrGqmC3oDXBdNdajk4k1RyxeVXCrcEDBUBHoIhE4QTsmhxgzXxsaqFDAkUZfmMa5f/N/79w==", + "requires": { + "camelcase-css": "^2.0.1", + "postcss": "^7.0.18" + }, + "dependencies": { + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "postcss-modules-extract-imports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", + "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "dev": true, + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "dev": true, + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "dev": true, + "requires": { + "icss-replace-symbols": "^1.1.0", + "postcss": "^6.0.1" + } + }, + "postcss-nested": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-4.2.3.tgz", + "integrity": "sha512-rOv0W1HquRCamWy2kFl3QazJMMe1ku6rCFoAAH+9AcxdbpDeBr6k968MLWuLjvjMcGEip01ak09hKOEgpK9hvw==", + "requires": { + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" + }, + "preact": { + "version": "10.5.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.5.3.tgz", + "integrity": "sha512-g9i3d4obUJ24idLW/wuKWATVj6YFjxXDN+POu4u68YrI5dypUOu9Z3lb8/M4kr2dojcwq3H1GSgfpQkFX7y5Zw==" + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "purgecss": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-2.3.0.tgz", + "integrity": "sha512-BE5CROfVGsx2XIhxGuZAT7rTH9lLeQx/6M0P7DTXQH4IUc3BBzs9JUzt4yzGf3JrH9enkeq6YJBe9CTtkm1WmQ==", + "requires": { + "commander": "^5.0.0", + "glob": "^7.0.0", + "postcss": "7.0.32", + "postcss-selector-parser": "^6.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", + "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "reduce-css-calc": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz", + "integrity": "sha512-fDnlZ+AybAS3C7Q9xDq5y8A2z+lT63zLbynew/lur/IR24OQF5x98tfNwf79mzEdfywZ0a2wpM860FhFfMxZlA==", + "requires": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-alpn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz", + "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==", + "dev": true + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + }, + "rollup": { + "version": "2.28.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.28.2.tgz", + "integrity": "sha512-8txbsFBFLmm9Xdt4ByTOGa9Muonmc8MfNjnGAR8U8scJlF1ZW7AgNZa7aqBXaKtlvnYP/ab++fQIq9dB9NWUbg==", + "dev": true, + "requires": { + "fsevents": "~2.1.2" + } + }, + "rollup-plugin-inject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", + "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1", + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + }, + "dependencies": { + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + } + } + }, + "rollup-plugin-node-polyfills": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", + "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", + "dev": true, + "requires": { + "rollup-plugin-inject": "^3.0.0" + } + }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + }, + "dependencies": { + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + } + } + }, + "rust-result": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz", + "integrity": "sha1-NMdbLm3Dn+WHXlveyFteD5FTb3I=", + "requires": { + "individual": "^2.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-json-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz", + "integrity": "sha1-fA9XjPzNEtM6ccDgVBPi7KFx6qw=", + "requires": { + "rust-result": "^1.0.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "showdown": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/showdown/-/showdown-1.9.1.tgz", + "integrity": "sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA==", + "requires": { + "yargs": "^14.2" + }, + "dependencies": { + "yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + } + }, + "yargs-parser": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "snowpack": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/snowpack/-/snowpack-2.12.1.tgz", + "integrity": "sha512-zMg7pOn96maiuMbKMXjMSbwTgq/hnZiWkBVIQL3TDXNCICT3uxSEW0vvPFyPNOg0JNvc0sb08Wvz5TyP5F8AjA==", + "dev": true, + "requires": { + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@snowpack/plugin-build-script": "^2.0.7", + "@snowpack/plugin-run-script": "^2.1.3", + "cacache": "^15.0.0", + "cachedir": "^2.3.0", + "chokidar": "^3.4.0", + "compressible": "^2.0.18", + "cosmiconfig": "^7.0.0", + "css-modules-loader-core": "^1.1.0", + "deepmerge": "^4.2.2", + "detect-port": "^1.3.0", + "es-module-lexer": "^0.3.24", + "esbuild": "^0.6.28", + "esinstall": "^0.2.0", + "etag": "^1.8.1", + "execa": "^4.0.3", + "find-cache-dir": "^3.3.1", + "find-up": "^4.1.0", + "glob": "^7.1.4", + "got": "^11.1.4", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.6", + "jsonschema": "^1.2.5", + "kleur": "^4.1.1", + "mime-types": "^2.1.26", + "mkdirp": "^1.0.3", + "npm-run-path": "^4.0.1", + "open": "^7.0.4", + "p-queue": "^6.6.1", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.3", + "strip-ansi": "^6.0.0", + "strip-comments": "^2.0.1", + "tar": "^6.0.1", + "validate-npm-package-name": "^3.0.0", + "ws": "^7.3.0", + "yargs-parser": "^18.1.3" + }, + "dependencies": { + "cacache": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.5.tgz", + "integrity": "sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==", + "dev": true, + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "ssri": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", + "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + } + } + }, + "strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "tabbable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz", + "integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==" + }, + "tailwindcss": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.8.10.tgz", + "integrity": "sha512-7QkERG/cWCzsuMqHMwjOaLMVixOGLNBiXsrkssxlE1aWfkxVbGqiuMokR2162xRyaH2mBIHKxmlf1qb3DvIPqw==", + "requires": { + "@fullhuman/postcss-purgecss": "^2.1.2", + "autoprefixer": "^9.4.5", + "browserslist": "^4.12.0", + "bytes": "^3.0.0", + "chalk": "^3.0.0 || ^4.0.0", + "color": "^3.1.2", + "detective": "^5.2.0", + "fs-extra": "^8.0.0", + "html-tags": "^3.1.0", + "lodash": "^4.17.20", + "node-emoji": "^1.8.1", + "normalize.css": "^8.0.1", + "object-hash": "^2.0.3", + "postcss": "^7.0.11", + "postcss-functions": "^3.0.0", + "postcss-js": "^2.0.0", + "postcss-nested": "^4.1.1", + "postcss-selector-parser": "^6.0.0", + "postcss-value-parser": "^4.1.0", + "pretty-hrtime": "^1.0.3", + "reduce-css-calc": "^2.1.6", + "resolve": "^1.14.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "tar": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", + "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + }, + "twemoji": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/twemoji/-/twemoji-13.0.1.tgz", + "integrity": "sha512-mrTBq+XpCLM4zm76NJOjLHoQNV9mHdBt3Cba/T5lS1rxn8ArwpqE47mqTocupNlkvcLxoeZJjYSUW0DU5ZwqZg==", + "requires": { + "fs-extra": "^8.0.1", + "jsonfile": "^5.0.0", + "twemoji-parser": "13.0.0", + "universalify": "^0.1.2" + } + }, + "twemoji-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-13.0.0.tgz", + "integrity": "sha512-zMaGdskpH8yKjT2RSE/HwE340R4Fm+fbie4AaqjDa4H/l07YUmAvxkSfNl6awVWNRRQ0zdzLQ8SAJZuY5MgstQ==" + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "url-toolkit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.0.tgz", + "integrity": "sha512-Rde0c9S4fJK3FaHim3DSgdQ8IFrSXcZCpAJo9T7/FA+BoQGhK0ow3mpwGQLJCPYsNn6TstpW7/7DzMpSpz9F9w==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "dev": true, + "requires": { + "builtins": "^1.0.3" + } + }, + "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==", + "requires": { + "@babel/runtime": "^7.9.2", + "@videojs/http-streaming": "1.13.4", + "@videojs/xhr": "2.5.1", + "global": "4.3.2", + "keycode": "^2.2.0", + "safe-json-parse": "4.0.0", + "videojs-font": "3.2.0", + "videojs-vtt.js": "^0.15.2" + }, + "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==", + "requires": { + "aes-decrypter": "3.0.2", + "global": "^4.3.0", + "m3u8-parser": "4.4.0", + "mpd-parser": "0.10.0", + "mux.js": "5.6.1", + "url-toolkit": "^2.1.3", + "video.js": "^6.8.0 || ^7.0.0" + } + }, + "@videojs/vhs-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-1.3.0.tgz", + "integrity": "sha512-oiqXDtHQqDPun7JseWkirUHGrgdYdeF12goUut5z7vwAj4DmUufEPFJ4xK5hYGXGFDyDhk2rSFOR122Ze6qXyQ==", + "requires": { + "@babel/runtime": "^7.5.5", + "global": "^4.3.2", + "url-toolkit": "^2.1.6" + } + }, + "m3u8-parser": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.4.0.tgz", + "integrity": "sha512-iH2AygTFILtato+XAgnoPYzLHM4R3DjATj7Ozbk7EHdB2XoLF2oyOUguM7Kc4UVHbQHHL/QPaw98r7PbWzG0gg==", + "requires": { + "global": "^4.3.2" + } + }, + "mpd-parser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.10.0.tgz", + "integrity": "sha512-eIqkH/2osPr7tIIjhRmDWqm2wdJ7Q8oPfWvdjealzsLV2D2oNe0a0ae2gyYYs1sw5e5hdssDA2V6Sz8MW+Uvvw==", + "requires": { + "@babel/runtime": "^7.5.5", + "@videojs/vhs-utils": "^1.1.0", + "global": "^4.3.2", + "xmldom": "^0.1.27" + } + } + } + }, + "videojs-font": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz", + "integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==" + }, + "videojs-vtt.js": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.2.tgz", + "integrity": "sha512-kEo4hNMvu+6KhPvVYPKwESruwhHC3oFis133LwhXHO9U7nRnx0RiJYMiqbgwjgazDEXHR6t8oGJiHM6wq5XlAw==", + "requires": { + "global": "^4.3.1" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "dev": true + }, + "xmldom": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz", + "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", + "dev": true + } + } +} diff --git a/build/javascript/package.json b/build/javascript/package.json new file mode 100644 index 000000000..ec2709956 --- /dev/null +++ b/build/javascript/package.json @@ -0,0 +1,40 @@ +{ + "name": "owncast-dependencies", + "version": "1.0.0", + "description": "", + "main": "index.js", + "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", + "showdown": "^1.9.1", + "tailwindcss": "^1.8.10", + "video.js": "^7.9.6" + }, + "devDependencies": { + "snowpack": "^2.12.1" + }, + "snowpack": { + "install": [ + "video.js/dist/video.min.js", + "@videojs/themes/fantasy/*", + "@videojs/http-streaming/dist/videojs-http-streaming.min.js", + "video.js/dist/video-js.min.css", + "@joeattardi/emoji-button", + "@justinribeiro/lite-youtube", + "htm", + "preact", + "showdown", + "tailwindcss/dist/tailwind.min.css" + ] + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "npm install && npx snowpack install && cp -R web_modules ../../webroot/js" + }, + "author": "", + "license": "ISC" +} diff --git a/webroot/index-standalone-chat.html b/webroot/index-standalone-chat.html index 47586a42a..c9bc5acdc 100644 --- a/webroot/index-standalone-chat.html +++ b/webroot/index-standalone-chat.html @@ -3,20 +3,19 @@ - + - - - -
- + + - - @@ -21,7 +17,10 @@
diff --git a/webroot/index.html b/webroot/index.html index c3eda8c78..60d7a55ea 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -24,34 +24,25 @@ - + - - - - - - - - - + + - - - - - - +
diff --git a/webroot/js/app-standalone-chat.js b/webroot/js/app-standalone-chat.js index ffc64bf9b..b57b32255 100644 --- a/webroot/js/app-standalone-chat.js +++ b/webroot/js/app-standalone-chat.js @@ -1,5 +1,5 @@ -import { h, Component } from 'https://unpkg.com/preact?module'; -import htm from 'https://unpkg.com/htm?module'; +import { h, Component } from '/js/web_modules/preact.js'; +import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); import Chat from './components/chat/chat.js'; diff --git a/webroot/js/app-video-only.js b/webroot/js/app-video-only.js index c45734ffe..8fd5e59b8 100644 --- a/webroot/js/app-video-only.js +++ b/webroot/js/app-video-only.js @@ -1,5 +1,5 @@ -import { h, Component } from 'https://unpkg.com/preact?module'; -import htm from 'https://unpkg.com/htm?module'; +import { h, Component } from '/js/web_modules/preact.js'; +import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); import { OwncastPlayer } from './components/player.js'; diff --git a/webroot/js/app.js b/webroot/js/app.js index 9fe34ca40..f81f1165f 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -1,5 +1,5 @@ -import { h, Component } from 'https://unpkg.com/preact?module'; -import htm from 'https://unpkg.com/htm?module'; +import { h, Component } from '/js/web_modules/preact.js'; +import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); import { OwncastPlayer } from './components/player.js'; diff --git a/webroot/js/components/chat/chat-input.js b/webroot/js/components/chat/chat-input.js index 5b9725205..d4a21bf96 100644 --- a/webroot/js/components/chat/chat-input.js +++ b/webroot/js/components/chat/chat-input.js @@ -1,8 +1,9 @@ -import { h, Component, createRef } from 'https://unpkg.com/preact?module'; -import htm from 'https://unpkg.com/htm?module'; +import { h, Component, createRef } from '/js/web_modules/preact.js'; +import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); -import { EmojiButton } from 'https://unpkg.com/@joeattardi/emoji-button@4.2.0/dist/index.js'; +import { EmojiButton } from '/js/web_modules/@joeattardi/emoji-button.js'; + import ContentEditable, { replaceCaret } from './content-editable.js'; import { generatePlaceholderText, getCaretPosition, convertToText, convertOnPaste } from '../../utils/chat.js'; import { getLocalStorage, setLocalStorage, classNames } from '../../utils/helpers.js'; diff --git a/webroot/js/components/chat/chat.js b/webroot/js/components/chat/chat.js index 4d2bb274d..c88a80947 100644 --- a/webroot/js/components/chat/chat.js +++ b/webroot/js/components/chat/chat.js @@ -1,5 +1,5 @@ -import { h, Component, createRef } from 'https://unpkg.com/preact?module'; -import htm from 'https://unpkg.com/htm?module'; +import { h, Component, createRef } from '/js/web_modules/preact.js'; +import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); import Message from './message.js'; diff --git a/webroot/js/components/chat/content-editable.js b/webroot/js/components/chat/content-editable.js index b8252c195..fa81bea00 100644 --- a/webroot/js/components/chat/content-editable.js +++ b/webroot/js/components/chat/content-editable.js @@ -6,7 +6,7 @@ and here: https://stackoverflow.com/questions/22677931/react-js-onchange-event-for-contenteditable/27255103#27255103 */ -import { Component, createRef, h } from 'https://unpkg.com/preact?module'; +import { h, Component, createRef } from '/js/web_modules/preact.js'; export function replaceCaret(el) { // Place the caret at the end of the element diff --git a/webroot/js/components/chat/message.js b/webroot/js/components/chat/message.js index c57515088..f7e69434e 100644 --- a/webroot/js/components/chat/message.js +++ b/webroot/js/components/chat/message.js @@ -1,5 +1,5 @@ -import { h, Component } from 'https://unpkg.com/preact?module'; -import htm from 'https://unpkg.com/htm?module'; +import { h, Component } from '/js/web_modules/preact.js'; +import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); import { messageBubbleColorForString } from '../../utils/user-colors.js'; diff --git a/webroot/js/components/chat/username.js b/webroot/js/components/chat/username.js index 6431a81bf..03c972e5b 100644 --- a/webroot/js/components/chat/username.js +++ b/webroot/js/components/chat/username.js @@ -1,5 +1,5 @@ -import { h, Component, createRef } from 'https://unpkg.com/preact?module'; -import htm from 'https://unpkg.com/htm?module'; +import { h, Component, createRef } from '/js/web_modules/preact.js'; +import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); import { generateAvatar, setLocalStorage } from '../../utils/helpers.js'; diff --git a/webroot/js/components/player.js b/webroot/js/components/player.js index ca33bffb5..108b17293 100644 --- a/webroot/js/components/player.js +++ b/webroot/js/components/player.js @@ -1,5 +1,7 @@ // https://docs.videojs.com/player +import videojs from '/js/web_modules/videojs/dist/video.min.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`; @@ -52,13 +54,14 @@ class OwncastPlayer { } init() { - videojs.Vhs.xhr.beforeRequest = function (options) { + this.vjsPlayer = videojs(VIDEO_ID, VIDEO_OPTIONS); + + this.vjsPlayer.beforeRequest = function (options) { const cachebuster = Math.round(new Date().getTime() / 1000); options.uri = `${options.uri}?cachebust=${cachebuster}`; return options; }; - this.vjsPlayer = videojs(VIDEO_ID, VIDEO_OPTIONS); this.addAirplay(); this.vjsPlayer.ready(this.handleReady); } @@ -75,10 +78,10 @@ class OwncastPlayer { // play startPlayer() { this.log('Start playing'); - const source = { ...VIDEO_SRC } + const source = { ...VIDEO_SRC }; this.vjsPlayer.src(source); // this.vjsPlayer.play(); - }; + } handleReady() { this.log('on Ready'); @@ -117,7 +120,7 @@ class OwncastPlayer { setPoster() { const cachebuster = Math.round(new Date().getTime() / 1000); - const poster = POSTER_THUMB + "?okhi=" + cachebuster; + const poster = POSTER_THUMB + '?okhi=' + cachebuster; this.vjsPlayer.poster(poster); } @@ -131,7 +134,6 @@ class OwncastPlayer { if (window.WebKitPlaybackTargetAvailabilityEvent) { var videoJsButtonClass = videojs.getComponent('Button'); var concreteButtonClass = videojs.extend(videoJsButtonClass, { - // The `init()` method will also work for constructor logic here, but it is // deprecated. If you provide an `init()` method, it will override the // `constructor()` method! @@ -145,8 +147,10 @@ class OwncastPlayer { }, }); - var concreteButtonInstance = this.vjsPlayer.controlBar.addChild(new concreteButtonClass()); - concreteButtonInstance.addClass("vjs-airplay"); + var concreteButtonInstance = this.vjsPlayer.controlBar.addChild( + new concreteButtonClass() + ); + concreteButtonInstance.addClass('vjs-airplay'); } }); } diff --git a/webroot/js/components/social.js b/webroot/js/components/social.js index 20d69f1f7..5c708e999 100644 --- a/webroot/js/components/social.js +++ b/webroot/js/components/social.js @@ -1,5 +1,5 @@ -import { h } from 'https://unpkg.com/preact?module'; -import htm from 'https://unpkg.com/htm?module'; +import { h } from '/js/web_modules/preact.js'; +import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); import { SOCIAL_PLATFORMS } from '../utils/social.js'; import { classNames } from '../utils/helpers.js'; diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js index f63890ac6..32e3763b5 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -4,6 +4,7 @@ import { CHAT_PLACEHOLDER_OFFLINE, } from './constants.js'; +import showdown from '/js/web_modules/showdown.js'; export function formatMessageText(message, username) { showdown.setFlavor('github'); let formattedText = new showdown.Converter({ diff --git a/webroot/js/web_modules/@joeattardi/emoji-button.js b/webroot/js/web_modules/@joeattardi/emoji-button.js new file mode 100644 index 000000000..c71a25309 --- /dev/null +++ b/webroot/js/web_modules/@joeattardi/emoji-button.js @@ -0,0 +1,3 @@ +!function(e,o){void 0===o&&(o={});var n=o.insertAt;if(e&&"undefined"!=typeof document){var i=document.head||document.getElementsByTagName("head")[0],a=document.createElement("style");a.type="text/css","top"===n&&i.firstChild?i.insertBefore(a,i.firstChild):i.appendChild(a),a.styleSheet?a.styleSheet.cssText=e:a.appendChild(document.createTextNode(e));}}('@keyframes show {\n 0% {\n opacity: 0;\n transform: scale3d(0.8, 0.8, 0.8);\n }\n\n 50% {\n transform: scale3d(1.05, 1.05, 1.05);\n }\n\n 100% {\n transform: scale3d(1, 1, 1);\n }\n}\n\n@keyframes hide {\n 0% {\n opacity: 1;\n transform: scale3d(1, 1, 1);\n }\n\n 100% {\n opacity: 0;\n transform: scale3d(0.8, 0.8, 0.8);\n }\n}\n\n@keyframes grow {\n 0% {\n opacity: 0;\n transform: scale3d(0.8, 0.8, 0.8); \n }\n\n 100% { \n opacity: 1;\n transform: scale3d(1, 1, 1); \n }\n}\n\n@keyframes shrink {\n 0% { \n opacity: 1;\n transform: scale3d(1, 1, 1);\n }\n\n 100% { \n opacity: 0;\n transform: scale3d(0.8, 0.8, 0.8); \n }\n}\n\n@keyframes fade-in {\n 0% { opacity: 0; }\n 100% { opacity: 1; }\n}\n\n@keyframes fade-out {\n 0% { opacity: 1; }\n 100% { opacity: 0; }\n}\n\n.emoji-picker {\n --animation-duration: 0.2s;\n --animation-easing: ease-in-out;\n\n --emoji-size: 1.8em;\n --emoji-size-multiplier: 1.5;\n --emoji-preview-size: 2em;\n --emoji-per-row: 8;\n --row-count: 6;\n\n --content-height: calc((var(--emoji-size) * var(--emoji-size-multiplier)) * var(--row-count) + var(--category-name-size) + var(--category-button-height) + 0.5em);\n\n --category-name-size: 0.85em;\n\n --category-button-height: 2em;\n --category-button-size: 1.1em;\n --category-border-bottom-size: 4px;\n\n --focus-indicator-color: #999999;\n\n --search-height: 2em;\n\n --blue-color: #4F81E5;\n\n --border-color: #CCCCCC;\n --background-color: #FFFFFF;\n --text-color: #000000;\n --secondary-text-color: #666666;\n --hover-color: #E8F4F9;\n --search-focus-border-color: var(--blue-color);\n --search-icon-color: #CCCCCC;\n --overlay-background-color: rgba(0, 0, 0, 0.8);\n --popup-background-color: #FFFFFF;\n --category-button-color: #666666;\n --category-button-active-color: var(--blue-color);\n\n --dark-border-color: #666666;\n --dark-background-color: #333333;\n --dark-text-color: #FFFFFF;\n --dark-secondary-text-color: #999999;\n --dark-hover-color: #666666;\n --dark-search-background-color: #666666;\n --dark-search-border-color: #999999;\n --dark-search-placeholder-color: #999999;\n --dark-search-focus-border-color: #DBE5F9;\n --dark-popup-background-color: #333333;\n --dark-category-button-color: #FFFFFF;\n}\n\n.emoji-picker {\n font-size: 16px;\n\n border: 1px solid var(--border-color);\n border-radius: 5px;\n background: var(--background-color);\n width: calc(var(--emoji-per-row) * var(--emoji-size) * var(--emoji-size-multiplier) + 1em + 1.5rem);\n font-family: Arial, Helvetica, sans-serif;\n overflow: hidden;\n animation: show var(--animation-duration) var(--animation-easing);\n}\n\n.emoji-picker h2 {\n font-family: Arial, Helvetica, sans-serif;\n}\n\n.emoji-picker__overlay {\n background: rgba(0, 0, 0, 0.75);\n z-index: 1000;\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.emoji-picker.hiding {\n animation: hide var(--animation-duration) var(--animation-easing);\n}\n\n.emoji-picker.dark {\n background: var(--dark-background-color);\n color: var(--dark-text-color);\n border-color: var(--dark-border-color);\n}\n\n.emoji-picker__content {\n padding: 0.5em;\n height: var(--content-height);\n position: relative;\n}\n\n.emoji-picker__preview {\n height: var(--emoji-preview-size);\n padding: 0.5em;\n border-top: 1px solid var(--border-color);\n display: flex;\n flex-direction: row;\n align-items: center;\n}\n\n.emoji-picker.dark .emoji-picker__preview {\n border-top-color: var(--dark-border-color);\n}\n\n.emoji-picker__preview-emoji {\n font-size: var(--emoji-preview-size);\n margin-right: 0.25em;\n font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "EmojiOne Color", "Android Emoji";\n}\n\n.emoji-picker__preview-emoji img.emoji {\n height: 1em;\n width: 1em;\n margin: 0 .05em 0 .1em;\n vertical-align: -0.1em;\n}\n\n.emoji-picker__preview-name {\n color: var(--text-color);\n font-size: 0.85em;\n overflow-wrap: break-word;\n word-break: break-all;\n}\n\n.emoji-picker.dark .emoji-picker__preview-name {\n color: var(--dark-text-color);\n}\n\n.emoji-picker__container {\n display: grid;\n justify-content: center;\n grid-template-columns: repeat(var(--emoji-per-row), calc(var(--emoji-size) * var(--emoji-size-multiplier)));\n grid-auto-rows: calc(var(--emoji-size) * var(--emoji-size-multiplier));\n}\n\n.emoji-picker__container.search-results {\n height: var(--content-height);\n overflow-y: auto;\n}\n\n.emoji-picker__custom-emoji {\n width: 1em;\n height: 1em;\n}\n\n.emoji-picker__emoji {\n background: transparent;\n border: none;\n cursor: pointer;\n overflow: hidden;\n font-size: var(--emoji-size);\n width: 1.5em;\n height: 1.5em;\n padding: 0;\n margin: 0;\n outline: none;\n font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "EmojiOne Color", "Android Emoji";\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.emoji-picker__emoji img.emoji {\n height: 1em;\n width: 1em;\n margin: 0 .05em 0 .1em;\n vertical-align: -0.1em;\n}\n\n.emoji-picker__emoji:focus, .emoji-picker__emoji:hover {\n background: var(--hover-color);\n}\n\n.emoji-picker__emoji:focus {\n outline: 1px dotted var(--focus-indicator-color);\n}\n\n.emoji-picker.dark .emoji-picker__emoji:focus, .emoji-picker.dark .emoji-picker__emoji:hover {\n background: var(--dark-hover-color);\n}\n\n.emoji-picker__plugin-container {\n margin: 0.5em;\n display: flex;\n flex-direction: row;\n}\n\n.emoji-picker__search-container {\n margin: 0.5em;\n position: relative;\n height: var(--search-height);\n display: flex;\n}\n\n.emoji-picker__search {\n box-sizing: border-box;\n width: 100%;\n border-radius: 3px;\n border: 1px solid var(--border-color);\n padding-right: 2em;\n padding: 0.5em 2.25em 0.5em 0.5em;\n font-size: 0.85em;\n outline: none;\n}\n\n.emoji-picker.dark .emoji-picker__search {\n background: var(--dark-search-background-color);\n color: var(--dark-text-color);\n border-color: var(--dark-search-border-color);\n}\n\n.emoji-picker.dark .emoji-picker__search::placeholder {\n color: var(--dark-search-placeholder-color);\n}\n\n.emoji-picker__search:focus {\n border: 1px solid var(--search-focus-border-color);\n}\n\n.emoji-picker.dark .emoji-picker__search:focus {\n border-color: var(--dark-search-focus-border-color);\n}\n\n.emoji-picker__search-icon {\n position: absolute;\n color: var(--search-icon-color);\n width: 1em;\n height: 1em;\n right: 0.75em;\n top: calc(50% - 0.5em);\n}\n\n.emoji-picker__search-icon img {\n width: 1em;\n height: 1em;\n}\n\n.emoji-picker__search-not-found {\n color: var(--secondary-text-color);\n text-align: center;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.emoji-picker__search-not-found h2 {\n color: var(--secondary-text-color);\n}\n\n.emoji-picker.dark .emoji-picker__search-not-found {\n color: var(--dark-secondary-text-color);\n}\n\n.emoji-picker.dark .emoji-picker__search-not-found h2 {\n color: var(--dark-secondary-text-color);\n}\n\n.emoji-picker__search-not-found-icon {\n font-size: 3em;\n}\n\n.emoji-picker__search-not-found-icon img {\n width: 1em;\n height: 1em;\n}\n\n.emoji-picker__search-not-found h2 {\n margin: 0.5em 0;\n font-size: 1em;\n}\n\n.emoji-picker__variant-overlay {\n background: var(--overlay-background-color);\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border-radius: 5px;\n display: flex;\n flex-direction: column;\n justify-content: center;\n animation: fade-in var(--animation-duration) var(--animation-easing);\n}\n\n.emoji-picker__variant-overlay.hiding {\n animation: fade-out var(--animation-duration) var(--animation-easing);\n}\n\n.emoji-picker__variant-popup {\n background: var(--popup-background-color);\n margin: 0.5em;\n padding: 0.5em;\n text-align: center;\n border-radius: 5px;\n animation: grow var(--animation-duration) var(--animation-easing);\n user-select: none;\n}\n\n.emoji-picker__variant-overlay.hiding .emoji-picker__variant-popup {\n animation: shrink var(--animation-duration) var(--animation-easing);\n}\n\n.emoji-picker.dark .emoji-picker__variant-popup {\n background: var(--dark-popup-background-color);\n}\n\n.emoji-picker__emojis {\n overflow-y: auto;\n position: relative;\n height: calc((var(--emoji-size) * var(--emoji-size-multiplier)) * var(--row-count) + var(--category-name-size));\n}\n\n.emoji-picker__emojis.hiding {\n animation: fade-out 0.05s var(--animation-easing);\n}\n\n.emoji-picker__emojis h2.emoji-picker__category-name {\n font-size: 0.85em;\n color: var(--secondary-text-color);\n text-transform: uppercase;\n margin: 0.25em 0;\n text-align: left;\n}\n\n.emoji-picker.dark h2.emoji-picker__category-name {\n color: var(--dark-secondary-text-color);\n}\n\n.emoji-picker__category-buttons {\n display: flex;\n flex-direction: row;\n justify-content: space-around;\n height: var(--category-button-height);\n margin-bottom: 0.5em;\n}\n\nbutton.emoji-picker__category-button {\n flex-grow: 1;\n background: transparent;\n padding: 0;\n border: none;\n cursor: pointer;\n font-size: var(--category-button-size);\n vertical-align: middle;\n color: var(--category-button-color);\n border-bottom: var(--category-border-bottom-size) solid transparent;\n outline: none;\n}\n\nbutton.emoji-picker__category-button img {\n width: var(--category-button-size);\n height: var(--category-button-size);\n}\n\n.emoji-picker.keyboard button.emoji-picker__category-button:focus {\n outline: 1px dotted var(--focus-indicator-color);\n}\n\n.emoji-picker.dark button.emoji-picker__category-button.active {\n color: var(--category-button-active-color);\n}\n\n.emoji-picker.dark button.emoji-picker__category-button {\n color: var(--dark-category-button-color);\n}\n\nbutton.emoji-picker__category-button.active {\n color: var(--category-button-active-color);\n border-bottom: var(--category-border-bottom-size) solid var(--category-button-active-color);\n}\n\n@media (prefers-color-scheme: dark) {\n .emoji-picker.auto {\n background: var(--dark-background-color);\n color: var(--dark-text-color);\n border-color: var(--dark-border-color);\n }\n\n .emoji-picker.auto .emoji-picker__preview {\n border-top-color: var(--dark-border-color);\n }\n\n .emoji-picker.auto .emoji-picker__preview-name {\n color: var(--dark-text-color);\n }\n\n .emoji-picker.auto button.emoji-picker__category-button {\n color: var(--dark-category-button-color);\n }\n\n .emoji-picker.auto button.emoji-picker__category-button.active {\n color: var(--category-button-active-color);\n }\n\n .emoji-picker.auto .emoji-picker__emoji:focus, .emoji-picker.auto .emoji-picker__emoji:hover {\n background: var(--dark-hover-color);\n }\n\n .emoji-picker.auto .emoji-picker__search {\n background: var(--dark-search-background-color);\n color: var(--dark-text-color);\n border-color: var(--dark-search-border-color);\n }\n \n .emoji-picker.auto h2.emoji-picker__category-name {\n color: var(--dark-secondary-text-color);\n }\n\n .emoji-picker.auto .emoji-picker__search::placeholder {\n color: var(--dark-search-placeholder-color);\n }\n\n .emoji-picker.auto .emoji-picker__search:focus {\n border-color: var(--dark-search-focus-border-color);\n }\n\n .emoji-picker.auto .emoji-picker__search-not-found {\n color: var(--dark-secondary-text-color);\n }\n\n .emoji-picker.auto .emoji-picker__search-not-found h2 {\n color: var(--dark-secondary-text-color);\n }\n\n .emoji-picker.auto .emoji-picker__variant-popup {\n background: var(--dark-popup-background-color);\n }\n}');var e=["input","select","textarea","a[href]","button","[tabindex]","audio[controls]","video[controls]",'[contenteditable]:not([contenteditable="false"])'],o=e.join(","),n="undefined"==typeof Element?function(){}:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector;function i(e,i){i=i||{};var r,t,c,d=[],g=[],u=e.querySelectorAll(o);for(i.includeContainer&&n.call(e,o)&&(u=Array.prototype.slice.apply(u)).unshift(e),r=0;r0){var o=v[v.length-1];o!==e&&o.pause();}var n=v.indexOf(e);-1===n||v.splice(n,1),v.push(e);},deactivateTrap:function(e){var o=v.indexOf(e);-1!==o&&v.splice(o,1),v.length>0&&v[v.length-1].unpause();}});function y(e){return setTimeout(e,0)}var j=function(e,o){var n=document,i="string"==typeof e?n.querySelector(e):e,a=u({returnFocusOnDeactivate:!0,escapeDeactivates:!0},o),r={firstTabbableNode:null,lastTabbableNode:null,nodeFocusedBeforeActivation:null,mostRecentlyFocusedNode:null,active:!1,paused:!1},t={activate:function(e){if(r.active)return;w(),r.active=!0,r.paused=!1,r.nodeFocusedBeforeActivation=n.activeElement;var o=e&&e.onActivate?e.onActivate:a.onActivate;o&&o();return m(),t},deactivate:s,pause:function(){if(r.paused||!r.active)return;r.paused=!0,c();},unpause:function(){if(!r.paused||!r.active)return;r.paused=!1,w(),m();}};return t;function s(e){if(r.active){clearTimeout(d),c(),r.active=!1,r.paused=!1,f.deactivateTrap(t);var o=e&&void 0!==e.onDeactivate?e.onDeactivate:a.onDeactivate;return o&&o(),(e&&void 0!==e.returnFocus?e.returnFocus:a.returnFocusOnDeactivate)&&y((function(){var e;k((e=r.nodeFocusedBeforeActivation,l("setReturnFocus")||e));})),t}}function m(){if(r.active)return f.activateTrap(t),d=y((function(){k(v());})),n.addEventListener("focusin",h,!0),n.addEventListener("mousedown",j,{capture:!0,passive:!1}),n.addEventListener("touchstart",j,{capture:!0,passive:!1}),n.addEventListener("click",b,{capture:!0,passive:!1}),n.addEventListener("keydown",p,{capture:!0,passive:!1}),t}function c(){if(r.active)return n.removeEventListener("focusin",h,!0),n.removeEventListener("mousedown",j,!0),n.removeEventListener("touchstart",j,!0),n.removeEventListener("click",b,!0),n.removeEventListener("keydown",p,!0),t}function l(e){var o=a[e],i=o;if(!o)return null;if("string"==typeof o&&!(i=n.querySelector(o)))throw new Error("`"+e+"` refers to no known node");if("function"==typeof o&&!(i=o()))throw new Error("`"+e+"` did not return a node");return i}function v(){var e;if(!(e=null!==l("initialFocus")?l("initialFocus"):i.contains(n.activeElement)?n.activeElement:r.firstTabbableNode||l("fallbackFocus")))throw new Error("Your focus-trap needs to have at least one focusable element");return e}function j(e){i.contains(e.target)||(a.clickOutsideDeactivates?s({returnFocus:!g.isFocusable(e.target)}):a.allowOutsideClick&&a.allowOutsideClick(e)||e.preventDefault());}function h(e){i.contains(e.target)||e.target instanceof Document||(e.stopImmediatePropagation(),k(r.mostRecentlyFocusedNode||v()));}function p(e){if(!1!==a.escapeDeactivates&&function(e){return "Escape"===e.key||"Esc"===e.key||27===e.keyCode}(e))return e.preventDefault(),void s();(function(e){return "Tab"===e.key||9===e.keyCode})(e)&&function(e){if(w(),e.shiftKey&&e.target===r.firstTabbableNode)return e.preventDefault(),void k(r.lastTabbableNode);if(!e.shiftKey&&e.target===r.lastTabbableNode)e.preventDefault(),k(r.firstTabbableNode);}(e);}function b(e){a.clickOutsideDeactivates||i.contains(e.target)||a.allowOutsideClick&&a.allowOutsideClick(e)||(e.preventDefault(),e.stopImmediatePropagation());}function w(){var e=g(i);r.firstTabbableNode=e[0]||v(),r.lastTabbableNode=e[e.length-1]||v();}function k(e){e!==n.activeElement&&(e&&e.focus?(e.focus(),r.mostRecentlyFocusedNode=e,function(e){return e.tagName&&"input"===e.tagName.toLowerCase()&&"function"==typeof e.select}(e)&&e.select()):k(v()));}};function h(){}h.prototype={on:function(e,o,n){var i=this.e||(this.e={});return (i[e]||(i[e]=[])).push({fn:o,ctx:n}),this},once:function(e,o,n){var i=this;function a(){i.off(e,a),o.apply(n,arguments);}return a._=o,this.on(e,a,n)},emit:function(e){for(var o=[].slice.call(arguments,1),n=((this.e||(this.e={}))[e]||[]).slice(),i=0,a=n.length;i=0?o.ownerDocument.body:C(o)&&S(o)?o:e(M(o))}(e),i="body"===E(n),a=w(n),r=i?[a].concat(a.visualViewport||[],S(n)?n:[]):n,t=o.concat(r);return i?t:t.concat(P(M(r)))}function T(e){return ["table","td","th"].indexOf(E(e))>=0}function L(e){return C(e)&&"fixed"!==I(e).position?e.offsetParent:null}function N(e){for(var o=w(e),n=L(e);n&&T(n);)n=L(n);return n&&"body"===E(n)&&"static"===I(n).position?o:n||o}h.TinyEmitter=p;var B="top",F="bottom",D="right",q="left",R=[B,F,D,q],V=R.reduce((function(e,o){return e.concat([o+"-start",o+"-end"])}),[]),H=[].concat(R,["auto"]).reduce((function(e,o){return e.concat([o,o+"-start",o+"-end"])}),[]),U=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function W(e){var o=new Map,n=new Set,i=[];return e.forEach((function(e){o.set(e.name,e);})),e.forEach((function(e){n.has(e.name)||function e(a){n.add(a.name),[].concat(a.requires||[],a.requiresIfExists||[]).forEach((function(i){if(!n.has(i)){var a=o.get(i);a&&e(a);}})),i.push(a);}(e);})),i}function K(e){return e.split("-")[0]}var J={placement:"bottom",modifiers:[],strategy:"absolute"};function G(){for(var e=arguments.length,o=new Array(e),n=0;n=0?"x":"y"}function Q(e){var o,n=e.reference,i=e.element,a=e.placement,r=a?K(a):null,t=a?$(a):null,s=n.x+n.width/2-i.width/2,m=n.y+n.height/2-i.height/2;switch(r){case B:o={x:s,y:n.y-i.height};break;case F:o={x:s,y:n.y+n.height};break;case D:o={x:n.x+n.width,y:m};break;case q:o={x:n.x-i.width,y:m};break;default:o={x:n.x,y:n.y};}var c=r?Z(r):null;if(null!=c){var d="y"===c?"height":"width";switch(t){case"start":o[c]=Math.floor(o[c])-Math.floor(n[d]/2-i[d]/2);break;case"end":o[c]=Math.floor(o[c])+Math.ceil(n[d]/2-i[d]/2);}}return o}var ee={top:"auto",right:"auto",bottom:"auto",left:"auto"};function oe(e){var o,n=e.popper,i=e.popperRect,a=e.placement,r=e.offsets,t=e.position,s=e.gpuAcceleration,m=e.adaptive,c=function(e){var o=e.x,n=e.y,i=window.devicePixelRatio||1;return {x:Math.round(o*i)/i||0,y:Math.round(n*i)/i||0}}(r),d=c.x,g=c.y,u=r.hasOwnProperty("x"),l=r.hasOwnProperty("y"),v=q,f=B,y=window;if(m){var j=N(n);j===w(n)&&(j=_(n)),a===B&&(f=F,g-=j.clientHeight-i.height,g*=s?1:-1),a===q&&(v=D,d-=j.clientWidth-i.width,d*=s?1:-1);}var h,p=Object.assign({position:t},m&&ee);return s?Object.assign({},p,((h={})[f]=l?"0":"",h[v]=u?"0":"",h.transform=(y.devicePixelRatio||1)<2?"translate("+d+"px, "+g+"px)":"translate3d("+d+"px, "+g+"px, 0)",h)):Object.assign({},p,((o={})[f]=l?g+"px":"",o[v]=u?d+"px":"",o.transform="",o))}var ne={left:"right",right:"left",bottom:"top",top:"bottom"};function ie(e){return e.replace(/left|right|bottom|top/g,(function(e){return ne[e]}))}var ae={start:"end",end:"start"};function re(e){return e.replace(/start|end/g,(function(e){return ae[e]}))}function te(e){return parseFloat(e)||0}function se(e){var o=w(e),n=function(e){var o=C(e)?I(e):{};return {top:te(o.borderTopWidth),right:te(o.borderRightWidth),bottom:te(o.borderBottomWidth),left:te(o.borderLeftWidth)}}(e),i="html"===E(e),a=z(e),r=e.clientWidth+n.right,t=e.clientHeight+n.bottom;return i&&o.innerHeight-e.clientHeight>50&&(t=o.innerHeight-n.bottom),{top:i?0:e.clientTop,right:e.clientLeft>n.left?n.right:i?o.innerWidth-r-a:e.offsetWidth-r,bottom:i?o.innerHeight-t:e.offsetHeight-t,left:i?a:e.clientLeft}}function me(e,o){var n=Boolean(o.getRootNode&&o.getRootNode().host);if(e.contains(o))return !0;if(n){var i=o;do{if(i&&e.isSameNode(i))return !0;i=i.parentNode||i.host;}while(i)}return !1}function ce(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function de(e,o){return "viewport"===o?ce(function(e){var o=w(e),n=o.visualViewport,i=o.innerWidth,a=o.innerHeight;return n&&/iPhone|iPod|iPad/.test(navigator.platform)&&(i=n.width,a=n.height),{width:i,height:a,x:0,y:0}}(e)):C(o)?b(o):ce(function(e){var o=w(e),n=k(e),i=O(_(e),o);return i.height=Math.max(i.height,o.innerHeight),i.width=Math.max(i.width,o.innerWidth),i.x=-n.scrollLeft,i.y=-n.scrollTop,i}(_(e)))}function ge(e,o,n){var i="clippingParents"===o?function(e){var o=P(e),n=["absolute","fixed"].indexOf(I(e).position)>=0&&C(e)?N(e):e;return x(n)?o.filter((function(e){return x(e)&&me(e,n)})):[]}(e):[].concat(o),a=[].concat(i,[n]),r=a[0],t=a.reduce((function(o,n){var i=de(e,n),a=se(C(n)?n:_(e));return o.top=Math.max(i.top+a.top,o.top),o.right=Math.min(i.right-a.right,o.right),o.bottom=Math.min(i.bottom-a.bottom,o.bottom),o.left=Math.max(i.left+a.left,o.left),o}),de(e,r));return t.width=t.right-t.left,t.height=t.bottom-t.top,t.x=t.left,t.y=t.top,t}function ue(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},{},e)}function le(e,o){return o.reduce((function(o,n){return o[n]=e,o}),{})}function ve(e,o){void 0===o&&(o={});var n=o,i=n.placement,a=void 0===i?e.placement:i,r=n.boundary,t=void 0===r?"clippingParents":r,s=n.rootBoundary,m=void 0===s?"viewport":s,c=n.elementContext,d=void 0===c?"popper":c,g=n.altBoundary,u=void 0!==g&&g,l=n.padding,v=void 0===l?0:l,f=ue("number"!=typeof v?v:le(v,R)),y="popper"===d?"reference":"popper",j=e.elements.reference,h=e.rects.popper,p=e.elements[u?y:d],w=ge(x(p)?p:p.contextElement||_(e.elements.popper),t,m),k=b(j),C=Q({reference:k,element:h,strategy:"absolute",placement:a}),E=ce(Object.assign({},h,{},C)),z="popper"===d?E:k,I={top:w.top-z.top+f.top,bottom:z.bottom-w.bottom+f.bottom,left:w.left-z.left+f.left,right:z.right-w.right+f.right},S=e.modifiersData.offset;if("popper"===d&&S){var O=S[a];Object.keys(I).forEach((function(e){var o=[D,F].indexOf(e)>=0?1:-1,n=[B,F].indexOf(e)>=0?"y":"x";I[e]+=O[n]*o;}));}return I}function fe(e,o){void 0===o&&(o={});var n=o,i=n.placement,a=n.boundary,r=n.rootBoundary,t=n.padding,s=n.flipVariations,m=n.allowedAutoPlacements,c=void 0===m?H:m,d=$(i),g=(d?s?V:V.filter((function(e){return $(e)===d})):R).filter((function(e){return c.indexOf(e)>=0})).reduce((function(o,n){return o[n]=ve(e,{placement:n,boundary:a,rootBoundary:r,padding:t})[K(n)],o}),{});return Object.keys(g).sort((function(e,o){return g[e]-g[o]}))}function ye(e,o,n){return Math.max(e,Math.min(o,n))}function je(e,o,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-o.height-n.y,right:e.right-o.width+n.x,bottom:e.bottom-o.height+n.y,left:e.left-o.width-n.x}}function he(e){return [B,D,F,q].some((function(o){return e[o]>=0}))}var pe=X({defaultModifiers:[{name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(e){var o=e.state,n=e.instance,i=e.options,a=i.scroll,r=void 0===a||a,t=i.resize,s=void 0===t||t,m=w(o.elements.popper),c=[].concat(o.scrollParents.reference,o.scrollParents.popper);return r&&c.forEach((function(e){e.addEventListener("scroll",n.update,Y);})),s&&m.addEventListener("resize",n.update,Y),function(){r&&c.forEach((function(e){e.removeEventListener("scroll",n.update,Y);})),s&&m.removeEventListener("resize",n.update,Y);}},data:{}},{name:"popperOffsets",enabled:!0,phase:"read",fn:function(e){var o=e.state,n=e.name;o.modifiersData[n]=Q({reference:o.rects.reference,element:o.rects.popper,strategy:"absolute",placement:o.placement});},data:{}},{name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(e){var o=e.state,n=e.options,i=n.gpuAcceleration,a=void 0===i||i,r=n.adaptive,t=void 0===r||r,s={placement:K(o.placement),popper:o.elements.popper,popperRect:o.rects.popper,gpuAcceleration:a};null!=o.modifiersData.popperOffsets&&(o.styles.popper=Object.assign({},o.styles.popper,{},oe(Object.assign({},s,{offsets:o.modifiersData.popperOffsets,position:o.options.strategy,adaptive:t})))),null!=o.modifiersData.arrow&&(o.styles.arrow=Object.assign({},o.styles.arrow,{},oe(Object.assign({},s,{offsets:o.modifiersData.arrow,position:"absolute",adaptive:!1})))),o.attributes.popper=Object.assign({},o.attributes.popper,{"data-popper-placement":o.placement});},data:{}},{name:"applyStyles",enabled:!0,phase:"write",fn:function(e){var o=e.state;Object.keys(o.elements).forEach((function(e){var n=o.styles[e]||{},i=o.attributes[e]||{},a=o.elements[e];C(a)&&E(a)&&(Object.assign(a.style,n),Object.keys(i).forEach((function(e){var o=i[e];!1===o?a.removeAttribute(e):a.setAttribute(e,!0===o?"":o);})));}));},effect:function(e){var o=e.state,n={popper:{position:o.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(o.elements.popper.style,n.popper),o.elements.arrow&&Object.assign(o.elements.arrow.style,n.arrow),function(){Object.keys(o.elements).forEach((function(e){var i=o.elements[e],a=o.attributes[e]||{},r=Object.keys(o.styles.hasOwnProperty(e)?o.styles[e]:n[e]).reduce((function(e,o){return e[o]="",e}),{});C(i)&&E(i)&&(Object.assign(i.style,r),Object.keys(a).forEach((function(e){i.removeAttribute(e);})));}));}},requires:["computeStyles"]},{name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(e){var o=e.state,n=e.options,i=e.name,a=n.offset,r=void 0===a?[0,0]:a,t=H.reduce((function(e,n){return e[n]=function(e,o,n){var i=K(e),a=[q,B].indexOf(i)>=0?-1:1,r="function"==typeof n?n(Object.assign({},o,{placement:e})):n,t=r[0],s=r[1];return t=t||0,s=(s||0)*a,[q,D].indexOf(i)>=0?{x:s,y:t}:{x:t,y:s}}(n,o.rects,r),e}),{}),s=t[o.placement],m=s.x,c=s.y;null!=o.modifiersData.popperOffsets&&(o.modifiersData.popperOffsets.x+=m,o.modifiersData.popperOffsets.y+=c),o.modifiersData[i]=t;}},{name:"flip",enabled:!0,phase:"main",fn:function(e){var o=e.state,n=e.options,i=e.name;if(!o.modifiersData[i]._skip){for(var a=n.mainAxis,r=void 0===a||a,t=n.altAxis,s=void 0===t||t,m=n.fallbackPlacements,c=n.padding,d=n.boundary,g=n.rootBoundary,u=n.altBoundary,l=n.flipVariations,v=void 0===l||l,f=n.allowedAutoPlacements,y=o.options.placement,j=K(y),h=m||(j===y||!v?[ie(y)]:function(e){if("auto"===K(e))return [];var o=ie(e);return [re(e),o,re(o)]}(y)),p=[y].concat(h).reduce((function(e,n){return e.concat("auto"===K(n)?fe(o,{placement:n,boundary:d,rootBoundary:g,padding:c,flipVariations:v,allowedAutoPlacements:f}):n)}),[]),b=o.rects.reference,w=o.rects.popper,k=new Map,x=!0,C=p[0],E=0;E=0,O=S?"width":"height",A=ve(o,{placement:_,boundary:d,rootBoundary:g,altBoundary:u,padding:c}),M=S?I?D:q:I?F:B;b[O]>w[O]&&(M=ie(M));var P=ie(M),T=[];if(r&&T.push(A[z]<=0),s&&T.push(A[M]<=0,A[P]<=0),T.every((function(e){return e}))){C=_,x=!1;break}k.set(_,T);}if(x)for(var L=function(e){var o=p.find((function(o){var n=k.get(o);if(n)return n.slice(0,e).every((function(e){return e}))}));if(o)return C=o,"break"},N=v?3:1;N>0;N--){if("break"===L(N))break}o.placement!==C&&(o.modifiersData[i]._skip=!0,o.placement=C,o.reset=!0);}},requiresIfExists:["offset"],data:{_skip:!1}},{name:"preventOverflow",enabled:!0,phase:"main",fn:function(e){var o=e.state,n=e.options,i=e.name,a=n.mainAxis,r=void 0===a||a,t=n.altAxis,s=void 0!==t&&t,m=n.boundary,c=n.rootBoundary,d=n.altBoundary,g=n.padding,u=n.tether,l=void 0===u||u,v=n.tetherOffset,f=void 0===v?0:v,y=ve(o,{boundary:m,rootBoundary:c,padding:g,altBoundary:d}),j=K(o.placement),h=$(o.placement),p=!h,b=Z(j),w="x"===b?"y":"x",k=o.modifiersData.popperOffsets,x=o.rects.reference,C=o.rects.popper,E="function"==typeof f?f(Object.assign({},o.rects,{placement:o.placement})):f,_={x:0,y:0};if(k){if(r){var z="y"===b?B:q,I="y"===b?F:D,S="y"===b?"height":"width",O=k[b],M=k[b]+y[z],P=k[b]-y[I],T=l?-C[S]/2:0,L="start"===h?x[S]:C[S],R="start"===h?-C[S]:-x[S],V=o.elements.arrow,H=l&&V?A(V):{width:0,height:0},U=o.modifiersData["arrow#persistent"]?o.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},W=U[z],J=U[I],G=ye(0,x[S],H[S]),X=p?x[S]/2-T-G-W-E:L-G-W-E,Y=p?-x[S]/2+T+G+J+E:R+G+J+E,Q=o.elements.arrow&&N(o.elements.arrow),ee=Q?"y"===b?Q.clientTop||0:Q.clientLeft||0:0,oe=o.modifiersData.offset?o.modifiersData.offset[o.placement][b]:0,ne=k[b]+X-oe-ee,ie=k[b]+Y-oe,ae=ye(l?Math.min(M,ne):M,O,l?Math.max(P,ie):P);k[b]=ae,_[b]=ae-O;}if(s){var re="x"===b?B:q,te="x"===b?F:D,se=k[w],me=ye(se+y[re],se,se-y[te]);k[w]=me,_[w]=me-se;}o.modifiersData[i]=_;}},requiresIfExists:["offset"]},{name:"arrow",enabled:!0,phase:"main",fn:function(e){var o,n=e.state,i=e.name,a=n.elements.arrow,r=n.modifiersData.popperOffsets,t=K(n.placement),s=Z(t),m=[q,D].indexOf(t)>=0?"height":"width";if(a&&r){var c=n.modifiersData[i+"#persistent"].padding,d=A(a),g="y"===s?B:q,u="y"===s?F:D,l=n.rects.reference[m]+n.rects.reference[s]-r[s]-n.rects.popper[m],v=r[s]-n.rects.reference[s],f=N(a),y=f?"y"===s?f.clientHeight||0:f.clientWidth||0:0,j=l/2-v/2,h=c[g],p=y-d[m]-c[u],b=y/2-d[m]/2+j,w=ye(h,b,p),k=s;n.modifiersData[i]=((o={})[k]=w,o.centerOffset=w-b,o);}},effect:function(e){var o=e.state,n=e.options,i=e.name,a=n.element,r=void 0===a?"[data-popper-arrow]":a,t=n.padding,s=void 0===t?0:t;null!=r&&("string"!=typeof r||(r=o.elements.popper.querySelector(r)))&&me(o.elements.popper,r)&&(o.elements.arrow=r,o.modifiersData[i+"#persistent"]={padding:ue("number"!=typeof s?s:le(s,R))});},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]},{name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(e){var o=e.state,n=e.name,i=o.rects.reference,a=o.rects.popper,r=o.modifiersData.preventOverflow,t=ve(o,{elementContext:"reference"}),s=ve(o,{altBoundary:!0}),m=je(t,i),c=je(s,a,r),d=he(m),g=he(c);o.modifiersData[n]={referenceClippingOffsets:m,popperEscapeOffsets:c,isReferenceHidden:d,hasPopperEscaped:g},o.attributes.popper=Object.assign({},o.attributes.popper,{"data-popper-reference-hidden":d,"data-popper-escaped":g});}}]}),be="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};var we=be.location||{},ke=function(){var e={base:"https://twemoji.maxcdn.com/v/13.0.0/",ext:".png",size:"72x72",className:"emoji",convert:{fromCodePoint:function(e){var o="string"==typeof e?parseInt(e,16):e;if(o<65536)return s(o);return s(55296+((o-=65536)>>10),56320+(1023&o))},toCodePoint:y},onerror:function(){this.parentNode&&this.parentNode.replaceChild(m(this.alt,!1),this);},parse:function(o,n){n&&"function"!=typeof n||(n={callback:n});return ("string"==typeof o?u:g)(o,{callback:n.callback||c,attributes:"function"==typeof n.attributes?n.attributes:v,base:"string"==typeof n.base?n.base:e.base,ext:n.ext||e.ext,size:n.folder||(i=n.size||e.size,"number"==typeof i?i+"x"+i:i),className:n.className||e.className,onerror:n.onerror||e.onerror});var i;},replace:f,test:function(e){n.lastIndex=0;var o=n.test(e);return n.lastIndex=0,o}},o={"&":"&","<":"<",">":">","'":"'",'"':"""},n=/(?:\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d])|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udeeb\udeec\udef4-\udefc\udfe0-\udfeb]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd1d\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78\udd7a-\uddb4\uddb7\uddba\uddbc-\uddcb\uddd0\uddde-\uddff\ude70-\ude74\ude78-\ude7a\ude80-\ude86\ude90-\udea8\udeb0-\udeb6\udec0-\udec2\uded0-\uded6]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g,i=/\uFE0F/g,a=String.fromCharCode(8205),r=/[&<>'"]/g,t=/^(?:iframe|noframes|noscript|script|select|style|textarea)$/,s=String.fromCharCode;return e;function m(e,o){return document.createTextNode(o?e.replace(i,""):e)}function c(e,o){return "".concat(o.base,o.size,"/",e,o.ext)}function d(e){return y(e.indexOf(a)<0?e.replace(i,""):e)}function g(e,o){for(var i,a,r,s,c,g,u,l,v,f,y,j,h,p=function e(o,n){for(var i,a,r=o.childNodes,s=r.length;s--;)3===(a=(i=r[s]).nodeType)?n.push(i):1!==a||"ownerSVGElement"in i||t.test(i.nodeName.toLowerCase())||e(i,n);return n}(e,[]),b=p.length;b--;){for(r=!1,s=document.createDocumentFragment(),g=(c=p[b]).nodeValue,l=0;u=n.exec(g);){if((v=u.index)!==l&&s.appendChild(m(g.slice(l,v),!0)),j=d(y=u[0]),l=v+y.length,h=o.callback(j,o),j&&h){for(a in (f=new Image).onerror=o.onerror,f.setAttribute("draggable","false"),i=o.attributes(y,j))i.hasOwnProperty(a)&&0!==a.indexOf("on")&&!f.hasAttribute(a)&&f.setAttribute(a,i[a]);f.className=o.className,f.alt=y,f.src=h,r=!0,s.appendChild(f);}f||s.appendChild(m(y,!1)),f=null;}r&&(l");}return a}))}function l(e){return o[e]}function v(){return null}function f(e,o){return String(e).replace(n,o)}function y(e,o){for(var n=[],i=0,a=0,r=0;rthis.showPreview(e)),this.events.on("hidePreview",()=>this.hidePreview()),e}showPreview(e){let o=e.emoji;e.custom?o=``:"twemoji"===this.options.style&&(o=xe.parse(e.emoji,Ie)),this.emoji.innerHTML=o,this.name.innerHTML=e.name;}hidePreview(){this.emoji.innerHTML="",this.name.innerHTML="";}}function Oe(e,o){for(var n=0;n0;)o+="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"[62*Math.random()|0];return o}function oo(e){return "".concat(e).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function no(e){return Object.keys(e||{}).reduce((function(o,n){return o+"".concat(n,": ").concat(e[n],";")}),"")}function io(e){return e.size!==Qe.size||e.x!==Qe.x||e.y!==Qe.y||e.rotate!==Qe.rotate||e.flipX||e.flipY}function ao(e){var o=e.transform,n=e.containerWidth,i=e.iconWidth,a={transform:"translate(".concat(n/2," 256)")},r="translate(".concat(32*o.x,", ").concat(32*o.y,") "),t="scale(".concat(o.size/16*(o.flipX?-1:1),", ").concat(o.size/16*(o.flipY?-1:1),") "),s="rotate(".concat(o.rotate," 0 0)");return {outer:a,inner:{transform:"".concat(r," ").concat(t," ").concat(s)},path:{transform:"translate(".concat(i/2*-1," -256)")}}}var ro={x:0,y:0,width:"100%",height:"100%"};function to(e){var o=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return e.attributes&&(e.attributes.fill||o)&&(e.attributes.fill="black"),e}function so(e){var o=e.icons,n=o.main,i=o.mask,a=e.prefix,r=e.iconName,t=e.transform,s=e.symbol,m=e.title,c=e.maskId,d=e.titleId,g=e.extra,u=e.watchable,l=void 0!==u&&u,v=i.found?i:n,f=v.width,y=v.height,j="fa-w-".concat(Math.ceil(f/y*16)),h=[Xe.replacementClass,r?"".concat(Xe.familyPrefix,"-").concat(r):"",j].filter((function(e){return -1===g.classes.indexOf(e)})).concat(g.classes).join(" "),p={children:[],attributes:Me({},g.attributes,{"data-prefix":a,"data-icon":r,class:h,role:g.attributes.role||"img",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 ".concat(f," ").concat(y)})};l&&(p.attributes["data-fa-i2svg"]=""),m&&p.children.push({tag:"title",attributes:{id:p.attributes["aria-labelledby"]||"title-".concat(d||eo())},children:[m]});var b=Me({},p,{prefix:a,iconName:r,main:n,mask:i,maskId:c,transform:t,symbol:s,styles:g.styles}),w=i.found&&n.found?function(e){var o,n=e.children,i=e.attributes,a=e.main,r=e.mask,t=e.maskId,s=e.transform,m=a.width,c=a.icon,d=r.width,g=r.icon,u=ao({transform:s,containerWidth:d,iconWidth:m}),l={tag:"rect",attributes:Me({},ro,{fill:"white"})},v=c.children?{children:c.children.map(to)}:{},f={tag:"g",attributes:Me({},u.inner),children:[to(Me({tag:c.tag,attributes:Me({},c.attributes,u.path)},v))]},y={tag:"g",attributes:Me({},u.outer),children:[f]},j="mask-".concat(t||eo()),h="clip-".concat(t||eo()),p={tag:"mask",attributes:Me({},ro,{id:j,maskUnits:"userSpaceOnUse",maskContentUnits:"userSpaceOnUse"}),children:[l,y]},b={tag:"defs",children:[{tag:"clipPath",attributes:{id:h},children:(o=g,"g"===o.tag?o.children:[o])},p]};return n.push(b,{tag:"rect",attributes:Me({fill:"currentColor","clip-path":"url(#".concat(h,")"),mask:"url(#".concat(j,")")},ro)}),{children:n,attributes:i}}(b):function(e){var o=e.children,n=e.attributes,i=e.main,a=e.transform,r=no(e.styles);if(r.length>0&&(n.style=r),io(a)){var t=ao({transform:a,containerWidth:i.width,iconWidth:i.width});o.push({tag:"g",attributes:Me({},t.outer),children:[{tag:"g",attributes:Me({},t.inner),children:[{tag:i.icon.tag,children:i.icon.children,attributes:Me({},i.icon.attributes,t.path)}]}]});}else o.push(i.icon);return {children:o,attributes:n}}(b),k=w.children,x=w.attributes;return b.children=k,b.attributes=x,s?function(e){var o=e.prefix,n=e.iconName,i=e.children,a=e.attributes,r=e.symbol;return [{tag:"svg",attributes:{style:"display: none;"},children:[{tag:"symbol",attributes:Me({},a,{id:!0===r?"".concat(o,"-").concat(Xe.familyPrefix,"-").concat(n):r}),children:i}]}]}(b):function(e){var o=e.children,n=e.main,i=e.mask,a=e.attributes,r=e.styles,t=e.transform;if(io(t)&&n.found&&!i.found){var s={x:n.width/n.height/2,y:.5};a.style=no(Me({},r,{"transform-origin":"".concat(s.x+t.x/16,"em ").concat(s.y+t.y/16,"em")}));}return [{tag:"svg",attributes:a,children:o}]}(b)}var co=(Xe.measurePerformance&&Ve&&Ve.mark&&Ve.measure,function(e,o,n,i){var a,r,t,s=Object.keys(e),m=s.length,c=void 0!==i?function(e,o){return function(n,i,a,r){return e.call(o,n,i,a,r)}}(o,i):o;for(void 0===n?(a=1,t=e[s[0]]):(a=0,t=n);a2&&void 0!==arguments[2]?arguments[2]:{},i=n.skipHooks,a=void 0!==i&&i,r=Object.keys(o).reduce((function(e,n){var i=o[n];return !!i.icon?e[i.iconName]=i.icon:e[n]=i,e}),{});"function"!=typeof $e.hooks.addPack||a?$e.styles[e]=Me({},$e.styles[e]||{},r):$e.hooks.addPack(e,r),"fas"===e&&go("fa",o);}var uo=$e.styles,lo=$e.shims,vo=function(){var e=function(e){return co(uo,(function(o,n,i){return o[i]=co(n,e,{}),o}),{})};e((function(e,o,n){return o[3]&&(e[o[3]]=n),e})),e((function(e,o,n){var i=o[2];return e[n]=n,i.forEach((function(o){e[o]=n;})),e}));var o="far"in uo;co(lo,(function(e,n){var i=n[0],a=n[1],r=n[2];return "far"!==a||o||(a="fas"),e[i]={prefix:a,iconName:r},e}),{});};vo();$e.styles;function fo(e,o,n){if(e&&e[o]&&e[o][n])return {prefix:o,iconName:n,icon:e[o][n]}}function yo(e){var o=e.tag,n=e.attributes,i=void 0===n?{}:n,a=e.children,r=void 0===a?[]:a;return "string"==typeof e?oo(e):"<".concat(o," ").concat(function(e){return Object.keys(e||{}).reduce((function(o,n){return o+"".concat(n,'="').concat(oo(e[n]),'" ')}),"").trim()}(i),">").concat(r.map(yo).join(""),"")}function jo(e){this.name="MissingIcon",this.message=e||"Icon unavailable",this.stack=(new Error).stack;}jo.prototype=Object.create(Error.prototype),jo.prototype.constructor=jo;var ho={fill:"currentColor"},po={attributeType:"XML",repeatCount:"indefinite",dur:"2s"},bo=(Me({},ho,{d:"M156.5,447.7l-12.6,29.5c-18.7-9.5-35.9-21.2-51.5-34.9l22.7-22.7C127.6,430.5,141.5,440,156.5,447.7z M40.6,272H8.5 c1.4,21.2,5.4,41.7,11.7,61.1L50,321.2C45.1,305.5,41.8,289,40.6,272z M40.6,240c1.4-18.8,5.2-37,11.1-54.1l-29.5-12.6 C14.7,194.3,10,216.7,8.5,240H40.6z M64.3,156.5c7.8-14.9,17.2-28.8,28.1-41.5L69.7,92.3c-13.7,15.6-25.5,32.8-34.9,51.5 L64.3,156.5z M397,419.6c-13.9,12-29.4,22.3-46.1,30.4l11.9,29.8c20.7-9.9,39.8-22.6,56.9-37.6L397,419.6z M115,92.4 c13.9-12,29.4-22.3,46.1-30.4l-11.9-29.8c-20.7,9.9-39.8,22.6-56.8,37.6L115,92.4z M447.7,355.5c-7.8,14.9-17.2,28.8-28.1,41.5 l22.7,22.7c13.7-15.6,25.5-32.9,34.9-51.5L447.7,355.5z M471.4,272c-1.4,18.8-5.2,37-11.1,54.1l29.5,12.6 c7.5-21.1,12.2-43.5,13.6-66.8H471.4z M321.2,462c-15.7,5-32.2,8.2-49.2,9.4v32.1c21.2-1.4,41.7-5.4,61.1-11.7L321.2,462z M240,471.4c-18.8-1.4-37-5.2-54.1-11.1l-12.6,29.5c21.1,7.5,43.5,12.2,66.8,13.6V471.4z M462,190.8c5,15.7,8.2,32.2,9.4,49.2h32.1 c-1.4-21.2-5.4-41.7-11.7-61.1L462,190.8z M92.4,397c-12-13.9-22.3-29.4-30.4-46.1l-29.8,11.9c9.9,20.7,22.6,39.8,37.6,56.9 L92.4,397z M272,40.6c18.8,1.4,36.9,5.2,54.1,11.1l12.6-29.5C317.7,14.7,295.3,10,272,8.5V40.6z M190.8,50 c15.7-5,32.2-8.2,49.2-9.4V8.5c-21.2,1.4-41.7,5.4-61.1,11.7L190.8,50z M442.3,92.3L419.6,115c12,13.9,22.3,29.4,30.5,46.1 l29.8-11.9C470,128.5,457.3,109.4,442.3,92.3z M397,92.4l22.7-22.7c-15.6-13.7-32.8-25.5-51.5-34.9l-12.6,29.5 C370.4,72.1,384.4,81.5,397,92.4z"}),Me({},po,{attributeName:"opacity"}));Me({},ho,{cx:"256",cy:"364",r:"28"}),Me({},po,{attributeName:"r",values:"28;14;28;28;14;28;"}),Me({},bo,{values:"1;0;1;1;0;1;"}),Me({},ho,{opacity:"1",d:"M263.7,312h-16c-6.6,0-12-5.4-12-12c0-71,77.4-63.9,77.4-107.8c0-20-17.8-40.2-57.4-40.2c-29.1,0-44.3,9.6-59.2,28.7 c-3.9,5-11.1,6-16.2,2.4l-13.1-9.2c-5.6-3.9-6.9-11.8-2.6-17.2c21.2-27.2,46.4-44.7,91.2-44.7c52.3,0,97.4,29.8,97.4,80.2 c0,67.6-77.4,63.5-77.4,107.8C275.7,306.6,270.3,312,263.7,312z"}),Me({},bo,{values:"1;0;0;0;0;1;"}),Me({},ho,{opacity:"0",d:"M232.5,134.5l7,168c0.3,6.4,5.6,11.5,12,11.5h9c6.4,0,11.7-5.1,12-11.5l7-168c0.3-6.8-5.2-12.5-12-12.5h-23 C237.7,122,232.2,127.7,232.5,134.5z"}),Me({},bo,{values:"0;0;1;1;0;0;"}),$e.styles;function wo(e){var o=e[0],n=e[1],i=Pe(e.slice(4),1)[0];return {found:!0,width:o,height:n,icon:Array.isArray(i)?{tag:"g",attributes:{class:"".concat(Xe.familyPrefix,"-").concat(Ue)},children:[{tag:"path",attributes:{class:"".concat(Xe.familyPrefix,"-").concat(Ke),fill:"currentColor",d:i[0]}},{tag:"path",attributes:{class:"".concat(Xe.familyPrefix,"-").concat(We),fill:"currentColor",d:i[1]}}]}:{tag:"path",attributes:{fill:"currentColor",d:i}}}}$e.styles;function ko(){Xe.autoAddCss&&!zo&&(!function(e){if(e&&He){var o=Re.createElement("style");o.setAttribute("type","text/css"),o.innerHTML=e;for(var n=Re.head.childNodes,i=null,a=n.length-1;a>-1;a--){var r=n[a],t=(r.tagName||"").toUpperCase();["STYLE","LINK"].indexOf(t)>-1&&(i=r);}Re.head.insertBefore(o,i);}}(function(){var e="svg-inline--fa",o=Xe.familyPrefix,n=Xe.replacementClass,i='svg:not(:root).svg-inline--fa {\n overflow: visible;\n}\n\n.svg-inline--fa {\n display: inline-block;\n font-size: inherit;\n height: 1em;\n overflow: visible;\n vertical-align: -0.125em;\n}\n.svg-inline--fa.fa-lg {\n vertical-align: -0.225em;\n}\n.svg-inline--fa.fa-w-1 {\n width: 0.0625em;\n}\n.svg-inline--fa.fa-w-2 {\n width: 0.125em;\n}\n.svg-inline--fa.fa-w-3 {\n width: 0.1875em;\n}\n.svg-inline--fa.fa-w-4 {\n width: 0.25em;\n}\n.svg-inline--fa.fa-w-5 {\n width: 0.3125em;\n}\n.svg-inline--fa.fa-w-6 {\n width: 0.375em;\n}\n.svg-inline--fa.fa-w-7 {\n width: 0.4375em;\n}\n.svg-inline--fa.fa-w-8 {\n width: 0.5em;\n}\n.svg-inline--fa.fa-w-9 {\n width: 0.5625em;\n}\n.svg-inline--fa.fa-w-10 {\n width: 0.625em;\n}\n.svg-inline--fa.fa-w-11 {\n width: 0.6875em;\n}\n.svg-inline--fa.fa-w-12 {\n width: 0.75em;\n}\n.svg-inline--fa.fa-w-13 {\n width: 0.8125em;\n}\n.svg-inline--fa.fa-w-14 {\n width: 0.875em;\n}\n.svg-inline--fa.fa-w-15 {\n width: 0.9375em;\n}\n.svg-inline--fa.fa-w-16 {\n width: 1em;\n}\n.svg-inline--fa.fa-w-17 {\n width: 1.0625em;\n}\n.svg-inline--fa.fa-w-18 {\n width: 1.125em;\n}\n.svg-inline--fa.fa-w-19 {\n width: 1.1875em;\n}\n.svg-inline--fa.fa-w-20 {\n width: 1.25em;\n}\n.svg-inline--fa.fa-pull-left {\n margin-right: 0.3em;\n width: auto;\n}\n.svg-inline--fa.fa-pull-right {\n margin-left: 0.3em;\n width: auto;\n}\n.svg-inline--fa.fa-border {\n height: 1.5em;\n}\n.svg-inline--fa.fa-li {\n width: 2em;\n}\n.svg-inline--fa.fa-fw {\n width: 1.25em;\n}\n\n.fa-layers svg.svg-inline--fa {\n bottom: 0;\n left: 0;\n margin: auto;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.fa-layers {\n display: inline-block;\n height: 1em;\n position: relative;\n text-align: center;\n vertical-align: -0.125em;\n width: 1em;\n}\n.fa-layers svg.svg-inline--fa {\n -webkit-transform-origin: center center;\n transform-origin: center center;\n}\n\n.fa-layers-counter, .fa-layers-text {\n display: inline-block;\n position: absolute;\n text-align: center;\n}\n\n.fa-layers-text {\n left: 50%;\n top: 50%;\n -webkit-transform: translate(-50%, -50%);\n transform: translate(-50%, -50%);\n -webkit-transform-origin: center center;\n transform-origin: center center;\n}\n\n.fa-layers-counter {\n background-color: #ff253a;\n border-radius: 1em;\n -webkit-box-sizing: border-box;\n box-sizing: border-box;\n color: #fff;\n height: 1.5em;\n line-height: 1;\n max-width: 5em;\n min-width: 1.5em;\n overflow: hidden;\n padding: 0.25em;\n right: 0;\n text-overflow: ellipsis;\n top: 0;\n -webkit-transform: scale(0.25);\n transform: scale(0.25);\n -webkit-transform-origin: top right;\n transform-origin: top right;\n}\n\n.fa-layers-bottom-right {\n bottom: 0;\n right: 0;\n top: auto;\n -webkit-transform: scale(0.25);\n transform: scale(0.25);\n -webkit-transform-origin: bottom right;\n transform-origin: bottom right;\n}\n\n.fa-layers-bottom-left {\n bottom: 0;\n left: 0;\n right: auto;\n top: auto;\n -webkit-transform: scale(0.25);\n transform: scale(0.25);\n -webkit-transform-origin: bottom left;\n transform-origin: bottom left;\n}\n\n.fa-layers-top-right {\n right: 0;\n top: 0;\n -webkit-transform: scale(0.25);\n transform: scale(0.25);\n -webkit-transform-origin: top right;\n transform-origin: top right;\n}\n\n.fa-layers-top-left {\n left: 0;\n right: auto;\n top: 0;\n -webkit-transform: scale(0.25);\n transform: scale(0.25);\n -webkit-transform-origin: top left;\n transform-origin: top left;\n}\n\n.fa-lg {\n font-size: 1.3333333333em;\n line-height: 0.75em;\n vertical-align: -0.0667em;\n}\n\n.fa-xs {\n font-size: 0.75em;\n}\n\n.fa-sm {\n font-size: 0.875em;\n}\n\n.fa-1x {\n font-size: 1em;\n}\n\n.fa-2x {\n font-size: 2em;\n}\n\n.fa-3x {\n font-size: 3em;\n}\n\n.fa-4x {\n font-size: 4em;\n}\n\n.fa-5x {\n font-size: 5em;\n}\n\n.fa-6x {\n font-size: 6em;\n}\n\n.fa-7x {\n font-size: 7em;\n}\n\n.fa-8x {\n font-size: 8em;\n}\n\n.fa-9x {\n font-size: 9em;\n}\n\n.fa-10x {\n font-size: 10em;\n}\n\n.fa-fw {\n text-align: center;\n width: 1.25em;\n}\n\n.fa-ul {\n list-style-type: none;\n margin-left: 2.5em;\n padding-left: 0;\n}\n.fa-ul > li {\n position: relative;\n}\n\n.fa-li {\n left: -2em;\n position: absolute;\n text-align: center;\n width: 2em;\n line-height: inherit;\n}\n\n.fa-border {\n border: solid 0.08em #eee;\n border-radius: 0.1em;\n padding: 0.2em 0.25em 0.15em;\n}\n\n.fa-pull-left {\n float: left;\n}\n\n.fa-pull-right {\n float: right;\n}\n\n.fa.fa-pull-left,\n.fas.fa-pull-left,\n.far.fa-pull-left,\n.fal.fa-pull-left,\n.fab.fa-pull-left {\n margin-right: 0.3em;\n}\n.fa.fa-pull-right,\n.fas.fa-pull-right,\n.far.fa-pull-right,\n.fal.fa-pull-right,\n.fab.fa-pull-right {\n margin-left: 0.3em;\n}\n\n.fa-spin {\n -webkit-animation: fa-spin 2s infinite linear;\n animation: fa-spin 2s infinite linear;\n}\n\n.fa-pulse {\n -webkit-animation: fa-spin 1s infinite steps(8);\n animation: fa-spin 1s infinite steps(8);\n}\n\n@-webkit-keyframes fa-spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n@keyframes fa-spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n.fa-rotate-90 {\n -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";\n -webkit-transform: rotate(90deg);\n transform: rotate(90deg);\n}\n\n.fa-rotate-180 {\n -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";\n -webkit-transform: rotate(180deg);\n transform: rotate(180deg);\n}\n\n.fa-rotate-270 {\n -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";\n -webkit-transform: rotate(270deg);\n transform: rotate(270deg);\n}\n\n.fa-flip-horizontal {\n -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";\n -webkit-transform: scale(-1, 1);\n transform: scale(-1, 1);\n}\n\n.fa-flip-vertical {\n -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";\n -webkit-transform: scale(1, -1);\n transform: scale(1, -1);\n}\n\n.fa-flip-both, .fa-flip-horizontal.fa-flip-vertical {\n -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";\n -webkit-transform: scale(-1, -1);\n transform: scale(-1, -1);\n}\n\n:root .fa-rotate-90,\n:root .fa-rotate-180,\n:root .fa-rotate-270,\n:root .fa-flip-horizontal,\n:root .fa-flip-vertical,\n:root .fa-flip-both {\n -webkit-filter: none;\n filter: none;\n}\n\n.fa-stack {\n display: inline-block;\n height: 2em;\n position: relative;\n width: 2.5em;\n}\n\n.fa-stack-1x,\n.fa-stack-2x {\n bottom: 0;\n left: 0;\n margin: auto;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.svg-inline--fa.fa-stack-1x {\n height: 1em;\n width: 1.25em;\n}\n.svg-inline--fa.fa-stack-2x {\n height: 2em;\n width: 2.5em;\n}\n\n.fa-inverse {\n color: #fff;\n}\n\n.sr-only {\n border: 0;\n clip: rect(0, 0, 0, 0);\n height: 1px;\n margin: -1px;\n overflow: hidden;\n padding: 0;\n position: absolute;\n width: 1px;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n clip: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n position: static;\n width: auto;\n}\n\n.svg-inline--fa .fa-primary {\n fill: var(--fa-primary-color, currentColor);\n opacity: 1;\n opacity: var(--fa-primary-opacity, 1);\n}\n\n.svg-inline--fa .fa-secondary {\n fill: var(--fa-secondary-color, currentColor);\n opacity: 0.4;\n opacity: var(--fa-secondary-opacity, 0.4);\n}\n\n.svg-inline--fa.fa-swap-opacity .fa-primary {\n opacity: 0.4;\n opacity: var(--fa-secondary-opacity, 0.4);\n}\n\n.svg-inline--fa.fa-swap-opacity .fa-secondary {\n opacity: 1;\n opacity: var(--fa-primary-opacity, 1);\n}\n\n.svg-inline--fa mask .fa-primary,\n.svg-inline--fa mask .fa-secondary {\n fill: black;\n}\n\n.fad.fa-inverse {\n color: #fff;\n}';if("fa"!==o||n!==e){var a=new RegExp("\\.".concat("fa","\\-"),"g"),r=new RegExp("\\--".concat("fa","\\-"),"g"),t=new RegExp("\\.".concat(e),"g");i=i.replace(a,".".concat(o,"-")).replace(r,"--".concat(o,"-")).replace(t,".".concat(n));}return i}()),zo=!0);}function xo(e,o){return Object.defineProperty(e,"abstract",{get:o}),Object.defineProperty(e,"html",{get:function(){return e.abstract.map((function(e){return yo(e)}))}}),Object.defineProperty(e,"node",{get:function(){if(He){var o=Re.createElement("div");return o.innerHTML=e.html,o.children}}}),e}function Co(e){var o=e.prefix,n=void 0===o?"fa":o,i=e.iconName;if(i)return fo(_o.definitions,n,i)||fo($e.styles,n,i)}var Eo,_o=new(function(){function e(){!function(e,o){if(!(e instanceof o))throw new TypeError("Cannot call a class as a function")}(this,e),this.definitions={};}var o,n;return o=e,(n=[{key:"add",value:function(){for(var e=this,o=arguments.length,n=new Array(o),i=0;i1&&void 0!==arguments[1]?arguments[1]:{},n=o.transform,i=void 0===n?Qe:n,a=o.symbol,r=void 0!==a&&a,t=o.mask,s=void 0===t?null:t,m=o.maskId,c=void 0===m?null:m,d=o.title,g=void 0===d?null:d,u=o.titleId,l=void 0===u?null:u,v=o.classes,f=void 0===v?[]:v,y=o.attributes,j=void 0===y?{}:y,h=o.styles,p=void 0===h?{}:h;if(e){var b=e.prefix,w=e.iconName,k=e.icon;return xo(Me({type:"icon"},e),(function(){return ko(),Xe.autoA11y&&(g?j["aria-labelledby"]="".concat(Xe.replacementClass,"-title-").concat(l||eo()):(j["aria-hidden"]="true",j.focusable="false")),so({icons:{main:wo(k),mask:s?wo(s.icon):{found:!1,width:null,height:null,icon:{}}},prefix:b,iconName:w,transform:Me({},Qe,i),symbol:r,title:g,maskId:c,titleId:l,extra:{attributes:j,styles:p,classes:f}})}))}},function(e){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=(e||{}).icon?e:Co(e||{}),i=o.mask;return i&&(i=(i||{}).icon?i:Co(i||{})),Eo(n,Me({},o,{mask:i}))});_o.add({prefix:"far",iconName:"building",icon:[448,512,[],"f1ad","M128 148v-40c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12zm140 12h40c6.6 0 12-5.4 12-12v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12zm-128 96h40c6.6 0 12-5.4 12-12v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12zm128 0h40c6.6 0 12-5.4 12-12v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12zm-76 84v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm76 12h40c6.6 0 12-5.4 12-12v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12zm180 124v36H0v-36c0-6.6 5.4-12 12-12h19.5V24c0-13.3 10.7-24 24-24h337c13.3 0 24 10.7 24 24v440H436c6.6 0 12 5.4 12 12zM79.5 463H192v-67c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v67h112.5V49L80 48l-.5 415z"]},{prefix:"fas",iconName:"cat",icon:[512,512,[],"f6be","M290.59 192c-20.18 0-106.82 1.98-162.59 85.95V192c0-52.94-43.06-96-96-96-17.67 0-32 14.33-32 32s14.33 32 32 32c17.64 0 32 14.36 32 32v256c0 35.3 28.7 64 64 64h176c8.84 0 16-7.16 16-16v-16c0-17.67-14.33-32-32-32h-32l128-96v144c0 8.84 7.16 16 16 16h32c8.84 0 16-7.16 16-16V289.86c-10.29 2.67-20.89 4.54-32 4.54-61.81 0-113.52-44.05-125.41-102.4zM448 96h-64l-64-64v134.4c0 53.02 42.98 96 96 96s96-42.98 96-96V32l-64 64zm-72 80c-8.84 0-16-7.16-16-16s7.16-16 16-16 16 7.16 16 16-7.16 16-16 16zm80 0c-8.84 0-16-7.16-16-16s7.16-16 16-16 16 7.16 16 16-7.16 16-16 16z"]},{prefix:"fas",iconName:"coffee",icon:[640,512,[],"f0f4","M192 384h192c53 0 96-43 96-96h32c70.6 0 128-57.4 128-128S582.6 32 512 32H120c-13.3 0-24 10.7-24 24v232c0 53 43 96 96 96zM512 96c35.3 0 64 28.7 64 64s-28.7 64-64 64h-32V96h32zm47.7 384H48.3c-47.6 0-61-64-36-64h583.3c25 0 11.8 64-35.9 64z"]},{prefix:"far",iconName:"flag",icon:[512,512,[],"f024","M336.174 80c-49.132 0-93.305-32-161.913-32-31.301 0-58.303 6.482-80.721 15.168a48.04 48.04 0 0 0 2.142-20.727C93.067 19.575 74.167 1.594 51.201.104 23.242-1.71 0 20.431 0 48c0 17.764 9.657 33.262 24 41.562V496c0 8.837 7.163 16 16 16h16c8.837 0 16-7.163 16-16v-83.443C109.869 395.28 143.259 384 199.826 384c49.132 0 93.305 32 161.913 32 58.479 0 101.972-22.617 128.548-39.981C503.846 367.161 512 352.051 512 335.855V95.937c0-34.459-35.264-57.768-66.904-44.117C409.193 67.309 371.641 80 336.174 80zM464 336c-21.783 15.412-60.824 32-102.261 32-59.945 0-102.002-32-161.913-32-43.361 0-96.379 9.403-127.826 24V128c21.784-15.412 60.824-32 102.261-32 59.945 0 102.002 32 161.913 32 43.271 0 96.32-17.366 127.826-32v240z"]},{prefix:"far",iconName:"frown",icon:[496,512,[],"f119","M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160-64c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm-80 128c-40.2 0-78 17.7-103.8 48.6-8.5 10.2-7.1 25.3 3.1 33.8 10.2 8.4 25.3 7.1 33.8-3.1 16.6-19.9 41-31.4 66.9-31.4s50.3 11.4 66.9 31.4c8.1 9.7 23.1 11.9 33.8 3.1 10.2-8.5 11.5-23.6 3.1-33.8C326 321.7 288.2 304 248 304z"]},{prefix:"fas",iconName:"futbol",icon:[512,512,[],"f1e3","M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zm-48 0l-.003-.282-26.064 22.741-62.679-58.5 16.454-84.355 34.303 3.072c-24.889-34.216-60.004-60.089-100.709-73.141l13.651 31.939L256 139l-74.953-41.525 13.651-31.939c-40.631 13.028-75.78 38.87-100.709 73.141l34.565-3.073 16.192 84.355-62.678 58.5-26.064-22.741-.003.282c0 43.015 13.497 83.952 38.472 117.991l7.704-33.897 85.138 10.447 36.301 77.826-29.902 17.786c40.202 13.122 84.29 13.148 124.572 0l-29.902-17.786 36.301-77.826 85.138-10.447 7.704 33.897C442.503 339.952 456 299.015 456 256zm-248.102 69.571l-29.894-91.312L256 177.732l77.996 56.527-29.622 91.312h-96.476z"]},{prefix:"fas",iconName:"history",icon:[512,512,[],"f1da","M504 255.531c.253 136.64-111.18 248.372-247.82 248.468-59.015.042-113.223-20.53-155.822-54.911-11.077-8.94-11.905-25.541-1.839-35.607l11.267-11.267c8.609-8.609 22.353-9.551 31.891-1.984C173.062 425.135 212.781 440 256 440c101.705 0 184-82.311 184-184 0-101.705-82.311-184-184-184-48.814 0-93.149 18.969-126.068 49.932l50.754 50.754c10.08 10.08 2.941 27.314-11.313 27.314H24c-8.837 0-16-7.163-16-16V38.627c0-14.254 17.234-21.393 27.314-11.314l49.372 49.372C129.209 34.136 189.552 8 256 8c136.81 0 247.747 110.78 248 247.531zm-180.912 78.784l9.823-12.63c8.138-10.463 6.253-25.542-4.21-33.679L288 256.349V152c0-13.255-10.745-24-24-24h-16c-13.255 0-24 10.745-24 24v135.651l65.409 50.874c10.463 8.137 25.541 6.253 33.679-4.21z"]},{prefix:"fas",iconName:"icons",icon:[512,512,[],"f86d","M116.65 219.35a15.68 15.68 0 0 0 22.65 0l96.75-99.83c28.15-29 26.5-77.1-4.91-103.88C203.75-7.7 163-3.5 137.86 22.44L128 32.58l-9.85-10.14C93.05-3.5 52.25-7.7 24.86 15.64c-31.41 26.78-33 74.85-5 103.88zm143.92 100.49h-48l-7.08-14.24a27.39 27.39 0 0 0-25.66-17.78h-71.71a27.39 27.39 0 0 0-25.66 17.78l-7 14.24h-48A27.45 27.45 0 0 0 0 347.3v137.25A27.44 27.44 0 0 0 27.43 512h233.14A27.45 27.45 0 0 0 288 484.55V347.3a27.45 27.45 0 0 0-27.43-27.46zM144 468a52 52 0 1 1 52-52 52 52 0 0 1-52 52zm355.4-115.9h-60.58l22.36-50.75c2.1-6.65-3.93-13.21-12.18-13.21h-75.59c-6.3 0-11.66 3.9-12.5 9.1l-16.8 106.93c-1 6.3 4.88 11.89 12.5 11.89h62.31l-24.2 83c-1.89 6.65 4.2 12.9 12.23 12.9a13.26 13.26 0 0 0 10.92-5.25l92.4-138.91c4.88-6.91-1.16-15.7-10.87-15.7zM478.08.33L329.51 23.17C314.87 25.42 304 38.92 304 54.83V161.6a83.25 83.25 0 0 0-16-1.7c-35.35 0-64 21.48-64 48s28.65 48 64 48c35.2 0 63.73-21.32 64-47.66V99.66l112-17.22v47.18a83.25 83.25 0 0 0-16-1.7c-35.35 0-64 21.48-64 48s28.65 48 64 48c35.2 0 63.73-21.32 64-47.66V32c0-19.48-16-34.42-33.92-31.67z"]},{prefix:"far",iconName:"lightbulb",icon:[352,512,[],"f0eb","M176 80c-52.94 0-96 43.06-96 96 0 8.84 7.16 16 16 16s16-7.16 16-16c0-35.3 28.72-64 64-64 8.84 0 16-7.16 16-16s-7.16-16-16-16zM96.06 459.17c0 3.15.93 6.22 2.68 8.84l24.51 36.84c2.97 4.46 7.97 7.14 13.32 7.14h78.85c5.36 0 10.36-2.68 13.32-7.14l24.51-36.84c1.74-2.62 2.67-5.7 2.68-8.84l.05-43.18H96.02l.04 43.18zM176 0C73.72 0 0 82.97 0 176c0 44.37 16.45 84.85 43.56 115.78 16.64 18.99 42.74 58.8 52.42 92.16v.06h48v-.12c-.01-4.77-.72-9.51-2.15-14.07-5.59-17.81-22.82-64.77-62.17-109.67-20.54-23.43-31.52-53.15-31.61-84.14-.2-73.64 59.67-128 127.95-128 70.58 0 128 57.42 128 128 0 30.97-11.24 60.85-31.65 84.14-39.11 44.61-56.42 91.47-62.1 109.46a47.507 47.507 0 0 0-2.22 14.3v.1h48v-.05c9.68-33.37 35.78-73.18 52.42-92.16C335.55 260.85 352 220.37 352 176 352 78.8 273.2 0 176 0z"]},{prefix:"fas",iconName:"music",icon:[512,512,[],"f001","M470.38 1.51L150.41 96A32 32 0 0 0 128 126.51v261.41A139 139 0 0 0 96 384c-53 0-96 28.66-96 64s43 64 96 64 96-28.66 96-64V214.32l256-75v184.61a138.4 138.4 0 0 0-32-3.93c-53 0-96 28.66-96 64s43 64 96 64 96-28.65 96-64V32a32 32 0 0 0-41.62-30.49z"]},{prefix:"fas",iconName:"search",icon:[512,512,[],"f002","M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"]},{prefix:"far",iconName:"smile",icon:[496,512,[],"f118","M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm4 72.6c-20.8 25-51.5 39.4-84 39.4s-63.2-14.3-84-39.4c-8.5-10.2-23.7-11.5-33.8-3.1-10.2 8.5-11.5 23.6-3.1 33.8 30 36 74.1 56.6 120.9 56.6s90.9-20.6 120.9-56.6c8.5-10.2 7.1-25.3-3.1-33.8-10.1-8.4-25.3-7.1-33.8 3.1z"]},{prefix:"fas",iconName:"times",icon:[352,512,[],"f00d","M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"]},{prefix:"fas",iconName:"user",icon:[448,512,[],"f007","M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"]});const So=Io({prefix:"far",iconName:"building"}).html[0],Oo=Io({prefix:"fas",iconName:"cat"}).html[0],Ao=Io({prefix:"fas",iconName:"coffee"}).html[0],Mo=Io({prefix:"far",iconName:"flag"}).html[0],Po=Io({prefix:"fas",iconName:"futbol"}).html[0],To=Io({prefix:"far",iconName:"frown"}).html[0],Lo=Io({prefix:"fas",iconName:"history"}).html[0],No=Io({prefix:"fas",iconName:"icons"}).html[0],Bo=Io({prefix:"far",iconName:"lightbulb"}).html[0],Fo=Io({prefix:"fas",iconName:"music"}).html[0],Do=Io({prefix:"fas",iconName:"search"}).html[0],qo=Io({prefix:"far",iconName:"smile"}).html[0],Ro=Io({prefix:"fas",iconName:"times"}).html[0],Vo=Io({prefix:"fas",iconName:"user"}).html[0];function Ho(e){const o=document.createElement("img");return o.src=e,o}function Uo(){const e=localStorage.getItem("emojiPicker.recent");return (e?JSON.parse(e):[]).filter(e=>!!e.emoji)}class Wo{constructor(e,o,n,i,a,r=!0){this.emoji=e,this.showVariants=o,this.showPreview=n,this.events=i,this.options=a,this.lazy=r;}render(){this.emojiButton=Ee("button",ze);let e=this.emoji.emoji;return this.emoji.custom?e=this.lazy?qo:``:"twemoji"===this.options.style&&(e=this.lazy?qo:xe.parse(this.emoji.emoji)),this.emojiButton.innerHTML=e,this.emojiButton.tabIndex=-1,this.emojiButton.dataset.emoji=this.emoji.emoji,this.emoji.custom&&(this.emojiButton.dataset.custom="true"),this.emojiButton.title=this.emoji.name,this.emojiButton.addEventListener("focus",()=>this.onEmojiHover()),this.emojiButton.addEventListener("blur",()=>this.onEmojiLeave()),this.emojiButton.addEventListener("click",()=>this.onEmojiClick()),this.emojiButton.addEventListener("mouseover",()=>this.onEmojiHover()),this.emojiButton.addEventListener("mouseout",()=>this.onEmojiLeave()),"twemoji"===this.options.style&&this.lazy&&(this.emojiButton.style.opacity="0.25"),this.emojiButton}onEmojiClick(){this.emoji.variations&&this.showVariants&&this.options.showVariants||!this.options.showRecents||function(e,o){const n=Uo(),i={emoji:e.emoji,name:e.name,key:e.key||e.name,custom:e.custom};localStorage.setItem("emojiPicker.recent",JSON.stringify([i,...n.filter(e=>!!e.emoji&&e.key!==i.key)].slice(0,o.recentsCount)));}(this.emoji,this.options),this.events.emit("emoji",{emoji:this.emoji,showVariants:this.showVariants,button:this.emojiButton});}onEmojiHover(){this.showPreview&&this.events.emit("showPreview",this.emoji);}onEmojiLeave(){this.showPreview&&this.events.emit("hidePreview");}}class Ko{constructor(e,o,n,i,a=!0){this.showVariants=o,this.events=n,this.options=i,this.lazy=a,this.emojis=e.filter(e=>!e.version||parseFloat(e.version)<=parseFloat(i.emojiVersion));}render(){const e=Ee("div","emoji-picker__container");return this.emojis.forEach(o=>e.appendChild(new Wo(o,this.showVariants,!0,this.events,this.options,this.lazy).render())),e}}var Jo=function(e,o){return e(o={exports:{}},o.exports),o.exports}((function(e){var o,n;o=be,n=function(){var e="undefined"==typeof window,o=new Map,n=new Map,i=[];i.total=0;var a=[],r=[];function t(){o.clear(),n.clear(),a=[],r=[];}function s(e){for(var o=-9007199254740991,n=e.length-1;n>=0;--n){var i=e[n];if(null!==i){var a=i.score;a>o&&(o=a);}}return -9007199254740991===o?null:o}function m(e,o){var n=e[o];if(void 0!==n)return n;var i=o;Array.isArray(o)||(i=o.split("."));for(var a=i.length,r=-1;e&&++r>1]=e[n],a=1+(n<<1);}for(var t=n-1>>1;n>0&&i.score>1)e[n]=e[t];e[n]=i;}return n.add=function(n){var i=o;e[o++]=n;for(var a=i-1>>1;i>0&&n.score>1)e[i]=e[a];e[i]=n;},n.poll=function(){if(0!==o){var n=e[0];return e[0]=e[--o],i(),n}},n.peek=function(n){if(0!==o)return e[0]},n.replaceTop=function(o){e[0]=o,i();},n},g=d();return function u(l){var v={single:function(e,o,n){return e?(c(e)||(e=v.getPreparedSearch(e)),o?(c(o)||(o=v.getPrepared(o)),((n&&void 0!==n.allowTypo?n.allowTypo:!l||void 0===l.allowTypo||l.allowTypo)?v.algorithm:v.algorithmNoTypo)(e,o,e[0])):null):null},go:function(e,o,n){if(!e)return i;var a=(e=v.prepareSearch(e))[0],r=n&&n.threshold||l&&l.threshold||-9007199254740991,t=n&&n.limit||l&&l.limit||9007199254740991,d=(n&&void 0!==n.allowTypo?n.allowTypo:!l||void 0===l.allowTypo||l.allowTypo)?v.algorithm:v.algorithmNoTypo,u=0,f=0,y=o.length;if(n&&n.keys)for(var j=n.scoreFn||s,h=n.keys,p=h.length,b=y-1;b>=0;--b){for(var w=o[b],k=new Array(p),x=p-1;x>=0;--x)(_=m(w,E=h[x]))?(c(_)||(_=v.getPrepared(_)),k[x]=d(e,_,a)):k[x]=null;k.obj=w;var C=j(k);null!==C&&(Cg.peek().score&&g.replaceTop(k))));}else if(n&&n.key){var E=n.key;for(b=y-1;b>=0;--b)(_=m(w=o[b],E))&&(c(_)||(_=v.getPrepared(_)),null!==(z=d(e,_,a))&&(z.scoreg.peek().score&&g.replaceTop(z)))));}else for(b=y-1;b>=0;--b){var _,z;(_=o[b])&&(c(_)||(_=v.getPrepared(_)),null!==(z=d(e,_,a))&&(z.scoreg.peek().score&&g.replaceTop(z)))));}if(0===u)return i;var I=new Array(u);for(b=u-1;b>=0;--b)I[b]=g.poll();return I.total=u+f,I},goAsync:function(o,n,a){var r=!1,t=new Promise((function(t,g){if(!o)return t(i);var u=(o=v.prepareSearch(o))[0],f=d(),y=n.length-1,j=a&&a.threshold||l&&l.threshold||-9007199254740991,h=a&&a.limit||l&&l.limit||9007199254740991,p=(a&&void 0!==a.allowTypo?a.allowTypo:!l||void 0===l.allowTypo||l.allowTypo)?v.algorithm:v.algorithmNoTypo,b=0,w=0;function k(){if(r)return g("canceled");var d=Date.now();if(a&&a.keys)for(var l=a.scoreFn||s,x=a.keys,C=x.length;y>=0;--y){for(var E=n[y],_=new Array(C),z=C-1;z>=0;--z)(O=m(E,S=x[z]))?(c(O)||(O=v.getPrepared(O)),_[z]=p(o,O,u)):_[z]=null;_.obj=E;var I=l(_);if(null!==I&&!(If.peek().score&&f.replaceTop(_)),y%1e3==0&&Date.now()-d>=10))return void(e?setImmediate(k):setTimeout(k))}else if(a&&a.key){for(var S=a.key;y>=0;--y)if((O=m(E=n[y],S))&&(c(O)||(O=v.getPrepared(O)),null!==(A=p(o,O,u))&&!(A.scoref.peek().score&&f.replaceTop(A)),y%1e3==0&&Date.now()-d>=10)))return void(e?setImmediate(k):setTimeout(k))}else for(;y>=0;--y){var O,A;if((O=n[y])&&(c(O)||(O=v.getPrepared(O)),null!==(A=p(o,O,u))&&!(A.scoref.peek().score&&f.replaceTop(A)),y%1e3==0&&Date.now()-d>=10)))return void(e?setImmediate(k):setTimeout(k))}if(0===b)return t(i);for(var M=new Array(b),P=b-1;P>=0;--P)M[P]=f.poll();M.total=b+w,t(M);}e?setImmediate(k):k();}));return t.cancel=function(){r=!0;},t},highlight:function(e,o,n){if(null===e)return null;void 0===o&&(o=""),void 0===n&&(n="");for(var i="",a=0,r=!1,t=e.target,s=t.length,m=e.indexes,c=0;c999)return v.prepare(e);var n=o.get(e);return void 0!==n||(n=v.prepare(e),o.set(e,n)),n},getPreparedSearch:function(e){if(e.length>999)return v.prepareSearch(e);var o=n.get(e);return void 0!==o||(o=v.prepareSearch(e),n.set(e,o)),o},algorithm:function(e,o,n){for(var i=o._targetLowerCodes,t=e.length,s=i.length,m=0,c=0,d=0,g=0;;){if(n===i[c]){if(a[g++]=c,++m===t)break;n=e[0===d?m:d===m?m+1:d===m-1?m-1:m];}if(++c>=s)for(;;){if(m<=1)return null;if(0===d){if(n===e[--m])continue;d=m;}else {if(1===d)return null;if((n=e[1+(m=--d)])===e[m])continue}c=a[(g=m)-1]+1;break}}m=0;var u=0,l=!1,f=0,y=o._nextBeginningIndexes;null===y&&(y=o._nextBeginningIndexes=v.prepareNextBeginningIndexes(o.target));var j=c=0===a[0]?0:y[a[0]-1];if(c!==s)for(;;)if(c>=s){if(m<=0){if(++u>t-2)break;if(e[u]===e[u+1])continue;c=j;continue}--m,c=y[r[--f]];}else if(e[0===u?m:u===m?m+1:u===m-1?m-1:m]===i[c]){if(r[f++]=c,++m===t){l=!0;break}++c;}else c=y[c];if(l)var h=r,p=f;else h=a,p=g;for(var b=0,w=-1,k=0;k=0;--k)o.indexes[k]=h[k];return o},algorithmNoTypo:function(e,o,n){for(var i=o._targetLowerCodes,t=e.length,s=i.length,m=0,c=0,d=0;;){if(n===i[c]){if(a[d++]=c,++m===t)break;n=e[m];}if(++c>=s)return null}m=0;var g=!1,u=0,l=o._nextBeginningIndexes;if(null===l&&(l=o._nextBeginningIndexes=v.prepareNextBeginningIndexes(o.target)),(c=0===a[0]?0:l[a[0]-1])!==s)for(;;)if(c>=s){if(m<=0)break;--m,c=l[r[--u]];}else if(e[m]===i[c]){if(r[u++]=c,++m===t){g=!0;break}++c;}else c=l[c];if(g)var f=r,y=u;else f=a,y=d;for(var j=0,h=-1,p=0;p=0;--p)o.indexes[p]=f[p];return o},prepareLowerCodes:function(e){for(var o=e.length,n=[],i=e.toLowerCase(),a=0;a=65&&s<=90,c=m||s>=97&&s<=122||s>=48&&s<=57,d=m&&!a||!r||!c;a=m,r=c,d&&(n[i++]=t);}return n},prepareNextBeginningIndexes:function(e){for(var o=e.length,n=v.prepareBeginningIndexes(e),i=[],a=n[0],r=0,t=0;tt?i[t]=a:(a=n[++r],i[t]=void 0===a?o:a);return i},cleanup:t,new:u};return v}()},e.exports?e.exports=n():o.fuzzysort=n();}));class Go{constructor(e,o){this.message=e,this.iconUrl=o;}render(){const e=Ee("div","emoji-picker__search-not-found"),o=Ee("div","emoji-picker__search-not-found-icon");this.iconUrl?o.appendChild(Ho(this.iconUrl)):o.innerHTML=To,e.appendChild(o);const n=Ee("h2");return n.innerHTML=this.message,e.appendChild(n),e}}class Xo{constructor(e,o,n,i,a){if(this.events=e,this.i18n=o,this.options=n,this.focusedEmojiIndex=0,this.emojisPerRow=this.options.emojisPerRow||8,this.emojiData=i.filter(e=>e.version&&parseFloat(e.version)<=parseFloat(n.emojiVersion)&&void 0!==e.category&&a.indexOf(e.category)>=0),this.options.custom){const e=this.options.custom.map(e=>({...e,custom:!0}));this.emojiData=[...this.emojiData,...e];}this.events.on("hideVariantPopup",()=>{setTimeout(()=>this.setFocusedEmoji(this.focusedEmojiIndex));});}render(){return this.searchContainer=Ee("div","emoji-picker__search-container"),this.searchField=Ee("input","emoji-picker__search"),this.searchField.placeholder=this.i18n.search,this.searchContainer.appendChild(this.searchField),this.searchIcon=Ee("span","emoji-picker__search-icon"),this.options.icons&&this.options.icons.search?this.searchIcon.appendChild(Ho(this.options.icons.search)):this.searchIcon.innerHTML=Do,this.searchIcon.addEventListener("click",e=>this.onClearSearch(e)),this.searchContainer.appendChild(this.searchIcon),this.searchField.addEventListener("keydown",e=>this.onKeyDown(e)),this.searchField.addEventListener("keyup",e=>this.onKeyUp(e)),this.searchContainer}onClearSearch(e){e.stopPropagation(),this.searchField.value&&(this.searchField.value="",this.resultsContainer=null,this.options.icons&&this.options.icons.search?(_e(this.searchIcon),this.searchIcon.appendChild(Ho(this.options.icons.search))):this.searchIcon.innerHTML=Do,this.searchIcon.style.cursor="default",this.events.emit("hideSearchResults"),setTimeout(()=>this.searchField.focus()));}setFocusedEmoji(e){if(this.resultsContainer){const o=this.resultsContainer.querySelectorAll("."+ze);o[this.focusedEmojiIndex].tabIndex=-1,this.focusedEmojiIndex=e;const n=o[this.focusedEmojiIndex];n.tabIndex=0,n.focus();}}handleResultsKeydown(e){if(this.resultsContainer){const o=this.resultsContainer.querySelectorAll("."+ze);"ArrowRight"===e.key?this.setFocusedEmoji(Math.min(this.focusedEmojiIndex+1,o.length-1)):"ArrowLeft"===e.key?this.setFocusedEmoji(Math.max(0,this.focusedEmojiIndex-1)):"ArrowDown"===e.key?(e.preventDefault(),this.focusedEmojiIndex=this.emojisPerRow&&this.setFocusedEmoji(this.focusedEmojiIndex-this.emojisPerRow)):"Escape"===e.key&&this.onClearSearch(e);}}onKeyDown(e){"Escape"===e.key&&this.searchField.value&&this.onClearSearch(e);}onKeyUp(e){if("Tab"!==e.key&&"Shift"!==e.key)if(this.searchField.value){this.options.icons&&this.options.icons.clearSearch?(_e(this.searchIcon),this.searchIcon.appendChild(Ho(this.options.icons.clearSearch))):this.searchIcon.innerHTML=Ro,this.searchIcon.style.cursor="pointer";const e=Jo.go(this.searchField.value,this.emojiData,{allowTypo:!0,limit:100,key:"name"}).map(e=>e.obj);this.events.emit("hidePreview"),e.length?(this.resultsContainer=new Ko(e,!0,this.events,this.options,!1).render(),this.resultsContainer&&(this.resultsContainer.querySelector("."+ze).tabIndex=0,this.focusedEmojiIndex=0,this.resultsContainer.addEventListener("keydown",e=>this.handleResultsKeydown(e)),this.events.emit("showSearchResults",this.resultsContainer))):this.events.emit("showSearchResults",new Go(this.i18n.notFound,this.options.icons&&this.options.icons.notFound).render());}else this.options.icons&&this.options.icons.search?(_e(this.searchIcon),this.searchIcon.appendChild(Ho(this.options.icons.search))):this.searchIcon.innerHTML=Do,this.searchIcon.style.cursor="default",this.events.emit("hideSearchResults");}}class Yo{constructor(e,o,n){this.events=e,this.emoji=o,this.options=n,this.focusedEmojiIndex=0;}getEmoji(e){return this.popup.querySelectorAll("."+ze)[e]}setFocusedEmoji(e){this.getEmoji(this.focusedEmojiIndex).tabIndex=-1,this.focusedEmojiIndex=e;const o=this.getEmoji(this.focusedEmojiIndex);o.tabIndex=0,o.focus();}render(){this.popup=Ee("div","emoji-picker__variant-popup");const e=Ee("div","emoji-picker__variant-overlay");e.addEventListener("click",e=>{e.stopPropagation(),this.popup.contains(e.target)||this.events.emit("hideVariantPopup");}),this.popup.appendChild(new Wo(this.emoji,!1,!1,this.events,this.options,!1).render()),(this.emoji.variations||[]).forEach((e,o)=>this.popup.appendChild(new Wo({name:this.emoji.name,emoji:e,key:this.emoji.name+o},!1,!1,this.events,this.options,!1).render()));const o=this.popup.querySelector("."+ze);return this.focusedEmojiIndex=0,o.tabIndex=0,setTimeout(()=>o.focus()),this.popup.addEventListener("keydown",e=>{"ArrowRight"===e.key?this.setFocusedEmoji(Math.min(this.focusedEmojiIndex+1,this.popup.querySelectorAll("."+ze).length-1)):"ArrowLeft"===e.key?this.setFocusedEmoji(Math.max(this.focusedEmojiIndex-1,0)):"Escape"===e.key&&(e.stopPropagation(),this.events.emit("hideVariantPopup"));}),e.appendChild(this.popup),e}}const $o={search:"Search emojis...",categories:{recents:"Recent Emojis",smileys:"Smileys & Emotion",people:"People & Body",animals:"Animals & Nature",food:"Food & Drink",activities:"Activities",travel:"Travel & Places",objects:"Objects",symbols:"Symbols",flags:"Flags",custom:"Custom"},notFound:"No emojis found"},Zo={recents:Lo,smileys:qo,people:Vo,animals:Oo,food:Ao,activities:Po,travel:So,objects:Bo,symbols:Fo,flags:Mo,custom:No};class Qo{constructor(e,o,n){this.options=e,this.events=o,this.i18n=n,this.activeButton=0,this.buttons=[];}render(){const e=Ee("div","emoji-picker__category-buttons");let o=this.options.showRecents?["recents",...this.options.categories||Ce.categories]:this.options.categories||Ce.categories;return this.options.custom&&(o=[...o,"custom"]),o.forEach(o=>{const n=Ee("button","emoji-picker__category-button");this.options.icons&&this.options.icons.categories&&this.options.icons.categories[o]?n.appendChild(Ho(this.options.icons.categories[o])):n.innerHTML=Zo[o],n.tabIndex=-1,n.title=this.i18n.categories[o],e.appendChild(n),this.buttons.push(n),n.addEventListener("click",()=>{this.events.emit("categoryClicked",o);});}),e.addEventListener("keydown",e=>{switch(e.key){case"ArrowRight":this.events.emit("categoryClicked",o[(this.activeButton+1)%this.buttons.length]);break;case"ArrowLeft":this.events.emit("categoryClicked",o[0===this.activeButton?this.buttons.length-1:this.activeButton-1]);break;case"ArrowUp":case"ArrowDown":e.stopPropagation(),e.preventDefault();}}),e}setActiveButton(e,o=!0){let n=this.buttons[this.activeButton];n.classList.remove("active"),n.tabIndex=-1,this.activeButton=e,n=this.buttons[this.activeButton],n.classList.add("active"),n.tabIndex=0,o&&n.focus();}}const en={};Ce.emoji.forEach(e=>{let o=en[Ce.categories[e.category]];o||(o=en[Ce.categories[e.category]]=[]),o.push(e);});class on{constructor(e,o,n){this.events=e,this.i18n=o,this.options=n,this.currentCategory=0,this.headers=[],this.focusedIndex=0,this.handleKeyDown=e=>{switch(this.emojis.removeEventListener("scroll",this.highlightCategory),e.key){case"ArrowRight":this.focusedEmoji.tabIndex=-1,this.focusedIndex===this.currentEmojiCount-1&&this.currentCategory0?(this.options.showCategoryButtons&&this.categoryButtons.setActiveButton(--this.currentCategory),this.setFocusedEmoji(this.currentEmojiCount-1)):this.setFocusedEmoji(Math.max(0,this.focusedIndex-1));break;case"ArrowDown":e.preventDefault(),this.focusedEmoji.tabIndex=-1,this.focusedIndex+this.emojisPerRow>=this.currentEmojiCount&&this.currentCategorythis.emojisPerRow&&this.setFocusedEmoji(this.focusedIndex+this.emojisPerRow);break;case"ArrowUp":if(e.preventDefault(),this.focusedEmoji.tabIndex=-1,this.focusedIndex0){const e=this.getEmojiCount(this.currentCategory-1);let o=e%this.emojisPerRow;0===o&&(o=this.emojisPerRow);const n=this.focusedIndex,i=n>o-1?e-1:e-o+n;this.currentCategory--,this.options.showCategoryButtons&&this.categoryButtons.setActiveButton(this.currentCategory),this.setFocusedEmoji(i);}else this.setFocusedEmoji(this.focusedIndex>=this.emojisPerRow?this.focusedIndex-this.emojisPerRow:this.focusedIndex);}requestAnimationFrame(()=>this.emojis.addEventListener("scroll",this.highlightCategory));},this.addCategory=(e,o)=>{const n=Ee("h2","emoji-picker__category-name");n.innerHTML=this.i18n.categories[e]||$o.categories[e],this.emojis.appendChild(n),this.headers.push(n),this.emojis.appendChild(new Ko(o,!0,this.events,this.options,"recents"!==e).render());},this.selectCategory=(e,o=!0)=>{this.emojis.removeEventListener("scroll",this.highlightCategory),this.focusedEmoji&&(this.focusedEmoji.tabIndex=-1);const n=this.categories.indexOf(e);this.currentCategory=n,this.setFocusedEmoji(0,!1),this.options.showCategoryButtons&&this.categoryButtons.setActiveButton(this.currentCategory,o);const i=this.headerOffsets[n];this.emojis.scrollTop=i,requestAnimationFrame(()=>this.emojis.addEventListener("scroll",this.highlightCategory));},this.highlightCategory=()=>{if(document.activeElement&&document.activeElement.classList.contains("emoji-picker__emoji"))return;let e=this.headerOffsets.findIndex(e=>e>=Math.round(this.emojis.scrollTop));this.emojis.scrollTop+this.emojis.offsetHeight===this.emojis.scrollHeight&&(e=-1),0===e?e=1:e<0&&(e=this.headerOffsets.length),this.headerOffsets[e]===this.emojis.scrollTop&&e++,this.currentCategory=e-1,this.options.showCategoryButtons&&this.categoryButtons.setActiveButton(this.currentCategory);},this.emojisPerRow=n.emojisPerRow||8,this.categories=n.categories||Ce.categories,n.showRecents&&(this.categories=["recents",...this.categories]),n.custom&&(this.categories=[...this.categories,"custom"]);}updateRecents(){if(this.options.showRecents){en.recents=Uo();const e=this.emojis.querySelector(".emoji-picker__container");e&&e.parentNode&&e.parentNode.replaceChild(new Ko(en.recents,!0,this.events,this.options,!1).render(),e);}}render(){this.container=Ee("div","emoji-picker__emoji-area"),this.options.showCategoryButtons&&(this.categoryButtons=new Qo(this.options,this.events,this.i18n),this.container.appendChild(this.categoryButtons.render())),this.emojis=Ee("div","emoji-picker__emojis"),this.options.showRecents&&(en.recents=Uo()),this.options.custom&&(en.custom=this.options.custom.map(e=>({...e,custom:!0}))),this.categories.forEach(e=>this.addCategory(e,en[e])),requestAnimationFrame(()=>{setTimeout(()=>{setTimeout(()=>this.emojis.addEventListener("scroll",this.highlightCategory));});}),this.emojis.addEventListener("keydown",this.handleKeyDown),this.events.on("categoryClicked",this.selectCategory),this.container.appendChild(this.emojis);return this.container.querySelectorAll("."+ze)[0].tabIndex=0,this.container}reset(){this.headerOffsets=Array.prototype.map.call(this.headers,e=>e.offsetTop),this.selectCategory(this.options.initialCategory||"smileys",!1),this.currentCategory=this.categories.indexOf(this.options.initialCategory||"smileys"),this.options.showCategoryButtons&&this.categoryButtons.setActiveButton(this.currentCategory,!1);}get currentCategoryEl(){return this.emojis.querySelectorAll(".emoji-picker__container")[this.currentCategory]}get focusedEmoji(){return this.currentCategoryEl.querySelectorAll("."+ze)[this.focusedIndex]}get currentEmojiCount(){return this.currentCategoryEl.querySelectorAll("."+ze).length}getEmojiCount(e){return this.emojis.querySelectorAll(".emoji-picker__container")[e].querySelectorAll("."+ze).length}setFocusedEmoji(e,o=!0){this.focusedIndex=e,this.focusedEmoji&&(this.focusedEmoji.tabIndex=0,o&&this.focusedEmoji.focus());}}const nn={ext:".svg",folder:"svg"},an={position:"auto",autoHide:!0,autoFocusSearch:!0,showAnimation:!0,showPreview:!0,showSearch:!0,showRecents:!0,showVariants:!0,showCategoryButtons:!0,recentsCount:50,emojiVersion:"12.1",theme:"light",categories:["smileys","people","animals","food","activities","travel","objects","symbols","flags"],style:"native",emojisPerRow:8,rows:6,emojiSize:"1.8em",initialCategory:"smileys"};class rn{constructor(e={}){this.events=new p,this.publicEvents=new p,this.pickerVisible=!1,this.options={...an,...e},this.options.rootElement||(this.options.rootElement=document.body),this.i18n={...$o,...e.i18n},this.onDocumentClick=this.onDocumentClick.bind(this),this.onDocumentKeydown=this.onDocumentKeydown.bind(this),this.theme=this.options.theme||"light",this.buildPicker();}on(e,o){this.publicEvents.on(e,o);}off(e,o){this.publicEvents.off(e,o);}buildPicker(){if(this.pickerEl=Ee("div","emoji-picker"),this.updateTheme(this.theme),this.options.showAnimation||this.pickerEl.style.setProperty("--animation-duration","0s"),this.options.emojisPerRow&&this.pickerEl.style.setProperty("--emoji-per-row",this.options.emojisPerRow.toString()),this.options.rows&&this.pickerEl.style.setProperty("--row-count",this.options.rows.toString()),this.options.emojiSize&&this.pickerEl.style.setProperty("--emoji-size",this.options.emojiSize),this.options.showCategoryButtons||this.pickerEl.style.setProperty("--category-button-height","0"),this.focusTrap=j(this.pickerEl,{clickOutsideDeactivates:!0,initialFocus:this.options.showSearch&&this.options.autoFocusSearch?".emoji-picker__search":'.emoji-picker__emoji[tabindex="0"]'}),this.pickerContent=Ee("div","emoji-picker__content"),this.options.plugins){const e=Ee("div","emoji-picker__plugin-container");this.options.plugins.forEach(o=>{if(!o.render)throw new Error('Emoji Button plugins must have a "render" function.');e.appendChild(o.render(this));}),this.pickerEl.appendChild(e);}if(this.options.showSearch){const e=new Xo(this.events,this.i18n,this.options,Ce.emoji,(this.options.categories||[]).map(e=>Ce.categories.indexOf(e))).render();this.pickerEl.appendChild(e);}this.pickerEl.appendChild(this.pickerContent),this.emojiArea=new on(this.events,this.i18n,this.options),this.pickerContent.appendChild(this.emojiArea.render()),this.events.on("showSearchResults",e=>{_e(this.pickerContent),e.classList.add("search-results"),this.pickerContent.appendChild(e);}),this.events.on("hideSearchResults",()=>{this.pickerContent.firstChild!==this.emojiArea.container&&(_e(this.pickerContent),this.pickerContent.appendChild(this.emojiArea.container)),this.emojiArea.reset();}),this.options.showPreview&&this.pickerEl.appendChild(new Se(this.events,this.options).render()),this.events.on("emoji",({emoji:e,showVariants:o})=>{e.variations&&o&&this.options.showVariants?this.showVariantPopup(e):(setTimeout(()=>this.emojiArea.updateRecents()),e.custom?this.publicEvents.emit("emoji",{url:e.emoji,name:e.name,custom:!0}):"twemoji"===this.options.style?xe.parse(e.emoji,{...nn,callback:(o,n)=>{this.publicEvents.emit("emoji",{url:`${n.base}${n.size}/${o}${n.ext}`,emoji:e.emoji,name:e.name});}}):this.publicEvents.emit("emoji",{emoji:e.emoji,name:e.name}),this.options.autoHide&&this.hidePicker());}),this.wrapper=Ee("div","emoji-picker__wrapper"),this.wrapper.appendChild(this.pickerEl),this.wrapper.style.display="none",this.options.zIndex&&(this.wrapper.style.zIndex=this.options.zIndex+""),this.options.rootElement&&this.options.rootElement.appendChild(this.wrapper),this.observeForLazyLoad();}showVariantPopup(e){const o=new Yo(this.events,e,this.options).render();o&&this.pickerEl.appendChild(o),this.events.on("hideVariantPopup",()=>{o&&(o.classList.add("hiding"),setTimeout(()=>{o&&this.pickerEl.removeChild(o);},175)),this.events.off("hideVariantPopup");});}observeForLazyLoad(){this.observer=new IntersectionObserver(e=>{Array.prototype.filter.call(e,e=>e.intersectionRatio>0).map(e=>e.target).forEach(e=>{if(!e.dataset.loaded)if(e.dataset.custom){const o=Ee("img","emoji-picker__custom-emoji");o.src=e.dataset.emoji,e.innerText="",e.appendChild(o),e.dataset.loaded=!0,e.style.opacity=1;}else "twemoji"===this.options.style&&(e.innerHTML=xe.parse(e.dataset.emoji,nn),e.dataset.loaded=!0,e.style.opacity="1");});},{root:this.emojiArea.emojis});this.emojiArea.emojis.querySelectorAll("."+ze).forEach(e=>{"twemoji"!==this.options.style&&"true"!==e.dataset.custom||this.observer.observe(e);});}onDocumentClick(e){this.pickerEl.contains(e.target)||this.hidePicker();}destroyPicker(){this.events.off("emoji"),this.events.off("hideVariantPopup"),this.options.rootElement&&(this.options.rootElement.removeChild(this.wrapper),this.popper&&this.popper.destroy()),this.observer&&this.observer.disconnect(),this.options.plugins&&this.options.plugins.forEach(e=>{e.destroy&&e.destroy();});}hidePicker(){this.hideInProgress=!0,this.focusTrap.deactivate(),this.pickerVisible=!1,this.overlay&&(document.body.removeChild(this.overlay),this.overlay=void 0),this.emojiArea.emojis.removeEventListener("scroll",this.emojiArea.highlightCategory),this.pickerEl.classList.add("hiding"),setTimeout(()=>{this.wrapper.style.display="none",this.pickerEl.classList.remove("hiding"),this.pickerContent.firstChild!==this.emojiArea.container&&(_e(this.pickerContent),this.pickerContent.appendChild(this.emojiArea.container));const e=this.pickerEl.querySelector(".emoji-picker__search");e&&(e.value="");this.pickerEl.querySelector(".emoji-picker__variant-overlay")&&this.events.emit("hideVariantPopup"),this.hideInProgress=!1,this.popper&&this.popper.destroy(),this.publicEvents.emit("hidden");},this.options.showAnimation?170:0),setTimeout(()=>{document.removeEventListener("click",this.onDocumentClick),document.removeEventListener("keydown",this.onDocumentKeydown);});}showPicker(e){if(this.hideInProgress)setTimeout(()=>this.showPicker(e),100);else {if(this.pickerVisible=!0,this.wrapper.style.display="block",window.matchMedia("screen and (max-width: 450px)").matches){const e=window.getComputedStyle(this.pickerEl),o=document.querySelector("html"),n=o&&o.clientHeight,i=o&&o.clientWidth,a=parseInt(e.height),r=n?n/2-a/2:0,t=parseInt(e.width),s=i?i/2-t/2:0;this.wrapper.style.position="fixed",this.wrapper.style.top=r+"px",this.wrapper.style.left=s+"px",this.wrapper.style.zIndex="5000",this.overlay=Ee("div","emoji-picker__overlay"),document.body.appendChild(this.overlay);}else "string"==typeof this.options.position?this.popper=pe(e,this.wrapper,{placement:this.options.position}):this.options.position&&(this.options.position.top||this.options.position.left)&&(this.wrapper.style.position="fixed",this.options.position.top&&(this.wrapper.style.top=this.options.position.top),this.options.position.bottom&&(this.wrapper.style.bottom=this.options.position.bottom),this.options.position.left&&(this.wrapper.style.left=this.options.position.left),this.options.position.right&&(this.wrapper.style.right=this.options.position.right));this.focusTrap.activate(),setTimeout(()=>{document.addEventListener("click",this.onDocumentClick),document.addEventListener("keydown",this.onDocumentKeydown);this.pickerEl.querySelector(this.options.showSearch&&this.options.autoFocusSearch?".emoji-picker__search":`.${ze}[tabindex="0"]`).focus();}),this.emojiArea.reset();}}togglePicker(e){this.pickerVisible?this.hidePicker():this.showPicker(e);}isPickerVisible(){return this.pickerVisible}onDocumentKeydown(e){if("Escape"===e.key)this.hidePicker();else if("Tab"===e.key)this.pickerEl.classList.add("keyboard");else if(e.key.match(/^[\w]$/)){const e=this.pickerEl.querySelector(".emoji-picker__search");e&&e.focus();}}setTheme(e){e!==this.theme&&(this.pickerEl.classList.remove(this.theme),this.theme=e,this.updateTheme(this.theme));}updateTheme(e){this.pickerEl.classList.add(e);}} + +export { rn as EmojiButton }; diff --git a/webroot/js/web_modules/@justinribeiro/lite-youtube.js b/webroot/js/web_modules/@justinribeiro/lite-youtube.js new file mode 100644 index 000000000..f6a750abf --- /dev/null +++ b/webroot/js/web_modules/@justinribeiro/lite-youtube.js @@ -0,0 +1,301 @@ +/** + * + * The shadowDom / Intersection Observer version of Paul's concept: + * https://github.com/paulirish/lite-youtube-embed + * + * A lightweight YouTube embed. Still should feel the same to the user, just + * MUCH faster to initialize and paint. + * + * Thx to these as the inspiration + * https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html + * https://autoplay-youtube-player.glitch.me/ + * + * Once built it, I also found these (👍👍): + * https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube + * https://github.com/Daugilas/lazyYT https://github.com/vb/lazyframe + */ +class LiteYTEmbed extends HTMLElement { + constructor() { + super(); + this.iframeLoaded = false; + this.setupDom(); + } + static get observedAttributes() { + return ['videoid']; + } + connectedCallback() { + this.addEventListener('pointerover', LiteYTEmbed.warmConnections, { + once: true, + }); + this.addEventListener('click', () => this.addIframe()); + } + get videoId() { + return encodeURIComponent(this.getAttribute('videoid') || ''); + } + set videoId(id) { + this.setAttribute('videoid', id); + } + get videoTitle() { + return this.getAttribute('videotitle') || 'Video'; + } + set videoTitle(title) { + this.setAttribute('videotitle', title); + } + get videoPlay() { + return this.getAttribute('videoPlay') || 'Play'; + } + set videoPlay(name) { + this.setAttribute('videoPlay', name); + } + get videoStartAt() { + return Number(this.getAttribute('videoStartAt') || '0'); + } + set videoStartAt(time) { + this.setAttribute('videoStartAt', String(time)); + } + get autoLoad() { + return this.hasAttribute('autoload'); + } + set autoLoad(value) { + if (value) { + this.setAttribute('autoload', ''); + } + else { + this.removeAttribute('autoload'); + } + } + get params() { + return `start=${this.videoStartAt}&${this.getAttribute('params')}`; + } + /** + * Define our shadowDOM for the component + */ + setupDom() { + const shadowDom = this.attachShadow({ mode: 'open' }); + shadowDom.innerHTML = ` + +
+ + + + + + +
+ `; + this.domRefFrame = this.shadowRoot.querySelector('#frame'); + this.domRefImg = { + fallback: this.shadowRoot.querySelector('#fallbackPlaceholder'), + webp: this.shadowRoot.querySelector('#webpPlaceholder'), + jpeg: this.shadowRoot.querySelector('#jpegPlaceholder'), + }; + this.domRefPlayButton = this.shadowRoot.querySelector('.lty-playbtn'); + } + /** + * Parse our attributes and fire up some placeholders + */ + setupComponent() { + this.initImagePlaceholder(); + this.domRefPlayButton.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`); + this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`); + if (this.autoLoad) { + this.initIntersectionObserver(); + } + } + /** + * Lifecycle method that we use to listen for attribute changes to period + * @param {*} name + * @param {*} oldVal + * @param {*} newVal + */ + attributeChangedCallback(name, oldVal, newVal) { + switch (name) { + case 'videoid': { + if (oldVal !== newVal) { + this.setupComponent(); + // if we have a previous iframe, remove it and the activated class + if (this.domRefFrame.classList.contains('lyt-activated')) { + this.domRefFrame.classList.remove('lyt-activated'); + this.shadowRoot.querySelector('iframe').remove(); + } + } + break; + } + } + } + /** + * Inject the iframe into the component body + */ + addIframe() { + if (!this.iframeLoaded) { + const iframeHTML = ` +`; + this.domRefFrame.insertAdjacentHTML('beforeend', iframeHTML); + this.domRefFrame.classList.add('lyt-activated'); + this.iframeLoaded = true; + } + } + /** + * Setup the placeholder image for the component + */ + initImagePlaceholder() { + // we don't know which image type to preload, so warm the connection + LiteYTEmbed.addPrefetch('preconnect', 'https://i.ytimg.com/'); + const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/hqdefault.webp`; + const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`; + this.domRefImg.webp.srcset = posterUrlWebp; + this.domRefImg.jpeg.srcset = posterUrlJpeg; + this.domRefImg.fallback.src = posterUrlJpeg; + this.domRefImg.fallback.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`); + this.domRefImg.fallback.setAttribute('alt', `${this.videoPlay}: ${this.videoTitle}`); + } + /** + * Setup the Intersection Observer to load the iframe when scrolled into view + */ + initIntersectionObserver() { + if ('IntersectionObserver' in window && + 'IntersectionObserverEntry' in window) { + const options = { + root: null, + rootMargin: '0px', + threshold: 0, + }; + const observer = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting && !this.iframeLoaded) { + LiteYTEmbed.warmConnections(); + this.addIframe(); + observer.unobserve(this); + } + }); + }, options); + observer.observe(this); + } + } + /** + * Add a to the head + * @param {*} kind + * @param {*} url + * @param {*} as + */ + static addPrefetch(kind, url, as) { + const linkElem = document.createElement('link'); + linkElem.rel = kind; + linkElem.href = url; + if (as) { + linkElem.as = as; + } + linkElem.crossOrigin = 'true'; + document.head.append(linkElem); + } + /** + * Begin preconnecting to warm up the iframe load Since the embed's netwok + * requests load within its iframe, preload/prefetch'ing them outside the + * iframe will only cause double-downloads. So, the best we can do is warm up + * a few connections to origins that are in the critical path. + * + * Maybe `` would work, but it's unsupported: + * http://crbug.com/593267 But TBH, I don't think it'll happen soon with Site + * Isolation and split caches adding serious complexity. + */ + static warmConnections() { + if (LiteYTEmbed.preconnected) + return; + // Host that YT uses to serve JS needed by player, per amp-youtube + LiteYTEmbed.addPrefetch('preconnect', 'https://s.ytimg.com'); + // The iframe document and most of its subresources come right off + // youtube.com + LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com'); + // The botguard script is fetched off from google.com + LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com'); + // TODO: Not certain if these ad related domains are in the critical path. + // Could verify with domain-specific throttling. + LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net'); + LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net'); + LiteYTEmbed.preconnected = true; + } +} +LiteYTEmbed.preconnected = false; +// Register custom element +customElements.define('lite-youtube', LiteYTEmbed); + +export { LiteYTEmbed }; diff --git a/webroot/js/web_modules/@videojs/themes/fantasy/index.css b/webroot/js/web_modules/@videojs/themes/fantasy/index.css new file mode 100644 index 000000000..d7695bdc4 --- /dev/null +++ b/webroot/js/web_modules/@videojs/themes/fantasy/index.css @@ -0,0 +1,116 @@ +.vjs-theme-fantasy { + --vjs-theme-fantasy--primary: #9f44b4; + --vjs-theme-fantasy--secondary: #fff; +} + +.vjs-theme-fantasy .vjs-big-play-button { + width: 70px; + height: 70px; + background: none; + line-height: 70px; + font-size: 80px; + border: none; + top: 50%; + left: 50%; + margin-top: -35px; + margin-left: -35px; + color: var(--vjs-theme-fantasy--primary); +} + +.vjs-theme-fantasy:hover .vjs-big-play-button, +.vjs-theme-fantasy.vjs-big-play-button:focus { + background-color: transparent; + color: #fff; +} + +.vjs-theme-fantasy .vjs-control-bar { + height: 54px; +} + +.vjs-theme-fantasy .vjs-button > .vjs-icon-placeholder::before { + line-height: 54px; +} + +.vjs-theme-fantasy .vjs-time-control { + line-height: 54px; +} + +/* Play Button */ +.vjs-theme-fantasy .vjs-play-control { + font-size: 1.5em; + position: relative; +} + +.vjs-theme-fantasy .vjs-volume-panel { + order: 4; +} + +.vjs-theme-fantasy .vjs-volume-bar { + margin-top: 2.5em; +} + +.vjs-theme-city .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-horizontal { + height: 100%; +} + +.vjs-theme-fantasy .vjs-progress-control .vjs-progress-holder { + font-size: 1.5em; +} + +.vjs-theme-fantasy .vjs-progress-control:hover .vjs-progress-holder { + font-size: 1.5em; +} + +.vjs-theme-fantasy .vjs-play-control .vjs-icon-placeholder::before { + height: 1.3em; + width: 1.3em; + margin-top: 0.2em; + border-radius: 1em; + border: 3px solid var(--vjs-theme-fantasy--secondary); + top: 2px; + left: 9px; + line-height: 1.1; +} + +.vjs-theme-fantasy .vjs-play-control:hover .vjs-icon-placeholder::before { + border: 3px solid var(--vjs-theme-fantasy--secondary); +} + +.vjs-theme-fantasy .vjs-play-progress { + background-color: var(--vjs-theme-fantasy--primary); +} + +.vjs-theme-fantasy .vjs-play-progress::before { + height: 0.8em; + width: 0.8em; + content: ''; + background-color: var(--vjs-theme-fantasy--primary); + border: 4px solid var(--vjs-theme-fantasy--secondary); + border-radius: 0.8em; + top: -0.25em; +} + +.vjs-theme-fantasy .vjs-progress-control { + font-size: 14px; +} + +.vjs-theme-fantasy .vjs-fullscreen-control { + order: 6; +} + +.vjs-theme-fantasy .vjs-remaining-time { + display: none; +} + +/* Nyan version */ +.vjs-theme-fantasy.nyan .vjs-play-progress { + background: linear-gradient(to bottom, #fe0000 0%, #fe9a01 16.666666667%, #fe9a01 16.666666667%, #ffff00 33.332666667%, #ffff00 33.332666667%, #32ff00 49.999326667%, #32ff00 49.999326667%, #0099fe 66.6659926%, #0099fe 66.6659926%, #6633ff 83.33266%, #6633ff 83.33266%); +} + +.vjs-theme-fantasy.nyan .vjs-play-progress::before { + height: 1.3em; + width: 1.3em; + background: svg-load('icons/nyan-cat.svg', fill=#fff) no-repeat; + border: none; + top: -0.35em; +} diff --git a/webroot/js/web_modules/common/_commonjsHelpers-37fa8da4.js b/webroot/js/web_modules/common/_commonjsHelpers-37fa8da4.js new file mode 100644 index 000000000..b96994cbc --- /dev/null +++ b/webroot/js/web_modules/common/_commonjsHelpers-37fa8da4.js @@ -0,0 +1,25 @@ +var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + +function getDefaultExportFromCjs (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; +} + +function createCommonjsModule(fn, basedir, module) { + return module = { + path: basedir, + exports: {}, + require: function (path, base) { + return commonjsRequire(path, (base === undefined || base === null) ? module.path : base); + } + }, fn(module, module.exports), module.exports; +} + +function getDefaultExportFromNamespaceIfNotNamed (n) { + return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n; +} + +function commonjsRequire () { + throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs'); +} + +export { commonjsGlobal as a, getDefaultExportFromNamespaceIfNotNamed as b, createCommonjsModule as c, getDefaultExportFromCjs as g }; diff --git a/webroot/js/web_modules/common/window-2f8a9a85.js b/webroot/js/web_modules/common/window-2f8a9a85.js new file mode 100644 index 000000000..3866e0b97 --- /dev/null +++ b/webroot/js/web_modules/common/window-2f8a9a85.js @@ -0,0 +1,44 @@ +import { b as getDefaultExportFromNamespaceIfNotNamed, a as commonjsGlobal } from './_commonjsHelpers-37fa8da4.js'; + +var _nodeResolve_empty = {}; + +var _nodeResolve_empty$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + 'default': _nodeResolve_empty +}); + +var minDoc = /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(_nodeResolve_empty$1); + +var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : + typeof window !== 'undefined' ? window : {}; + + +var doccy; + +if (typeof document !== 'undefined') { + doccy = document; +} else { + doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4']; + + if (!doccy) { + doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc; + } +} + +var document_1 = doccy; + +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; + +export { document_1 as d, window_1 as w }; diff --git a/webroot/js/web_modules/htm.js b/webroot/js/web_modules/htm.js new file mode 100644 index 000000000..4c86298da --- /dev/null +++ b/webroot/js/web_modules/htm.js @@ -0,0 +1,3 @@ +var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e="";},a=0;a"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0]);}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]} + +export default htm_module; diff --git a/webroot/js/web_modules/import-map.json b/webroot/js/web_modules/import-map.json new file mode 100644 index 000000000..487d76b86 --- /dev/null +++ b/webroot/js/web_modules/import-map.json @@ -0,0 +1,14 @@ +{ + "imports": { + "@joeattardi/emoji-button": "./@joeattardi/emoji-button.js", + "@justinribeiro/lite-youtube": "./@justinribeiro/lite-youtube.js", + "@videojs/http-streaming/dist/videojs-http-streaming.min.js": "./@videojs/http-streaming/dist/videojs-http-streaming.min.js", + "@videojs/themes/fantasy/index.css": "./@videojs/themes/fantasy/index.css", + "htm": "./htm.js", + "preact": "./preact.js", + "showdown": "./showdown.js", + "tailwindcss/dist/tailwind.min.css": "./tailwindcss/dist/tailwind.min.css", + "video.js/dist/video-js.min.css": "./videojs/dist/video-js.min.css", + "video.js/dist/video.min.js": "./videojs/dist/video.min.js" + } +} \ No newline at end of file diff --git a/webroot/js/web_modules/preact.js b/webroot/js/web_modules/preact.js new file mode 100644 index 000000000..6b84c5e2a --- /dev/null +++ b/webroot/js/web_modules/preact.js @@ -0,0 +1,3 @@ +var n,l,u,i,t,o,r,f={},e=[],c=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function s(n,l){for(var u in l)n[u]=l[u];return n}function a(n){var l=n.parentNode;l&&l.removeChild(n);}function h(n,l,u){var i,t,o,r=arguments,f={};for(o in l)"key"==o?i=l[o]:"ref"==o?t=l[o]:f[o]=l[o];if(arguments.length>3)for(u=[u],o=3;o1&&T(t,l,u),l=x(u,t,t,n.__k,null,t.__e,l),"function"==typeof n.type&&(n.__d=l)));}function $(l,u,i,t,o,r,f,e,c){var a,h,v,y,_,w,k,g,b,x,A,P=u.type;if(void 0!==u.constructor)return null;null!=i.__h&&(c=i.__h,e=u.__e=i.__e,u.__h=null,r=[e]),(a=n.__b)&&a(u);try{n:if("function"==typeof P){if(g=u.props,b=(a=P.contextType)&&t[a.__c],x=a?b?b.props.value:a.__:t,i.__c?k=(h=u.__c=i.__c).__=h.__E:("prototype"in P&&P.prototype.render?u.__c=h=new P(g,x):(u.__c=h=new d(g,x),h.constructor=P,h.render=M),b&&b.sub(h),h.props=g,h.state||(h.state={}),h.context=x,h.__n=t,v=h.__d=!0,h.__h=[]),null==h.__s&&(h.__s=h.state),null!=P.getDerivedStateFromProps&&(h.__s==h.state&&(h.__s=s({},h.__s)),s(h.__s,P.getDerivedStateFromProps(g,h.__s))),y=h.props,_=h.state,v)null==P.getDerivedStateFromProps&&null!=h.componentWillMount&&h.componentWillMount(),null!=h.componentDidMount&&h.__h.push(h.componentDidMount);else {if(null==P.getDerivedStateFromProps&&g!==y&&null!=h.componentWillReceiveProps&&h.componentWillReceiveProps(g,x),!h.__e&&null!=h.shouldComponentUpdate&&!1===h.shouldComponentUpdate(g,h.__s,x)||u.__v===i.__v){h.props=g,h.state=h.__s,u.__v!==i.__v&&(h.__d=!1),h.__v=u,u.__e=i.__e,u.__k=i.__k,h.__h.length&&f.push(h),T(u,e,l);break n}null!=h.componentWillUpdate&&h.componentWillUpdate(g,h.__s,x),null!=h.componentDidUpdate&&h.__h.push(function(){h.componentDidUpdate(y,_,w);});}h.context=x,h.props=g,h.state=h.__s,(a=n.__r)&&a(u),h.__d=!1,h.__v=u,h.__P=l,a=h.render(h.props,h.state,h.context),h.state=h.__s,null!=h.getChildContext&&(t=s(s({},t),h.getChildContext())),v||null==h.getSnapshotBeforeUpdate||(w=h.getSnapshotBeforeUpdate(y,_)),A=null!=a&&a.type==p&&null==a.key?a.props.children:a,m(l,Array.isArray(A)?A:[A],u,i,t,o,r,f,e,c),h.base=u.__e,u.__h=null,h.__h.length&&f.push(h),k&&(h.__E=h.__=null),h.__e=!1;}else null==r&&u.__v===i.__v?(u.__k=i.__k,u.__e=i.__e):u.__e=H(i.__e,u,i,t,o,r,f,c);(a=n.diffed)&&a(u);}catch(l){u.__v=null,(c||null!=r)&&(u.__e=e,u.__h=!!c,r[r.indexOf(e)]=null),n.__e(l,u,i);}return u.__e}function j(l,u){n.__c&&n.__c(u,l),l.some(function(u){try{l=u.__h,u.__h=[],l.some(function(n){n.call(u);});}catch(l){n.__e(l,u.__v);}});}function H(n,l,u,i,t,o,r,c){var s,a,h,v,y,p=u.props,d=l.props;if(t="svg"===l.type||t,null!=o)for(s=0;s3)for(u=[u],o=3;o (GFM Style)', + type: 'boolean' + }, + requireSpaceBeforeHeadingText: { + defaultValue: false, + description: 'Makes adding a space between `#` and the header text mandatory (GFM Style)', + type: 'boolean' + }, + ghMentions: { + defaultValue: false, + description: 'Enables github @mentions', + type: 'boolean' + }, + ghMentionsLink: { + defaultValue: 'https://github.com/{u}', + description: 'Changes the link generated by @mentions. Only applies if ghMentions option is enabled.', + type: 'string' + }, + encodeEmails: { + defaultValue: true, + description: 'Encode e-mail addresses through the use of Character Entities, transforming ASCII e-mail addresses into its equivalent decimal entities', + type: 'boolean' + }, + openLinksInNewWindow: { + defaultValue: false, + description: 'Open all links in new windows', + type: 'boolean' + }, + backslashEscapesHTMLTags: { + defaultValue: false, + description: 'Support for HTML Tag escaping. ex: \
foo\
', + type: 'boolean' + }, + emoji: { + defaultValue: false, + description: 'Enable emoji support. Ex: `this is a :smile: emoji`', + type: 'boolean' + }, + underline: { + defaultValue: false, + description: 'Enable support for underline. Syntax is double or triple underscores: `__underline word__`. With this option enabled, underscores no longer parses into `` and ``', + type: 'boolean' + }, + completeHTMLDocument: { + defaultValue: false, + description: 'Outputs a complete html document, including ``, `` and `` tags', + type: 'boolean' + }, + metadata: { + defaultValue: false, + description: 'Enable support for document metadata (defined at the top of the document between `«««` and `»»»` or between `---` and `---`).', + type: 'boolean' + }, + splitAdjacentBlockquotes: { + defaultValue: false, + description: 'Split adjacent blockquote blocks', + type: 'boolean' + } + }; + if (simple === false) { + return JSON.parse(JSON.stringify(defaultOptions)); + } + var ret = {}; + for (var opt in defaultOptions) { + if (defaultOptions.hasOwnProperty(opt)) { + ret[opt] = defaultOptions[opt].defaultValue; + } + } + return ret; +} + +function allOptionsOn () { + var options = getDefaultOpts(true), + ret = {}; + for (var opt in options) { + if (options.hasOwnProperty(opt)) { + ret[opt] = true; + } + } + return ret; +} + +/** + * Created by Tivie on 06-01-2015. + */ + +// Private properties +var showdown = {}, + parsers = {}, + extensions = {}, + globalOptions = getDefaultOpts(true), + setFlavor = 'vanilla', + flavor = { + github: { + omitExtraWLInCodeBlocks: true, + simplifiedAutoLink: true, + excludeTrailingPunctuationFromURLs: true, + literalMidWordUnderscores: true, + strikethrough: true, + tables: true, + tablesHeaderId: true, + ghCodeBlocks: true, + tasklists: true, + disableForced4SpacesIndentedSublists: true, + simpleLineBreaks: true, + requireSpaceBeforeHeadingText: true, + ghCompatibleHeaderId: true, + ghMentions: true, + backslashEscapesHTMLTags: true, + emoji: true, + splitAdjacentBlockquotes: true + }, + original: { + noHeaderId: true, + ghCodeBlocks: false + }, + ghost: { + omitExtraWLInCodeBlocks: true, + parseImgDimensions: true, + simplifiedAutoLink: true, + excludeTrailingPunctuationFromURLs: true, + literalMidWordUnderscores: true, + strikethrough: true, + tables: true, + tablesHeaderId: true, + ghCodeBlocks: true, + tasklists: true, + smoothLivePreview: true, + simpleLineBreaks: true, + requireSpaceBeforeHeadingText: true, + ghMentions: false, + encodeEmails: true + }, + vanilla: getDefaultOpts(true), + allOn: allOptionsOn() + }; + +/** + * helper namespace + * @type {{}} + */ +showdown.helper = {}; + +/** + * TODO LEGACY SUPPORT CODE + * @type {{}} + */ +showdown.extensions = {}; + +/** + * Set a global option + * @static + * @param {string} key + * @param {*} value + * @returns {showdown} + */ +showdown.setOption = function (key, value) { + globalOptions[key] = value; + return this; +}; + +/** + * Get a global option + * @static + * @param {string} key + * @returns {*} + */ +showdown.getOption = function (key) { + return globalOptions[key]; +}; + +/** + * Get the global options + * @static + * @returns {{}} + */ +showdown.getOptions = function () { + return globalOptions; +}; + +/** + * Reset global options to the default values + * @static + */ +showdown.resetOptions = function () { + globalOptions = getDefaultOpts(true); +}; + +/** + * Set the flavor showdown should use as default + * @param {string} name + */ +showdown.setFlavor = function (name) { + if (!flavor.hasOwnProperty(name)) { + throw Error(name + ' flavor was not found'); + } + showdown.resetOptions(); + var preset = flavor[name]; + setFlavor = name; + for (var option in preset) { + if (preset.hasOwnProperty(option)) { + globalOptions[option] = preset[option]; + } + } +}; + +/** + * Get the currently set flavor + * @returns {string} + */ +showdown.getFlavor = function () { + return setFlavor; +}; + +/** + * Get the options of a specified flavor. Returns undefined if the flavor was not found + * @param {string} name Name of the flavor + * @returns {{}|undefined} + */ +showdown.getFlavorOptions = function (name) { + if (flavor.hasOwnProperty(name)) { + return flavor[name]; + } +}; + +/** + * Get the default options + * @static + * @param {boolean} [simple=true] + * @returns {{}} + */ +showdown.getDefaultOptions = function (simple) { + return getDefaultOpts(simple); +}; + +/** + * Get or set a subParser + * + * subParser(name) - Get a registered subParser + * subParser(name, func) - Register a subParser + * @static + * @param {string} name + * @param {function} [func] + * @returns {*} + */ +showdown.subParser = function (name, func) { + if (showdown.helper.isString(name)) { + if (typeof func !== 'undefined') { + parsers[name] = func; + } else { + if (parsers.hasOwnProperty(name)) { + return parsers[name]; + } else { + throw Error('SubParser named ' + name + ' not registered!'); + } + } + } +}; + +/** + * Gets or registers an extension + * @static + * @param {string} name + * @param {object|function=} ext + * @returns {*} + */ +showdown.extension = function (name, ext) { + + if (!showdown.helper.isString(name)) { + throw Error('Extension \'name\' must be a string'); + } + + name = showdown.helper.stdExtName(name); + + // Getter + if (showdown.helper.isUndefined(ext)) { + if (!extensions.hasOwnProperty(name)) { + throw Error('Extension named ' + name + ' is not registered!'); + } + return extensions[name]; + + // Setter + } else { + // Expand extension if it's wrapped in a function + if (typeof ext === 'function') { + ext = ext(); + } + + // Ensure extension is an array + if (!showdown.helper.isArray(ext)) { + ext = [ext]; + } + + var validExtension = validate(ext, name); + + if (validExtension.valid) { + extensions[name] = ext; + } else { + throw Error(validExtension.error); + } + } +}; + +/** + * Gets all extensions registered + * @returns {{}} + */ +showdown.getAllExtensions = function () { + return extensions; +}; + +/** + * Remove an extension + * @param {string} name + */ +showdown.removeExtension = function (name) { + delete extensions[name]; +}; + +/** + * Removes all extensions + */ +showdown.resetExtensions = function () { + extensions = {}; +}; + +/** + * Validate extension + * @param {array} extension + * @param {string} name + * @returns {{valid: boolean, error: string}} + */ +function validate (extension, name) { + + var errMsg = (name) ? 'Error in ' + name + ' extension->' : 'Error in unnamed extension', + ret = { + valid: true, + error: '' + }; + + if (!showdown.helper.isArray(extension)) { + extension = [extension]; + } + + for (var i = 0; i < extension.length; ++i) { + var baseMsg = errMsg + ' sub-extension ' + i + ': ', + ext = extension[i]; + if (typeof ext !== 'object') { + ret.valid = false; + ret.error = baseMsg + 'must be an object, but ' + typeof ext + ' given'; + return ret; + } + + if (!showdown.helper.isString(ext.type)) { + ret.valid = false; + ret.error = baseMsg + 'property "type" must be a string, but ' + typeof ext.type + ' given'; + return ret; + } + + var type = ext.type = ext.type.toLowerCase(); + + // normalize extension type + if (type === 'language') { + type = ext.type = 'lang'; + } + + if (type === 'html') { + type = ext.type = 'output'; + } + + if (type !== 'lang' && type !== 'output' && type !== 'listener') { + ret.valid = false; + ret.error = baseMsg + 'type ' + type + ' is not recognized. Valid values: "lang/language", "output/html" or "listener"'; + return ret; + } + + if (type === 'listener') { + if (showdown.helper.isUndefined(ext.listeners)) { + ret.valid = false; + ret.error = baseMsg + '. Extensions of type "listener" must have a property called "listeners"'; + return ret; + } + } else { + if (showdown.helper.isUndefined(ext.filter) && showdown.helper.isUndefined(ext.regex)) { + ret.valid = false; + ret.error = baseMsg + type + ' extensions must define either a "regex" property or a "filter" method'; + return ret; + } + } + + if (ext.listeners) { + if (typeof ext.listeners !== 'object') { + ret.valid = false; + ret.error = baseMsg + '"listeners" property must be an object but ' + typeof ext.listeners + ' given'; + return ret; + } + for (var ln in ext.listeners) { + if (ext.listeners.hasOwnProperty(ln)) { + if (typeof ext.listeners[ln] !== 'function') { + ret.valid = false; + ret.error = baseMsg + '"listeners" property must be an hash of [event name]: [callback]. listeners.' + ln + + ' must be a function but ' + typeof ext.listeners[ln] + ' given'; + return ret; + } + } + } + } + + if (ext.filter) { + if (typeof ext.filter !== 'function') { + ret.valid = false; + ret.error = baseMsg + '"filter" must be a function, but ' + typeof ext.filter + ' given'; + return ret; + } + } else if (ext.regex) { + if (showdown.helper.isString(ext.regex)) { + ext.regex = new RegExp(ext.regex, 'g'); + } + if (!(ext.regex instanceof RegExp)) { + ret.valid = false; + ret.error = baseMsg + '"regex" property must either be a string or a RegExp object, but ' + typeof ext.regex + ' given'; + return ret; + } + if (showdown.helper.isUndefined(ext.replace)) { + ret.valid = false; + ret.error = baseMsg + '"regex" extensions must implement a replace string or function'; + return ret; + } + } + } + return ret; +} + +/** + * Validate extension + * @param {object} ext + * @returns {boolean} + */ +showdown.validateExtension = function (ext) { + + var validateExtension = validate(ext, null); + if (!validateExtension.valid) { + console.warn(validateExtension.error); + return false; + } + return true; +}; + +/** + * showdownjs helper functions + */ + +if (!showdown.hasOwnProperty('helper')) { + showdown.helper = {}; +} + +/** + * Check if var is string + * @static + * @param {string} a + * @returns {boolean} + */ +showdown.helper.isString = function (a) { + return (typeof a === 'string' || a instanceof String); +}; + +/** + * Check if var is a function + * @static + * @param {*} a + * @returns {boolean} + */ +showdown.helper.isFunction = function (a) { + var getType = {}; + return a && getType.toString.call(a) === '[object Function]'; +}; + +/** + * isArray helper function + * @static + * @param {*} a + * @returns {boolean} + */ +showdown.helper.isArray = function (a) { + return Array.isArray(a); +}; + +/** + * Check if value is undefined + * @static + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`. + */ +showdown.helper.isUndefined = function (value) { + return typeof value === 'undefined'; +}; + +/** + * ForEach helper function + * Iterates over Arrays and Objects (own properties only) + * @static + * @param {*} obj + * @param {function} callback Accepts 3 params: 1. value, 2. key, 3. the original array/object + */ +showdown.helper.forEach = function (obj, callback) { + // check if obj is defined + if (showdown.helper.isUndefined(obj)) { + throw new Error('obj param is required'); + } + + if (showdown.helper.isUndefined(callback)) { + throw new Error('callback param is required'); + } + + if (!showdown.helper.isFunction(callback)) { + throw new Error('callback param must be a function/closure'); + } + + if (typeof obj.forEach === 'function') { + obj.forEach(callback); + } else if (showdown.helper.isArray(obj)) { + for (var i = 0; i < obj.length; i++) { + callback(obj[i], i, obj); + } + } else if (typeof (obj) === 'object') { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + callback(obj[prop], prop, obj); + } + } + } else { + throw new Error('obj does not seem to be an array or an iterable object'); + } +}; + +/** + * Standardidize extension name + * @static + * @param {string} s extension name + * @returns {string} + */ +showdown.helper.stdExtName = function (s) { + return s.replace(/[_?*+\/\\.^-]/g, '').replace(/\s/g, '').toLowerCase(); +}; + +function escapeCharactersCallback (wholeMatch, m1) { + var charCodeToEscape = m1.charCodeAt(0); + return '¨E' + charCodeToEscape + 'E'; +} + +/** + * Callback used to escape characters when passing through String.replace + * @static + * @param {string} wholeMatch + * @param {string} m1 + * @returns {string} + */ +showdown.helper.escapeCharactersCallback = escapeCharactersCallback; + +/** + * Escape characters in a string + * @static + * @param {string} text + * @param {string} charsToEscape + * @param {boolean} afterBackslash + * @returns {XML|string|void|*} + */ +showdown.helper.escapeCharacters = function (text, charsToEscape, afterBackslash) { + // First we have to escape the escape characters so that + // we can build a character class out of them + var regexString = '([' + charsToEscape.replace(/([\[\]\\])/g, '\\$1') + '])'; + + if (afterBackslash) { + regexString = '\\\\' + regexString; + } + + var regex = new RegExp(regexString, 'g'); + text = text.replace(regex, escapeCharactersCallback); + + return text; +}; + +/** + * Unescape HTML entities + * @param txt + * @returns {string} + */ +showdown.helper.unescapeHTMLEntities = function (txt) { + + return txt + .replace(/"/g, '"') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&'); +}; + +var rgxFindMatchPos = function (str, left, right, flags) { + var f = flags || '', + g = f.indexOf('g') > -1, + x = new RegExp(left + '|' + right, 'g' + f.replace(/g/g, '')), + l = new RegExp(left, f.replace(/g/g, '')), + pos = [], + t, s, m, start, end; + + do { + t = 0; + while ((m = x.exec(str))) { + if (l.test(m[0])) { + if (!(t++)) { + s = x.lastIndex; + start = s - m[0].length; + } + } else if (t) { + if (!--t) { + end = m.index + m[0].length; + var obj = { + left: {start: start, end: s}, + match: {start: s, end: m.index}, + right: {start: m.index, end: end}, + wholeMatch: {start: start, end: end} + }; + pos.push(obj); + if (!g) { + return pos; + } + } + } + } + } while (t && (x.lastIndex = s)); + + return pos; +}; + +/** + * matchRecursiveRegExp + * + * (c) 2007 Steven Levithan + * MIT License + * + * Accepts a string to search, a left and right format delimiter + * as regex patterns, and optional regex flags. Returns an array + * of matches, allowing nested instances of left/right delimiters. + * Use the "g" flag to return all matches, otherwise only the + * first is returned. Be careful to ensure that the left and + * right format delimiters produce mutually exclusive matches. + * Backreferences are not supported within the right delimiter + * due to how it is internally combined with the left delimiter. + * When matching strings whose format delimiters are unbalanced + * to the left or right, the output is intentionally as a + * conventional regex library with recursion support would + * produce, e.g. "<" and ">" both produce ["x"] when using + * "<" and ">" as the delimiters (both strings contain a single, + * balanced instance of ""). + * + * examples: + * matchRecursiveRegExp("test", "\\(", "\\)") + * returns: [] + * matchRecursiveRegExp(">>t<>", "<", ">", "g") + * returns: ["t<>", ""] + * matchRecursiveRegExp("
test
", "]*>", "
", "gi") + * returns: ["test"] + */ +showdown.helper.matchRecursiveRegExp = function (str, left, right, flags) { + + var matchPos = rgxFindMatchPos (str, left, right, flags), + results = []; + + for (var i = 0; i < matchPos.length; ++i) { + results.push([ + str.slice(matchPos[i].wholeMatch.start, matchPos[i].wholeMatch.end), + str.slice(matchPos[i].match.start, matchPos[i].match.end), + str.slice(matchPos[i].left.start, matchPos[i].left.end), + str.slice(matchPos[i].right.start, matchPos[i].right.end) + ]); + } + return results; +}; + +/** + * + * @param {string} str + * @param {string|function} replacement + * @param {string} left + * @param {string} right + * @param {string} flags + * @returns {string} + */ +showdown.helper.replaceRecursiveRegExp = function (str, replacement, left, right, flags) { + + if (!showdown.helper.isFunction(replacement)) { + var repStr = replacement; + replacement = function () { + return repStr; + }; + } + + var matchPos = rgxFindMatchPos(str, left, right, flags), + finalStr = str, + lng = matchPos.length; + + if (lng > 0) { + var bits = []; + if (matchPos[0].wholeMatch.start !== 0) { + bits.push(str.slice(0, matchPos[0].wholeMatch.start)); + } + for (var i = 0; i < lng; ++i) { + bits.push( + replacement( + str.slice(matchPos[i].wholeMatch.start, matchPos[i].wholeMatch.end), + str.slice(matchPos[i].match.start, matchPos[i].match.end), + str.slice(matchPos[i].left.start, matchPos[i].left.end), + str.slice(matchPos[i].right.start, matchPos[i].right.end) + ) + ); + if (i < lng - 1) { + bits.push(str.slice(matchPos[i].wholeMatch.end, matchPos[i + 1].wholeMatch.start)); + } + } + if (matchPos[lng - 1].wholeMatch.end < str.length) { + bits.push(str.slice(matchPos[lng - 1].wholeMatch.end)); + } + finalStr = bits.join(''); + } + return finalStr; +}; + +/** + * Returns the index within the passed String object of the first occurrence of the specified regex, + * starting the search at fromIndex. Returns -1 if the value is not found. + * + * @param {string} str string to search + * @param {RegExp} regex Regular expression to search + * @param {int} [fromIndex = 0] Index to start the search + * @returns {Number} + * @throws InvalidArgumentError + */ +showdown.helper.regexIndexOf = function (str, regex, fromIndex) { + if (!showdown.helper.isString(str)) { + throw 'InvalidArgumentError: first parameter of showdown.helper.regexIndexOf function must be a string'; + } + if (regex instanceof RegExp === false) { + throw 'InvalidArgumentError: second parameter of showdown.helper.regexIndexOf function must be an instance of RegExp'; + } + var indexOf = str.substring(fromIndex || 0).search(regex); + return (indexOf >= 0) ? (indexOf + (fromIndex || 0)) : indexOf; +}; + +/** + * Splits the passed string object at the defined index, and returns an array composed of the two substrings + * @param {string} str string to split + * @param {int} index index to split string at + * @returns {[string,string]} + * @throws InvalidArgumentError + */ +showdown.helper.splitAtIndex = function (str, index) { + if (!showdown.helper.isString(str)) { + throw 'InvalidArgumentError: first parameter of showdown.helper.regexIndexOf function must be a string'; + } + return [str.substring(0, index), str.substring(index)]; +}; + +/** + * Obfuscate an e-mail address through the use of Character Entities, + * transforming ASCII characters into their equivalent decimal or hex entities. + * + * Since it has a random component, subsequent calls to this function produce different results + * + * @param {string} mail + * @returns {string} + */ +showdown.helper.encodeEmailAddress = function (mail) { + var encode = [ + function (ch) { + return '&#' + ch.charCodeAt(0) + ';'; + }, + function (ch) { + return '&#x' + ch.charCodeAt(0).toString(16) + ';'; + }, + function (ch) { + return ch; + } + ]; + + mail = mail.replace(/./g, function (ch) { + if (ch === '@') { + // this *must* be encoded. I insist. + ch = encode[Math.floor(Math.random() * 2)](ch); + } else { + var r = Math.random(); + // roughly 10% raw, 45% hex, 45% dec + ch = ( + r > 0.9 ? encode[2](ch) : r > 0.45 ? encode[1](ch) : encode[0](ch) + ); + } + return ch; + }); + + return mail; +}; + +/** + * + * @param str + * @param targetLength + * @param padString + * @returns {string} + */ +showdown.helper.padEnd = function padEnd (str, targetLength, padString) { + /*jshint bitwise: false*/ + // eslint-disable-next-line space-infix-ops + targetLength = targetLength>>0; //floor if number or convert non-number to 0; + /*jshint bitwise: true*/ + padString = String(padString || ' '); + if (str.length > targetLength) { + return String(str); + } else { + targetLength = targetLength - str.length; + if (targetLength > padString.length) { + padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed + } + return String(str) + padString.slice(0,targetLength); + } +}; + +/** + * POLYFILLS + */ +// use this instead of builtin is undefined for IE8 compatibility +if (typeof console === 'undefined') { + console = { + warn: function (msg) { + alert(msg); + }, + log: function (msg) { + alert(msg); + }, + error: function (msg) { + throw msg; + } + }; +} + +/** + * Common regexes. + * We declare some common regexes to improve performance + */ +showdown.helper.regexes = { + asteriskDashAndColon: /([*_:~])/g +}; + +/** + * EMOJIS LIST + */ +showdown.helper.emojis = { + '+1':'\ud83d\udc4d', + '-1':'\ud83d\udc4e', + '100':'\ud83d\udcaf', + '1234':'\ud83d\udd22', + '1st_place_medal':'\ud83e\udd47', + '2nd_place_medal':'\ud83e\udd48', + '3rd_place_medal':'\ud83e\udd49', + '8ball':'\ud83c\udfb1', + 'a':'\ud83c\udd70\ufe0f', + 'ab':'\ud83c\udd8e', + 'abc':'\ud83d\udd24', + 'abcd':'\ud83d\udd21', + 'accept':'\ud83c\ude51', + 'aerial_tramway':'\ud83d\udea1', + 'airplane':'\u2708\ufe0f', + 'alarm_clock':'\u23f0', + 'alembic':'\u2697\ufe0f', + 'alien':'\ud83d\udc7d', + 'ambulance':'\ud83d\ude91', + 'amphora':'\ud83c\udffa', + 'anchor':'\u2693\ufe0f', + 'angel':'\ud83d\udc7c', + 'anger':'\ud83d\udca2', + 'angry':'\ud83d\ude20', + 'anguished':'\ud83d\ude27', + 'ant':'\ud83d\udc1c', + 'apple':'\ud83c\udf4e', + 'aquarius':'\u2652\ufe0f', + 'aries':'\u2648\ufe0f', + 'arrow_backward':'\u25c0\ufe0f', + 'arrow_double_down':'\u23ec', + 'arrow_double_up':'\u23eb', + 'arrow_down':'\u2b07\ufe0f', + 'arrow_down_small':'\ud83d\udd3d', + 'arrow_forward':'\u25b6\ufe0f', + 'arrow_heading_down':'\u2935\ufe0f', + 'arrow_heading_up':'\u2934\ufe0f', + 'arrow_left':'\u2b05\ufe0f', + 'arrow_lower_left':'\u2199\ufe0f', + 'arrow_lower_right':'\u2198\ufe0f', + 'arrow_right':'\u27a1\ufe0f', + 'arrow_right_hook':'\u21aa\ufe0f', + 'arrow_up':'\u2b06\ufe0f', + 'arrow_up_down':'\u2195\ufe0f', + 'arrow_up_small':'\ud83d\udd3c', + 'arrow_upper_left':'\u2196\ufe0f', + 'arrow_upper_right':'\u2197\ufe0f', + 'arrows_clockwise':'\ud83d\udd03', + 'arrows_counterclockwise':'\ud83d\udd04', + 'art':'\ud83c\udfa8', + 'articulated_lorry':'\ud83d\ude9b', + 'artificial_satellite':'\ud83d\udef0', + 'astonished':'\ud83d\ude32', + 'athletic_shoe':'\ud83d\udc5f', + 'atm':'\ud83c\udfe7', + 'atom_symbol':'\u269b\ufe0f', + 'avocado':'\ud83e\udd51', + 'b':'\ud83c\udd71\ufe0f', + 'baby':'\ud83d\udc76', + 'baby_bottle':'\ud83c\udf7c', + 'baby_chick':'\ud83d\udc24', + 'baby_symbol':'\ud83d\udebc', + 'back':'\ud83d\udd19', + 'bacon':'\ud83e\udd53', + 'badminton':'\ud83c\udff8', + 'baggage_claim':'\ud83d\udec4', + 'baguette_bread':'\ud83e\udd56', + 'balance_scale':'\u2696\ufe0f', + 'balloon':'\ud83c\udf88', + 'ballot_box':'\ud83d\uddf3', + 'ballot_box_with_check':'\u2611\ufe0f', + 'bamboo':'\ud83c\udf8d', + 'banana':'\ud83c\udf4c', + 'bangbang':'\u203c\ufe0f', + 'bank':'\ud83c\udfe6', + 'bar_chart':'\ud83d\udcca', + 'barber':'\ud83d\udc88', + 'baseball':'\u26be\ufe0f', + 'basketball':'\ud83c\udfc0', + 'basketball_man':'\u26f9\ufe0f', + 'basketball_woman':'\u26f9\ufe0f‍\u2640\ufe0f', + 'bat':'\ud83e\udd87', + 'bath':'\ud83d\udec0', + 'bathtub':'\ud83d\udec1', + 'battery':'\ud83d\udd0b', + 'beach_umbrella':'\ud83c\udfd6', + 'bear':'\ud83d\udc3b', + 'bed':'\ud83d\udecf', + 'bee':'\ud83d\udc1d', + 'beer':'\ud83c\udf7a', + 'beers':'\ud83c\udf7b', + 'beetle':'\ud83d\udc1e', + 'beginner':'\ud83d\udd30', + 'bell':'\ud83d\udd14', + 'bellhop_bell':'\ud83d\udece', + 'bento':'\ud83c\udf71', + 'biking_man':'\ud83d\udeb4', + 'bike':'\ud83d\udeb2', + 'biking_woman':'\ud83d\udeb4‍\u2640\ufe0f', + 'bikini':'\ud83d\udc59', + 'biohazard':'\u2623\ufe0f', + 'bird':'\ud83d\udc26', + 'birthday':'\ud83c\udf82', + 'black_circle':'\u26ab\ufe0f', + 'black_flag':'\ud83c\udff4', + 'black_heart':'\ud83d\udda4', + 'black_joker':'\ud83c\udccf', + 'black_large_square':'\u2b1b\ufe0f', + 'black_medium_small_square':'\u25fe\ufe0f', + 'black_medium_square':'\u25fc\ufe0f', + 'black_nib':'\u2712\ufe0f', + 'black_small_square':'\u25aa\ufe0f', + 'black_square_button':'\ud83d\udd32', + 'blonde_man':'\ud83d\udc71', + 'blonde_woman':'\ud83d\udc71‍\u2640\ufe0f', + 'blossom':'\ud83c\udf3c', + 'blowfish':'\ud83d\udc21', + 'blue_book':'\ud83d\udcd8', + 'blue_car':'\ud83d\ude99', + 'blue_heart':'\ud83d\udc99', + 'blush':'\ud83d\ude0a', + 'boar':'\ud83d\udc17', + 'boat':'\u26f5\ufe0f', + 'bomb':'\ud83d\udca3', + 'book':'\ud83d\udcd6', + 'bookmark':'\ud83d\udd16', + 'bookmark_tabs':'\ud83d\udcd1', + 'books':'\ud83d\udcda', + 'boom':'\ud83d\udca5', + 'boot':'\ud83d\udc62', + 'bouquet':'\ud83d\udc90', + 'bowing_man':'\ud83d\ude47', + 'bow_and_arrow':'\ud83c\udff9', + 'bowing_woman':'\ud83d\ude47‍\u2640\ufe0f', + 'bowling':'\ud83c\udfb3', + 'boxing_glove':'\ud83e\udd4a', + 'boy':'\ud83d\udc66', + 'bread':'\ud83c\udf5e', + 'bride_with_veil':'\ud83d\udc70', + 'bridge_at_night':'\ud83c\udf09', + 'briefcase':'\ud83d\udcbc', + 'broken_heart':'\ud83d\udc94', + 'bug':'\ud83d\udc1b', + 'building_construction':'\ud83c\udfd7', + 'bulb':'\ud83d\udca1', + 'bullettrain_front':'\ud83d\ude85', + 'bullettrain_side':'\ud83d\ude84', + 'burrito':'\ud83c\udf2f', + 'bus':'\ud83d\ude8c', + 'business_suit_levitating':'\ud83d\udd74', + 'busstop':'\ud83d\ude8f', + 'bust_in_silhouette':'\ud83d\udc64', + 'busts_in_silhouette':'\ud83d\udc65', + 'butterfly':'\ud83e\udd8b', + 'cactus':'\ud83c\udf35', + 'cake':'\ud83c\udf70', + 'calendar':'\ud83d\udcc6', + 'call_me_hand':'\ud83e\udd19', + 'calling':'\ud83d\udcf2', + 'camel':'\ud83d\udc2b', + 'camera':'\ud83d\udcf7', + 'camera_flash':'\ud83d\udcf8', + 'camping':'\ud83c\udfd5', + 'cancer':'\u264b\ufe0f', + 'candle':'\ud83d\udd6f', + 'candy':'\ud83c\udf6c', + 'canoe':'\ud83d\udef6', + 'capital_abcd':'\ud83d\udd20', + 'capricorn':'\u2651\ufe0f', + 'car':'\ud83d\ude97', + 'card_file_box':'\ud83d\uddc3', + 'card_index':'\ud83d\udcc7', + 'card_index_dividers':'\ud83d\uddc2', + 'carousel_horse':'\ud83c\udfa0', + 'carrot':'\ud83e\udd55', + 'cat':'\ud83d\udc31', + 'cat2':'\ud83d\udc08', + 'cd':'\ud83d\udcbf', + 'chains':'\u26d3', + 'champagne':'\ud83c\udf7e', + 'chart':'\ud83d\udcb9', + 'chart_with_downwards_trend':'\ud83d\udcc9', + 'chart_with_upwards_trend':'\ud83d\udcc8', + 'checkered_flag':'\ud83c\udfc1', + 'cheese':'\ud83e\uddc0', + 'cherries':'\ud83c\udf52', + 'cherry_blossom':'\ud83c\udf38', + 'chestnut':'\ud83c\udf30', + 'chicken':'\ud83d\udc14', + 'children_crossing':'\ud83d\udeb8', + 'chipmunk':'\ud83d\udc3f', + 'chocolate_bar':'\ud83c\udf6b', + 'christmas_tree':'\ud83c\udf84', + 'church':'\u26ea\ufe0f', + 'cinema':'\ud83c\udfa6', + 'circus_tent':'\ud83c\udfaa', + 'city_sunrise':'\ud83c\udf07', + 'city_sunset':'\ud83c\udf06', + 'cityscape':'\ud83c\udfd9', + 'cl':'\ud83c\udd91', + 'clamp':'\ud83d\udddc', + 'clap':'\ud83d\udc4f', + 'clapper':'\ud83c\udfac', + 'classical_building':'\ud83c\udfdb', + 'clinking_glasses':'\ud83e\udd42', + 'clipboard':'\ud83d\udccb', + 'clock1':'\ud83d\udd50', + 'clock10':'\ud83d\udd59', + 'clock1030':'\ud83d\udd65', + 'clock11':'\ud83d\udd5a', + 'clock1130':'\ud83d\udd66', + 'clock12':'\ud83d\udd5b', + 'clock1230':'\ud83d\udd67', + 'clock130':'\ud83d\udd5c', + 'clock2':'\ud83d\udd51', + 'clock230':'\ud83d\udd5d', + 'clock3':'\ud83d\udd52', + 'clock330':'\ud83d\udd5e', + 'clock4':'\ud83d\udd53', + 'clock430':'\ud83d\udd5f', + 'clock5':'\ud83d\udd54', + 'clock530':'\ud83d\udd60', + 'clock6':'\ud83d\udd55', + 'clock630':'\ud83d\udd61', + 'clock7':'\ud83d\udd56', + 'clock730':'\ud83d\udd62', + 'clock8':'\ud83d\udd57', + 'clock830':'\ud83d\udd63', + 'clock9':'\ud83d\udd58', + 'clock930':'\ud83d\udd64', + 'closed_book':'\ud83d\udcd5', + 'closed_lock_with_key':'\ud83d\udd10', + 'closed_umbrella':'\ud83c\udf02', + 'cloud':'\u2601\ufe0f', + 'cloud_with_lightning':'\ud83c\udf29', + 'cloud_with_lightning_and_rain':'\u26c8', + 'cloud_with_rain':'\ud83c\udf27', + 'cloud_with_snow':'\ud83c\udf28', + 'clown_face':'\ud83e\udd21', + 'clubs':'\u2663\ufe0f', + 'cocktail':'\ud83c\udf78', + 'coffee':'\u2615\ufe0f', + 'coffin':'\u26b0\ufe0f', + 'cold_sweat':'\ud83d\ude30', + 'comet':'\u2604\ufe0f', + 'computer':'\ud83d\udcbb', + 'computer_mouse':'\ud83d\uddb1', + 'confetti_ball':'\ud83c\udf8a', + 'confounded':'\ud83d\ude16', + 'confused':'\ud83d\ude15', + 'congratulations':'\u3297\ufe0f', + 'construction':'\ud83d\udea7', + 'construction_worker_man':'\ud83d\udc77', + 'construction_worker_woman':'\ud83d\udc77‍\u2640\ufe0f', + 'control_knobs':'\ud83c\udf9b', + 'convenience_store':'\ud83c\udfea', + 'cookie':'\ud83c\udf6a', + 'cool':'\ud83c\udd92', + 'policeman':'\ud83d\udc6e', + 'copyright':'\u00a9\ufe0f', + 'corn':'\ud83c\udf3d', + 'couch_and_lamp':'\ud83d\udecb', + 'couple':'\ud83d\udc6b', + 'couple_with_heart_woman_man':'\ud83d\udc91', + 'couple_with_heart_man_man':'\ud83d\udc68‍\u2764\ufe0f‍\ud83d\udc68', + 'couple_with_heart_woman_woman':'\ud83d\udc69‍\u2764\ufe0f‍\ud83d\udc69', + 'couplekiss_man_man':'\ud83d\udc68‍\u2764\ufe0f‍\ud83d\udc8b‍\ud83d\udc68', + 'couplekiss_man_woman':'\ud83d\udc8f', + 'couplekiss_woman_woman':'\ud83d\udc69‍\u2764\ufe0f‍\ud83d\udc8b‍\ud83d\udc69', + 'cow':'\ud83d\udc2e', + 'cow2':'\ud83d\udc04', + 'cowboy_hat_face':'\ud83e\udd20', + 'crab':'\ud83e\udd80', + 'crayon':'\ud83d\udd8d', + 'credit_card':'\ud83d\udcb3', + 'crescent_moon':'\ud83c\udf19', + 'cricket':'\ud83c\udfcf', + 'crocodile':'\ud83d\udc0a', + 'croissant':'\ud83e\udd50', + 'crossed_fingers':'\ud83e\udd1e', + 'crossed_flags':'\ud83c\udf8c', + 'crossed_swords':'\u2694\ufe0f', + 'crown':'\ud83d\udc51', + 'cry':'\ud83d\ude22', + 'crying_cat_face':'\ud83d\ude3f', + 'crystal_ball':'\ud83d\udd2e', + 'cucumber':'\ud83e\udd52', + 'cupid':'\ud83d\udc98', + 'curly_loop':'\u27b0', + 'currency_exchange':'\ud83d\udcb1', + 'curry':'\ud83c\udf5b', + 'custard':'\ud83c\udf6e', + 'customs':'\ud83d\udec3', + 'cyclone':'\ud83c\udf00', + 'dagger':'\ud83d\udde1', + 'dancer':'\ud83d\udc83', + 'dancing_women':'\ud83d\udc6f', + 'dancing_men':'\ud83d\udc6f‍\u2642\ufe0f', + 'dango':'\ud83c\udf61', + 'dark_sunglasses':'\ud83d\udd76', + 'dart':'\ud83c\udfaf', + 'dash':'\ud83d\udca8', + 'date':'\ud83d\udcc5', + 'deciduous_tree':'\ud83c\udf33', + 'deer':'\ud83e\udd8c', + 'department_store':'\ud83c\udfec', + 'derelict_house':'\ud83c\udfda', + 'desert':'\ud83c\udfdc', + 'desert_island':'\ud83c\udfdd', + 'desktop_computer':'\ud83d\udda5', + 'male_detective':'\ud83d\udd75\ufe0f', + 'diamond_shape_with_a_dot_inside':'\ud83d\udca0', + 'diamonds':'\u2666\ufe0f', + 'disappointed':'\ud83d\ude1e', + 'disappointed_relieved':'\ud83d\ude25', + 'dizzy':'\ud83d\udcab', + 'dizzy_face':'\ud83d\ude35', + 'do_not_litter':'\ud83d\udeaf', + 'dog':'\ud83d\udc36', + 'dog2':'\ud83d\udc15', + 'dollar':'\ud83d\udcb5', + 'dolls':'\ud83c\udf8e', + 'dolphin':'\ud83d\udc2c', + 'door':'\ud83d\udeaa', + 'doughnut':'\ud83c\udf69', + 'dove':'\ud83d\udd4a', + 'dragon':'\ud83d\udc09', + 'dragon_face':'\ud83d\udc32', + 'dress':'\ud83d\udc57', + 'dromedary_camel':'\ud83d\udc2a', + 'drooling_face':'\ud83e\udd24', + 'droplet':'\ud83d\udca7', + 'drum':'\ud83e\udd41', + 'duck':'\ud83e\udd86', + 'dvd':'\ud83d\udcc0', + 'e-mail':'\ud83d\udce7', + 'eagle':'\ud83e\udd85', + 'ear':'\ud83d\udc42', + 'ear_of_rice':'\ud83c\udf3e', + 'earth_africa':'\ud83c\udf0d', + 'earth_americas':'\ud83c\udf0e', + 'earth_asia':'\ud83c\udf0f', + 'egg':'\ud83e\udd5a', + 'eggplant':'\ud83c\udf46', + 'eight_pointed_black_star':'\u2734\ufe0f', + 'eight_spoked_asterisk':'\u2733\ufe0f', + 'electric_plug':'\ud83d\udd0c', + 'elephant':'\ud83d\udc18', + 'email':'\u2709\ufe0f', + 'end':'\ud83d\udd1a', + 'envelope_with_arrow':'\ud83d\udce9', + 'euro':'\ud83d\udcb6', + 'european_castle':'\ud83c\udff0', + 'european_post_office':'\ud83c\udfe4', + 'evergreen_tree':'\ud83c\udf32', + 'exclamation':'\u2757\ufe0f', + 'expressionless':'\ud83d\ude11', + 'eye':'\ud83d\udc41', + 'eye_speech_bubble':'\ud83d\udc41‍\ud83d\udde8', + 'eyeglasses':'\ud83d\udc53', + 'eyes':'\ud83d\udc40', + 'face_with_head_bandage':'\ud83e\udd15', + 'face_with_thermometer':'\ud83e\udd12', + 'fist_oncoming':'\ud83d\udc4a', + 'factory':'\ud83c\udfed', + 'fallen_leaf':'\ud83c\udf42', + 'family_man_woman_boy':'\ud83d\udc6a', + 'family_man_boy':'\ud83d\udc68‍\ud83d\udc66', + 'family_man_boy_boy':'\ud83d\udc68‍\ud83d\udc66‍\ud83d\udc66', + 'family_man_girl':'\ud83d\udc68‍\ud83d\udc67', + 'family_man_girl_boy':'\ud83d\udc68‍\ud83d\udc67‍\ud83d\udc66', + 'family_man_girl_girl':'\ud83d\udc68‍\ud83d\udc67‍\ud83d\udc67', + 'family_man_man_boy':'\ud83d\udc68‍\ud83d\udc68‍\ud83d\udc66', + 'family_man_man_boy_boy':'\ud83d\udc68‍\ud83d\udc68‍\ud83d\udc66‍\ud83d\udc66', + 'family_man_man_girl':'\ud83d\udc68‍\ud83d\udc68‍\ud83d\udc67', + 'family_man_man_girl_boy':'\ud83d\udc68‍\ud83d\udc68‍\ud83d\udc67‍\ud83d\udc66', + 'family_man_man_girl_girl':'\ud83d\udc68‍\ud83d\udc68‍\ud83d\udc67‍\ud83d\udc67', + 'family_man_woman_boy_boy':'\ud83d\udc68‍\ud83d\udc69‍\ud83d\udc66‍\ud83d\udc66', + 'family_man_woman_girl':'\ud83d\udc68‍\ud83d\udc69‍\ud83d\udc67', + 'family_man_woman_girl_boy':'\ud83d\udc68‍\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc66', + 'family_man_woman_girl_girl':'\ud83d\udc68‍\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc67', + 'family_woman_boy':'\ud83d\udc69‍\ud83d\udc66', + 'family_woman_boy_boy':'\ud83d\udc69‍\ud83d\udc66‍\ud83d\udc66', + 'family_woman_girl':'\ud83d\udc69‍\ud83d\udc67', + 'family_woman_girl_boy':'\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc66', + 'family_woman_girl_girl':'\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc67', + 'family_woman_woman_boy':'\ud83d\udc69‍\ud83d\udc69‍\ud83d\udc66', + 'family_woman_woman_boy_boy':'\ud83d\udc69‍\ud83d\udc69‍\ud83d\udc66‍\ud83d\udc66', + 'family_woman_woman_girl':'\ud83d\udc69‍\ud83d\udc69‍\ud83d\udc67', + 'family_woman_woman_girl_boy':'\ud83d\udc69‍\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc66', + 'family_woman_woman_girl_girl':'\ud83d\udc69‍\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc67', + 'fast_forward':'\u23e9', + 'fax':'\ud83d\udce0', + 'fearful':'\ud83d\ude28', + 'feet':'\ud83d\udc3e', + 'female_detective':'\ud83d\udd75\ufe0f‍\u2640\ufe0f', + 'ferris_wheel':'\ud83c\udfa1', + 'ferry':'\u26f4', + 'field_hockey':'\ud83c\udfd1', + 'file_cabinet':'\ud83d\uddc4', + 'file_folder':'\ud83d\udcc1', + 'film_projector':'\ud83d\udcfd', + 'film_strip':'\ud83c\udf9e', + 'fire':'\ud83d\udd25', + 'fire_engine':'\ud83d\ude92', + 'fireworks':'\ud83c\udf86', + 'first_quarter_moon':'\ud83c\udf13', + 'first_quarter_moon_with_face':'\ud83c\udf1b', + 'fish':'\ud83d\udc1f', + 'fish_cake':'\ud83c\udf65', + 'fishing_pole_and_fish':'\ud83c\udfa3', + 'fist_raised':'\u270a', + 'fist_left':'\ud83e\udd1b', + 'fist_right':'\ud83e\udd1c', + 'flags':'\ud83c\udf8f', + 'flashlight':'\ud83d\udd26', + 'fleur_de_lis':'\u269c\ufe0f', + 'flight_arrival':'\ud83d\udeec', + 'flight_departure':'\ud83d\udeeb', + 'floppy_disk':'\ud83d\udcbe', + 'flower_playing_cards':'\ud83c\udfb4', + 'flushed':'\ud83d\ude33', + 'fog':'\ud83c\udf2b', + 'foggy':'\ud83c\udf01', + 'football':'\ud83c\udfc8', + 'footprints':'\ud83d\udc63', + 'fork_and_knife':'\ud83c\udf74', + 'fountain':'\u26f2\ufe0f', + 'fountain_pen':'\ud83d\udd8b', + 'four_leaf_clover':'\ud83c\udf40', + 'fox_face':'\ud83e\udd8a', + 'framed_picture':'\ud83d\uddbc', + 'free':'\ud83c\udd93', + 'fried_egg':'\ud83c\udf73', + 'fried_shrimp':'\ud83c\udf64', + 'fries':'\ud83c\udf5f', + 'frog':'\ud83d\udc38', + 'frowning':'\ud83d\ude26', + 'frowning_face':'\u2639\ufe0f', + 'frowning_man':'\ud83d\ude4d‍\u2642\ufe0f', + 'frowning_woman':'\ud83d\ude4d', + 'middle_finger':'\ud83d\udd95', + 'fuelpump':'\u26fd\ufe0f', + 'full_moon':'\ud83c\udf15', + 'full_moon_with_face':'\ud83c\udf1d', + 'funeral_urn':'\u26b1\ufe0f', + 'game_die':'\ud83c\udfb2', + 'gear':'\u2699\ufe0f', + 'gem':'\ud83d\udc8e', + 'gemini':'\u264a\ufe0f', + 'ghost':'\ud83d\udc7b', + 'gift':'\ud83c\udf81', + 'gift_heart':'\ud83d\udc9d', + 'girl':'\ud83d\udc67', + 'globe_with_meridians':'\ud83c\udf10', + 'goal_net':'\ud83e\udd45', + 'goat':'\ud83d\udc10', + 'golf':'\u26f3\ufe0f', + 'golfing_man':'\ud83c\udfcc\ufe0f', + 'golfing_woman':'\ud83c\udfcc\ufe0f‍\u2640\ufe0f', + 'gorilla':'\ud83e\udd8d', + 'grapes':'\ud83c\udf47', + 'green_apple':'\ud83c\udf4f', + 'green_book':'\ud83d\udcd7', + 'green_heart':'\ud83d\udc9a', + 'green_salad':'\ud83e\udd57', + 'grey_exclamation':'\u2755', + 'grey_question':'\u2754', + 'grimacing':'\ud83d\ude2c', + 'grin':'\ud83d\ude01', + 'grinning':'\ud83d\ude00', + 'guardsman':'\ud83d\udc82', + 'guardswoman':'\ud83d\udc82‍\u2640\ufe0f', + 'guitar':'\ud83c\udfb8', + 'gun':'\ud83d\udd2b', + 'haircut_woman':'\ud83d\udc87', + 'haircut_man':'\ud83d\udc87‍\u2642\ufe0f', + 'hamburger':'\ud83c\udf54', + 'hammer':'\ud83d\udd28', + 'hammer_and_pick':'\u2692', + 'hammer_and_wrench':'\ud83d\udee0', + 'hamster':'\ud83d\udc39', + 'hand':'\u270b', + 'handbag':'\ud83d\udc5c', + 'handshake':'\ud83e\udd1d', + 'hankey':'\ud83d\udca9', + 'hatched_chick':'\ud83d\udc25', + 'hatching_chick':'\ud83d\udc23', + 'headphones':'\ud83c\udfa7', + 'hear_no_evil':'\ud83d\ude49', + 'heart':'\u2764\ufe0f', + 'heart_decoration':'\ud83d\udc9f', + 'heart_eyes':'\ud83d\ude0d', + 'heart_eyes_cat':'\ud83d\ude3b', + 'heartbeat':'\ud83d\udc93', + 'heartpulse':'\ud83d\udc97', + 'hearts':'\u2665\ufe0f', + 'heavy_check_mark':'\u2714\ufe0f', + 'heavy_division_sign':'\u2797', + 'heavy_dollar_sign':'\ud83d\udcb2', + 'heavy_heart_exclamation':'\u2763\ufe0f', + 'heavy_minus_sign':'\u2796', + 'heavy_multiplication_x':'\u2716\ufe0f', + 'heavy_plus_sign':'\u2795', + 'helicopter':'\ud83d\ude81', + 'herb':'\ud83c\udf3f', + 'hibiscus':'\ud83c\udf3a', + 'high_brightness':'\ud83d\udd06', + 'high_heel':'\ud83d\udc60', + 'hocho':'\ud83d\udd2a', + 'hole':'\ud83d\udd73', + 'honey_pot':'\ud83c\udf6f', + 'horse':'\ud83d\udc34', + 'horse_racing':'\ud83c\udfc7', + 'hospital':'\ud83c\udfe5', + 'hot_pepper':'\ud83c\udf36', + 'hotdog':'\ud83c\udf2d', + 'hotel':'\ud83c\udfe8', + 'hotsprings':'\u2668\ufe0f', + 'hourglass':'\u231b\ufe0f', + 'hourglass_flowing_sand':'\u23f3', + 'house':'\ud83c\udfe0', + 'house_with_garden':'\ud83c\udfe1', + 'houses':'\ud83c\udfd8', + 'hugs':'\ud83e\udd17', + 'hushed':'\ud83d\ude2f', + 'ice_cream':'\ud83c\udf68', + 'ice_hockey':'\ud83c\udfd2', + 'ice_skate':'\u26f8', + 'icecream':'\ud83c\udf66', + 'id':'\ud83c\udd94', + 'ideograph_advantage':'\ud83c\ude50', + 'imp':'\ud83d\udc7f', + 'inbox_tray':'\ud83d\udce5', + 'incoming_envelope':'\ud83d\udce8', + 'tipping_hand_woman':'\ud83d\udc81', + 'information_source':'\u2139\ufe0f', + 'innocent':'\ud83d\ude07', + 'interrobang':'\u2049\ufe0f', + 'iphone':'\ud83d\udcf1', + 'izakaya_lantern':'\ud83c\udfee', + 'jack_o_lantern':'\ud83c\udf83', + 'japan':'\ud83d\uddfe', + 'japanese_castle':'\ud83c\udfef', + 'japanese_goblin':'\ud83d\udc7a', + 'japanese_ogre':'\ud83d\udc79', + 'jeans':'\ud83d\udc56', + 'joy':'\ud83d\ude02', + 'joy_cat':'\ud83d\ude39', + 'joystick':'\ud83d\udd79', + 'kaaba':'\ud83d\udd4b', + 'key':'\ud83d\udd11', + 'keyboard':'\u2328\ufe0f', + 'keycap_ten':'\ud83d\udd1f', + 'kick_scooter':'\ud83d\udef4', + 'kimono':'\ud83d\udc58', + 'kiss':'\ud83d\udc8b', + 'kissing':'\ud83d\ude17', + 'kissing_cat':'\ud83d\ude3d', + 'kissing_closed_eyes':'\ud83d\ude1a', + 'kissing_heart':'\ud83d\ude18', + 'kissing_smiling_eyes':'\ud83d\ude19', + 'kiwi_fruit':'\ud83e\udd5d', + 'koala':'\ud83d\udc28', + 'koko':'\ud83c\ude01', + 'label':'\ud83c\udff7', + 'large_blue_circle':'\ud83d\udd35', + 'large_blue_diamond':'\ud83d\udd37', + 'large_orange_diamond':'\ud83d\udd36', + 'last_quarter_moon':'\ud83c\udf17', + 'last_quarter_moon_with_face':'\ud83c\udf1c', + 'latin_cross':'\u271d\ufe0f', + 'laughing':'\ud83d\ude06', + 'leaves':'\ud83c\udf43', + 'ledger':'\ud83d\udcd2', + 'left_luggage':'\ud83d\udec5', + 'left_right_arrow':'\u2194\ufe0f', + 'leftwards_arrow_with_hook':'\u21a9\ufe0f', + 'lemon':'\ud83c\udf4b', + 'leo':'\u264c\ufe0f', + 'leopard':'\ud83d\udc06', + 'level_slider':'\ud83c\udf9a', + 'libra':'\u264e\ufe0f', + 'light_rail':'\ud83d\ude88', + 'link':'\ud83d\udd17', + 'lion':'\ud83e\udd81', + 'lips':'\ud83d\udc44', + 'lipstick':'\ud83d\udc84', + 'lizard':'\ud83e\udd8e', + 'lock':'\ud83d\udd12', + 'lock_with_ink_pen':'\ud83d\udd0f', + 'lollipop':'\ud83c\udf6d', + 'loop':'\u27bf', + 'loud_sound':'\ud83d\udd0a', + 'loudspeaker':'\ud83d\udce2', + 'love_hotel':'\ud83c\udfe9', + 'love_letter':'\ud83d\udc8c', + 'low_brightness':'\ud83d\udd05', + 'lying_face':'\ud83e\udd25', + 'm':'\u24c2\ufe0f', + 'mag':'\ud83d\udd0d', + 'mag_right':'\ud83d\udd0e', + 'mahjong':'\ud83c\udc04\ufe0f', + 'mailbox':'\ud83d\udceb', + 'mailbox_closed':'\ud83d\udcea', + 'mailbox_with_mail':'\ud83d\udcec', + 'mailbox_with_no_mail':'\ud83d\udced', + 'man':'\ud83d\udc68', + 'man_artist':'\ud83d\udc68‍\ud83c\udfa8', + 'man_astronaut':'\ud83d\udc68‍\ud83d\ude80', + 'man_cartwheeling':'\ud83e\udd38‍\u2642\ufe0f', + 'man_cook':'\ud83d\udc68‍\ud83c\udf73', + 'man_dancing':'\ud83d\udd7a', + 'man_facepalming':'\ud83e\udd26‍\u2642\ufe0f', + 'man_factory_worker':'\ud83d\udc68‍\ud83c\udfed', + 'man_farmer':'\ud83d\udc68‍\ud83c\udf3e', + 'man_firefighter':'\ud83d\udc68‍\ud83d\ude92', + 'man_health_worker':'\ud83d\udc68‍\u2695\ufe0f', + 'man_in_tuxedo':'\ud83e\udd35', + 'man_judge':'\ud83d\udc68‍\u2696\ufe0f', + 'man_juggling':'\ud83e\udd39‍\u2642\ufe0f', + 'man_mechanic':'\ud83d\udc68‍\ud83d\udd27', + 'man_office_worker':'\ud83d\udc68‍\ud83d\udcbc', + 'man_pilot':'\ud83d\udc68‍\u2708\ufe0f', + 'man_playing_handball':'\ud83e\udd3e‍\u2642\ufe0f', + 'man_playing_water_polo':'\ud83e\udd3d‍\u2642\ufe0f', + 'man_scientist':'\ud83d\udc68‍\ud83d\udd2c', + 'man_shrugging':'\ud83e\udd37‍\u2642\ufe0f', + 'man_singer':'\ud83d\udc68‍\ud83c\udfa4', + 'man_student':'\ud83d\udc68‍\ud83c\udf93', + 'man_teacher':'\ud83d\udc68‍\ud83c\udfeb', + 'man_technologist':'\ud83d\udc68‍\ud83d\udcbb', + 'man_with_gua_pi_mao':'\ud83d\udc72', + 'man_with_turban':'\ud83d\udc73', + 'tangerine':'\ud83c\udf4a', + 'mans_shoe':'\ud83d\udc5e', + 'mantelpiece_clock':'\ud83d\udd70', + 'maple_leaf':'\ud83c\udf41', + 'martial_arts_uniform':'\ud83e\udd4b', + 'mask':'\ud83d\ude37', + 'massage_woman':'\ud83d\udc86', + 'massage_man':'\ud83d\udc86‍\u2642\ufe0f', + 'meat_on_bone':'\ud83c\udf56', + 'medal_military':'\ud83c\udf96', + 'medal_sports':'\ud83c\udfc5', + 'mega':'\ud83d\udce3', + 'melon':'\ud83c\udf48', + 'memo':'\ud83d\udcdd', + 'men_wrestling':'\ud83e\udd3c‍\u2642\ufe0f', + 'menorah':'\ud83d\udd4e', + 'mens':'\ud83d\udeb9', + 'metal':'\ud83e\udd18', + 'metro':'\ud83d\ude87', + 'microphone':'\ud83c\udfa4', + 'microscope':'\ud83d\udd2c', + 'milk_glass':'\ud83e\udd5b', + 'milky_way':'\ud83c\udf0c', + 'minibus':'\ud83d\ude90', + 'minidisc':'\ud83d\udcbd', + 'mobile_phone_off':'\ud83d\udcf4', + 'money_mouth_face':'\ud83e\udd11', + 'money_with_wings':'\ud83d\udcb8', + 'moneybag':'\ud83d\udcb0', + 'monkey':'\ud83d\udc12', + 'monkey_face':'\ud83d\udc35', + 'monorail':'\ud83d\ude9d', + 'moon':'\ud83c\udf14', + 'mortar_board':'\ud83c\udf93', + 'mosque':'\ud83d\udd4c', + 'motor_boat':'\ud83d\udee5', + 'motor_scooter':'\ud83d\udef5', + 'motorcycle':'\ud83c\udfcd', + 'motorway':'\ud83d\udee3', + 'mount_fuji':'\ud83d\uddfb', + 'mountain':'\u26f0', + 'mountain_biking_man':'\ud83d\udeb5', + 'mountain_biking_woman':'\ud83d\udeb5‍\u2640\ufe0f', + 'mountain_cableway':'\ud83d\udea0', + 'mountain_railway':'\ud83d\ude9e', + 'mountain_snow':'\ud83c\udfd4', + 'mouse':'\ud83d\udc2d', + 'mouse2':'\ud83d\udc01', + 'movie_camera':'\ud83c\udfa5', + 'moyai':'\ud83d\uddff', + 'mrs_claus':'\ud83e\udd36', + 'muscle':'\ud83d\udcaa', + 'mushroom':'\ud83c\udf44', + 'musical_keyboard':'\ud83c\udfb9', + 'musical_note':'\ud83c\udfb5', + 'musical_score':'\ud83c\udfbc', + 'mute':'\ud83d\udd07', + 'nail_care':'\ud83d\udc85', + 'name_badge':'\ud83d\udcdb', + 'national_park':'\ud83c\udfde', + 'nauseated_face':'\ud83e\udd22', + 'necktie':'\ud83d\udc54', + 'negative_squared_cross_mark':'\u274e', + 'nerd_face':'\ud83e\udd13', + 'neutral_face':'\ud83d\ude10', + 'new':'\ud83c\udd95', + 'new_moon':'\ud83c\udf11', + 'new_moon_with_face':'\ud83c\udf1a', + 'newspaper':'\ud83d\udcf0', + 'newspaper_roll':'\ud83d\uddde', + 'next_track_button':'\u23ed', + 'ng':'\ud83c\udd96', + 'no_good_man':'\ud83d\ude45‍\u2642\ufe0f', + 'no_good_woman':'\ud83d\ude45', + 'night_with_stars':'\ud83c\udf03', + 'no_bell':'\ud83d\udd15', + 'no_bicycles':'\ud83d\udeb3', + 'no_entry':'\u26d4\ufe0f', + 'no_entry_sign':'\ud83d\udeab', + 'no_mobile_phones':'\ud83d\udcf5', + 'no_mouth':'\ud83d\ude36', + 'no_pedestrians':'\ud83d\udeb7', + 'no_smoking':'\ud83d\udead', + 'non-potable_water':'\ud83d\udeb1', + 'nose':'\ud83d\udc43', + 'notebook':'\ud83d\udcd3', + 'notebook_with_decorative_cover':'\ud83d\udcd4', + 'notes':'\ud83c\udfb6', + 'nut_and_bolt':'\ud83d\udd29', + 'o':'\u2b55\ufe0f', + 'o2':'\ud83c\udd7e\ufe0f', + 'ocean':'\ud83c\udf0a', + 'octopus':'\ud83d\udc19', + 'oden':'\ud83c\udf62', + 'office':'\ud83c\udfe2', + 'oil_drum':'\ud83d\udee2', + 'ok':'\ud83c\udd97', + 'ok_hand':'\ud83d\udc4c', + 'ok_man':'\ud83d\ude46‍\u2642\ufe0f', + 'ok_woman':'\ud83d\ude46', + 'old_key':'\ud83d\udddd', + 'older_man':'\ud83d\udc74', + 'older_woman':'\ud83d\udc75', + 'om':'\ud83d\udd49', + 'on':'\ud83d\udd1b', + 'oncoming_automobile':'\ud83d\ude98', + 'oncoming_bus':'\ud83d\ude8d', + 'oncoming_police_car':'\ud83d\ude94', + 'oncoming_taxi':'\ud83d\ude96', + 'open_file_folder':'\ud83d\udcc2', + 'open_hands':'\ud83d\udc50', + 'open_mouth':'\ud83d\ude2e', + 'open_umbrella':'\u2602\ufe0f', + 'ophiuchus':'\u26ce', + 'orange_book':'\ud83d\udcd9', + 'orthodox_cross':'\u2626\ufe0f', + 'outbox_tray':'\ud83d\udce4', + 'owl':'\ud83e\udd89', + 'ox':'\ud83d\udc02', + 'package':'\ud83d\udce6', + 'page_facing_up':'\ud83d\udcc4', + 'page_with_curl':'\ud83d\udcc3', + 'pager':'\ud83d\udcdf', + 'paintbrush':'\ud83d\udd8c', + 'palm_tree':'\ud83c\udf34', + 'pancakes':'\ud83e\udd5e', + 'panda_face':'\ud83d\udc3c', + 'paperclip':'\ud83d\udcce', + 'paperclips':'\ud83d\udd87', + 'parasol_on_ground':'\u26f1', + 'parking':'\ud83c\udd7f\ufe0f', + 'part_alternation_mark':'\u303d\ufe0f', + 'partly_sunny':'\u26c5\ufe0f', + 'passenger_ship':'\ud83d\udef3', + 'passport_control':'\ud83d\udec2', + 'pause_button':'\u23f8', + 'peace_symbol':'\u262e\ufe0f', + 'peach':'\ud83c\udf51', + 'peanuts':'\ud83e\udd5c', + 'pear':'\ud83c\udf50', + 'pen':'\ud83d\udd8a', + 'pencil2':'\u270f\ufe0f', + 'penguin':'\ud83d\udc27', + 'pensive':'\ud83d\ude14', + 'performing_arts':'\ud83c\udfad', + 'persevere':'\ud83d\ude23', + 'person_fencing':'\ud83e\udd3a', + 'pouting_woman':'\ud83d\ude4e', + 'phone':'\u260e\ufe0f', + 'pick':'\u26cf', + 'pig':'\ud83d\udc37', + 'pig2':'\ud83d\udc16', + 'pig_nose':'\ud83d\udc3d', + 'pill':'\ud83d\udc8a', + 'pineapple':'\ud83c\udf4d', + 'ping_pong':'\ud83c\udfd3', + 'pisces':'\u2653\ufe0f', + 'pizza':'\ud83c\udf55', + 'place_of_worship':'\ud83d\uded0', + 'plate_with_cutlery':'\ud83c\udf7d', + 'play_or_pause_button':'\u23ef', + 'point_down':'\ud83d\udc47', + 'point_left':'\ud83d\udc48', + 'point_right':'\ud83d\udc49', + 'point_up':'\u261d\ufe0f', + 'point_up_2':'\ud83d\udc46', + 'police_car':'\ud83d\ude93', + 'policewoman':'\ud83d\udc6e‍\u2640\ufe0f', + 'poodle':'\ud83d\udc29', + 'popcorn':'\ud83c\udf7f', + 'post_office':'\ud83c\udfe3', + 'postal_horn':'\ud83d\udcef', + 'postbox':'\ud83d\udcee', + 'potable_water':'\ud83d\udeb0', + 'potato':'\ud83e\udd54', + 'pouch':'\ud83d\udc5d', + 'poultry_leg':'\ud83c\udf57', + 'pound':'\ud83d\udcb7', + 'rage':'\ud83d\ude21', + 'pouting_cat':'\ud83d\ude3e', + 'pouting_man':'\ud83d\ude4e‍\u2642\ufe0f', + 'pray':'\ud83d\ude4f', + 'prayer_beads':'\ud83d\udcff', + 'pregnant_woman':'\ud83e\udd30', + 'previous_track_button':'\u23ee', + 'prince':'\ud83e\udd34', + 'princess':'\ud83d\udc78', + 'printer':'\ud83d\udda8', + 'purple_heart':'\ud83d\udc9c', + 'purse':'\ud83d\udc5b', + 'pushpin':'\ud83d\udccc', + 'put_litter_in_its_place':'\ud83d\udeae', + 'question':'\u2753', + 'rabbit':'\ud83d\udc30', + 'rabbit2':'\ud83d\udc07', + 'racehorse':'\ud83d\udc0e', + 'racing_car':'\ud83c\udfce', + 'radio':'\ud83d\udcfb', + 'radio_button':'\ud83d\udd18', + 'radioactive':'\u2622\ufe0f', + 'railway_car':'\ud83d\ude83', + 'railway_track':'\ud83d\udee4', + 'rainbow':'\ud83c\udf08', + 'rainbow_flag':'\ud83c\udff3\ufe0f‍\ud83c\udf08', + 'raised_back_of_hand':'\ud83e\udd1a', + 'raised_hand_with_fingers_splayed':'\ud83d\udd90', + 'raised_hands':'\ud83d\ude4c', + 'raising_hand_woman':'\ud83d\ude4b', + 'raising_hand_man':'\ud83d\ude4b‍\u2642\ufe0f', + 'ram':'\ud83d\udc0f', + 'ramen':'\ud83c\udf5c', + 'rat':'\ud83d\udc00', + 'record_button':'\u23fa', + 'recycle':'\u267b\ufe0f', + 'red_circle':'\ud83d\udd34', + 'registered':'\u00ae\ufe0f', + 'relaxed':'\u263a\ufe0f', + 'relieved':'\ud83d\ude0c', + 'reminder_ribbon':'\ud83c\udf97', + 'repeat':'\ud83d\udd01', + 'repeat_one':'\ud83d\udd02', + 'rescue_worker_helmet':'\u26d1', + 'restroom':'\ud83d\udebb', + 'revolving_hearts':'\ud83d\udc9e', + 'rewind':'\u23ea', + 'rhinoceros':'\ud83e\udd8f', + 'ribbon':'\ud83c\udf80', + 'rice':'\ud83c\udf5a', + 'rice_ball':'\ud83c\udf59', + 'rice_cracker':'\ud83c\udf58', + 'rice_scene':'\ud83c\udf91', + 'right_anger_bubble':'\ud83d\uddef', + 'ring':'\ud83d\udc8d', + 'robot':'\ud83e\udd16', + 'rocket':'\ud83d\ude80', + 'rofl':'\ud83e\udd23', + 'roll_eyes':'\ud83d\ude44', + 'roller_coaster':'\ud83c\udfa2', + 'rooster':'\ud83d\udc13', + 'rose':'\ud83c\udf39', + 'rosette':'\ud83c\udff5', + 'rotating_light':'\ud83d\udea8', + 'round_pushpin':'\ud83d\udccd', + 'rowing_man':'\ud83d\udea3', + 'rowing_woman':'\ud83d\udea3‍\u2640\ufe0f', + 'rugby_football':'\ud83c\udfc9', + 'running_man':'\ud83c\udfc3', + 'running_shirt_with_sash':'\ud83c\udfbd', + 'running_woman':'\ud83c\udfc3‍\u2640\ufe0f', + 'sa':'\ud83c\ude02\ufe0f', + 'sagittarius':'\u2650\ufe0f', + 'sake':'\ud83c\udf76', + 'sandal':'\ud83d\udc61', + 'santa':'\ud83c\udf85', + 'satellite':'\ud83d\udce1', + 'saxophone':'\ud83c\udfb7', + 'school':'\ud83c\udfeb', + 'school_satchel':'\ud83c\udf92', + 'scissors':'\u2702\ufe0f', + 'scorpion':'\ud83e\udd82', + 'scorpius':'\u264f\ufe0f', + 'scream':'\ud83d\ude31', + 'scream_cat':'\ud83d\ude40', + 'scroll':'\ud83d\udcdc', + 'seat':'\ud83d\udcba', + 'secret':'\u3299\ufe0f', + 'see_no_evil':'\ud83d\ude48', + 'seedling':'\ud83c\udf31', + 'selfie':'\ud83e\udd33', + 'shallow_pan_of_food':'\ud83e\udd58', + 'shamrock':'\u2618\ufe0f', + 'shark':'\ud83e\udd88', + 'shaved_ice':'\ud83c\udf67', + 'sheep':'\ud83d\udc11', + 'shell':'\ud83d\udc1a', + 'shield':'\ud83d\udee1', + 'shinto_shrine':'\u26e9', + 'ship':'\ud83d\udea2', + 'shirt':'\ud83d\udc55', + 'shopping':'\ud83d\udecd', + 'shopping_cart':'\ud83d\uded2', + 'shower':'\ud83d\udebf', + 'shrimp':'\ud83e\udd90', + 'signal_strength':'\ud83d\udcf6', + 'six_pointed_star':'\ud83d\udd2f', + 'ski':'\ud83c\udfbf', + 'skier':'\u26f7', + 'skull':'\ud83d\udc80', + 'skull_and_crossbones':'\u2620\ufe0f', + 'sleeping':'\ud83d\ude34', + 'sleeping_bed':'\ud83d\udecc', + 'sleepy':'\ud83d\ude2a', + 'slightly_frowning_face':'\ud83d\ude41', + 'slightly_smiling_face':'\ud83d\ude42', + 'slot_machine':'\ud83c\udfb0', + 'small_airplane':'\ud83d\udee9', + 'small_blue_diamond':'\ud83d\udd39', + 'small_orange_diamond':'\ud83d\udd38', + 'small_red_triangle':'\ud83d\udd3a', + 'small_red_triangle_down':'\ud83d\udd3b', + 'smile':'\ud83d\ude04', + 'smile_cat':'\ud83d\ude38', + 'smiley':'\ud83d\ude03', + 'smiley_cat':'\ud83d\ude3a', + 'smiling_imp':'\ud83d\ude08', + 'smirk':'\ud83d\ude0f', + 'smirk_cat':'\ud83d\ude3c', + 'smoking':'\ud83d\udeac', + 'snail':'\ud83d\udc0c', + 'snake':'\ud83d\udc0d', + 'sneezing_face':'\ud83e\udd27', + 'snowboarder':'\ud83c\udfc2', + 'snowflake':'\u2744\ufe0f', + 'snowman':'\u26c4\ufe0f', + 'snowman_with_snow':'\u2603\ufe0f', + 'sob':'\ud83d\ude2d', + 'soccer':'\u26bd\ufe0f', + 'soon':'\ud83d\udd1c', + 'sos':'\ud83c\udd98', + 'sound':'\ud83d\udd09', + 'space_invader':'\ud83d\udc7e', + 'spades':'\u2660\ufe0f', + 'spaghetti':'\ud83c\udf5d', + 'sparkle':'\u2747\ufe0f', + 'sparkler':'\ud83c\udf87', + 'sparkles':'\u2728', + 'sparkling_heart':'\ud83d\udc96', + 'speak_no_evil':'\ud83d\ude4a', + 'speaker':'\ud83d\udd08', + 'speaking_head':'\ud83d\udde3', + 'speech_balloon':'\ud83d\udcac', + 'speedboat':'\ud83d\udea4', + 'spider':'\ud83d\udd77', + 'spider_web':'\ud83d\udd78', + 'spiral_calendar':'\ud83d\uddd3', + 'spiral_notepad':'\ud83d\uddd2', + 'spoon':'\ud83e\udd44', + 'squid':'\ud83e\udd91', + 'stadium':'\ud83c\udfdf', + 'star':'\u2b50\ufe0f', + 'star2':'\ud83c\udf1f', + 'star_and_crescent':'\u262a\ufe0f', + 'star_of_david':'\u2721\ufe0f', + 'stars':'\ud83c\udf20', + 'station':'\ud83d\ude89', + 'statue_of_liberty':'\ud83d\uddfd', + 'steam_locomotive':'\ud83d\ude82', + 'stew':'\ud83c\udf72', + 'stop_button':'\u23f9', + 'stop_sign':'\ud83d\uded1', + 'stopwatch':'\u23f1', + 'straight_ruler':'\ud83d\udccf', + 'strawberry':'\ud83c\udf53', + 'stuck_out_tongue':'\ud83d\ude1b', + 'stuck_out_tongue_closed_eyes':'\ud83d\ude1d', + 'stuck_out_tongue_winking_eye':'\ud83d\ude1c', + 'studio_microphone':'\ud83c\udf99', + 'stuffed_flatbread':'\ud83e\udd59', + 'sun_behind_large_cloud':'\ud83c\udf25', + 'sun_behind_rain_cloud':'\ud83c\udf26', + 'sun_behind_small_cloud':'\ud83c\udf24', + 'sun_with_face':'\ud83c\udf1e', + 'sunflower':'\ud83c\udf3b', + 'sunglasses':'\ud83d\ude0e', + 'sunny':'\u2600\ufe0f', + 'sunrise':'\ud83c\udf05', + 'sunrise_over_mountains':'\ud83c\udf04', + 'surfing_man':'\ud83c\udfc4', + 'surfing_woman':'\ud83c\udfc4‍\u2640\ufe0f', + 'sushi':'\ud83c\udf63', + 'suspension_railway':'\ud83d\ude9f', + 'sweat':'\ud83d\ude13', + 'sweat_drops':'\ud83d\udca6', + 'sweat_smile':'\ud83d\ude05', + 'sweet_potato':'\ud83c\udf60', + 'swimming_man':'\ud83c\udfca', + 'swimming_woman':'\ud83c\udfca‍\u2640\ufe0f', + 'symbols':'\ud83d\udd23', + 'synagogue':'\ud83d\udd4d', + 'syringe':'\ud83d\udc89', + 'taco':'\ud83c\udf2e', + 'tada':'\ud83c\udf89', + 'tanabata_tree':'\ud83c\udf8b', + 'taurus':'\u2649\ufe0f', + 'taxi':'\ud83d\ude95', + 'tea':'\ud83c\udf75', + 'telephone_receiver':'\ud83d\udcde', + 'telescope':'\ud83d\udd2d', + 'tennis':'\ud83c\udfbe', + 'tent':'\u26fa\ufe0f', + 'thermometer':'\ud83c\udf21', + 'thinking':'\ud83e\udd14', + 'thought_balloon':'\ud83d\udcad', + 'ticket':'\ud83c\udfab', + 'tickets':'\ud83c\udf9f', + 'tiger':'\ud83d\udc2f', + 'tiger2':'\ud83d\udc05', + 'timer_clock':'\u23f2', + 'tipping_hand_man':'\ud83d\udc81‍\u2642\ufe0f', + 'tired_face':'\ud83d\ude2b', + 'tm':'\u2122\ufe0f', + 'toilet':'\ud83d\udebd', + 'tokyo_tower':'\ud83d\uddfc', + 'tomato':'\ud83c\udf45', + 'tongue':'\ud83d\udc45', + 'top':'\ud83d\udd1d', + 'tophat':'\ud83c\udfa9', + 'tornado':'\ud83c\udf2a', + 'trackball':'\ud83d\uddb2', + 'tractor':'\ud83d\ude9c', + 'traffic_light':'\ud83d\udea5', + 'train':'\ud83d\ude8b', + 'train2':'\ud83d\ude86', + 'tram':'\ud83d\ude8a', + 'triangular_flag_on_post':'\ud83d\udea9', + 'triangular_ruler':'\ud83d\udcd0', + 'trident':'\ud83d\udd31', + 'triumph':'\ud83d\ude24', + 'trolleybus':'\ud83d\ude8e', + 'trophy':'\ud83c\udfc6', + 'tropical_drink':'\ud83c\udf79', + 'tropical_fish':'\ud83d\udc20', + 'truck':'\ud83d\ude9a', + 'trumpet':'\ud83c\udfba', + 'tulip':'\ud83c\udf37', + 'tumbler_glass':'\ud83e\udd43', + 'turkey':'\ud83e\udd83', + 'turtle':'\ud83d\udc22', + 'tv':'\ud83d\udcfa', + 'twisted_rightwards_arrows':'\ud83d\udd00', + 'two_hearts':'\ud83d\udc95', + 'two_men_holding_hands':'\ud83d\udc6c', + 'two_women_holding_hands':'\ud83d\udc6d', + 'u5272':'\ud83c\ude39', + 'u5408':'\ud83c\ude34', + 'u55b6':'\ud83c\ude3a', + 'u6307':'\ud83c\ude2f\ufe0f', + 'u6708':'\ud83c\ude37\ufe0f', + 'u6709':'\ud83c\ude36', + 'u6e80':'\ud83c\ude35', + 'u7121':'\ud83c\ude1a\ufe0f', + 'u7533':'\ud83c\ude38', + 'u7981':'\ud83c\ude32', + 'u7a7a':'\ud83c\ude33', + 'umbrella':'\u2614\ufe0f', + 'unamused':'\ud83d\ude12', + 'underage':'\ud83d\udd1e', + 'unicorn':'\ud83e\udd84', + 'unlock':'\ud83d\udd13', + 'up':'\ud83c\udd99', + 'upside_down_face':'\ud83d\ude43', + 'v':'\u270c\ufe0f', + 'vertical_traffic_light':'\ud83d\udea6', + 'vhs':'\ud83d\udcfc', + 'vibration_mode':'\ud83d\udcf3', + 'video_camera':'\ud83d\udcf9', + 'video_game':'\ud83c\udfae', + 'violin':'\ud83c\udfbb', + 'virgo':'\u264d\ufe0f', + 'volcano':'\ud83c\udf0b', + 'volleyball':'\ud83c\udfd0', + 'vs':'\ud83c\udd9a', + 'vulcan_salute':'\ud83d\udd96', + 'walking_man':'\ud83d\udeb6', + 'walking_woman':'\ud83d\udeb6‍\u2640\ufe0f', + 'waning_crescent_moon':'\ud83c\udf18', + 'waning_gibbous_moon':'\ud83c\udf16', + 'warning':'\u26a0\ufe0f', + 'wastebasket':'\ud83d\uddd1', + 'watch':'\u231a\ufe0f', + 'water_buffalo':'\ud83d\udc03', + 'watermelon':'\ud83c\udf49', + 'wave':'\ud83d\udc4b', + 'wavy_dash':'\u3030\ufe0f', + 'waxing_crescent_moon':'\ud83c\udf12', + 'wc':'\ud83d\udebe', + 'weary':'\ud83d\ude29', + 'wedding':'\ud83d\udc92', + 'weight_lifting_man':'\ud83c\udfcb\ufe0f', + 'weight_lifting_woman':'\ud83c\udfcb\ufe0f‍\u2640\ufe0f', + 'whale':'\ud83d\udc33', + 'whale2':'\ud83d\udc0b', + 'wheel_of_dharma':'\u2638\ufe0f', + 'wheelchair':'\u267f\ufe0f', + 'white_check_mark':'\u2705', + 'white_circle':'\u26aa\ufe0f', + 'white_flag':'\ud83c\udff3\ufe0f', + 'white_flower':'\ud83d\udcae', + 'white_large_square':'\u2b1c\ufe0f', + 'white_medium_small_square':'\u25fd\ufe0f', + 'white_medium_square':'\u25fb\ufe0f', + 'white_small_square':'\u25ab\ufe0f', + 'white_square_button':'\ud83d\udd33', + 'wilted_flower':'\ud83e\udd40', + 'wind_chime':'\ud83c\udf90', + 'wind_face':'\ud83c\udf2c', + 'wine_glass':'\ud83c\udf77', + 'wink':'\ud83d\ude09', + 'wolf':'\ud83d\udc3a', + 'woman':'\ud83d\udc69', + 'woman_artist':'\ud83d\udc69‍\ud83c\udfa8', + 'woman_astronaut':'\ud83d\udc69‍\ud83d\ude80', + 'woman_cartwheeling':'\ud83e\udd38‍\u2640\ufe0f', + 'woman_cook':'\ud83d\udc69‍\ud83c\udf73', + 'woman_facepalming':'\ud83e\udd26‍\u2640\ufe0f', + 'woman_factory_worker':'\ud83d\udc69‍\ud83c\udfed', + 'woman_farmer':'\ud83d\udc69‍\ud83c\udf3e', + 'woman_firefighter':'\ud83d\udc69‍\ud83d\ude92', + 'woman_health_worker':'\ud83d\udc69‍\u2695\ufe0f', + 'woman_judge':'\ud83d\udc69‍\u2696\ufe0f', + 'woman_juggling':'\ud83e\udd39‍\u2640\ufe0f', + 'woman_mechanic':'\ud83d\udc69‍\ud83d\udd27', + 'woman_office_worker':'\ud83d\udc69‍\ud83d\udcbc', + 'woman_pilot':'\ud83d\udc69‍\u2708\ufe0f', + 'woman_playing_handball':'\ud83e\udd3e‍\u2640\ufe0f', + 'woman_playing_water_polo':'\ud83e\udd3d‍\u2640\ufe0f', + 'woman_scientist':'\ud83d\udc69‍\ud83d\udd2c', + 'woman_shrugging':'\ud83e\udd37‍\u2640\ufe0f', + 'woman_singer':'\ud83d\udc69‍\ud83c\udfa4', + 'woman_student':'\ud83d\udc69‍\ud83c\udf93', + 'woman_teacher':'\ud83d\udc69‍\ud83c\udfeb', + 'woman_technologist':'\ud83d\udc69‍\ud83d\udcbb', + 'woman_with_turban':'\ud83d\udc73‍\u2640\ufe0f', + 'womans_clothes':'\ud83d\udc5a', + 'womans_hat':'\ud83d\udc52', + 'women_wrestling':'\ud83e\udd3c‍\u2640\ufe0f', + 'womens':'\ud83d\udeba', + 'world_map':'\ud83d\uddfa', + 'worried':'\ud83d\ude1f', + 'wrench':'\ud83d\udd27', + 'writing_hand':'\u270d\ufe0f', + 'x':'\u274c', + 'yellow_heart':'\ud83d\udc9b', + 'yen':'\ud83d\udcb4', + 'yin_yang':'\u262f\ufe0f', + 'yum':'\ud83d\ude0b', + 'zap':'\u26a1\ufe0f', + 'zipper_mouth_face':'\ud83e\udd10', + 'zzz':'\ud83d\udca4', + + /* special emojis :P */ + 'octocat': ':octocat:', + 'showdown': 'S' +}; + +/** + * Created by Estevao on 31-05-2015. + */ + +/** + * Showdown Converter class + * @class + * @param {object} [converterOptions] + * @returns {Converter} + */ +showdown.Converter = function (converterOptions) { + + var + /** + * Options used by this converter + * @private + * @type {{}} + */ + options = {}, + + /** + * Language extensions used by this converter + * @private + * @type {Array} + */ + langExtensions = [], + + /** + * Output modifiers extensions used by this converter + * @private + * @type {Array} + */ + outputModifiers = [], + + /** + * Event listeners + * @private + * @type {{}} + */ + listeners = {}, + + /** + * The flavor set in this converter + */ + setConvFlavor = setFlavor, + + /** + * Metadata of the document + * @type {{parsed: {}, raw: string, format: string}} + */ + metadata = { + parsed: {}, + raw: '', + format: '' + }; + + _constructor(); + + /** + * Converter constructor + * @private + */ + function _constructor () { + converterOptions = converterOptions || {}; + + for (var gOpt in globalOptions) { + if (globalOptions.hasOwnProperty(gOpt)) { + options[gOpt] = globalOptions[gOpt]; + } + } + + // Merge options + if (typeof converterOptions === 'object') { + for (var opt in converterOptions) { + if (converterOptions.hasOwnProperty(opt)) { + options[opt] = converterOptions[opt]; + } + } + } else { + throw Error('Converter expects the passed parameter to be an object, but ' + typeof converterOptions + + ' was passed instead.'); + } + + if (options.extensions) { + showdown.helper.forEach(options.extensions, _parseExtension); + } + } + + /** + * Parse extension + * @param {*} ext + * @param {string} [name=''] + * @private + */ + function _parseExtension (ext, name) { + + name = name || null; + // If it's a string, the extension was previously loaded + if (showdown.helper.isString(ext)) { + ext = showdown.helper.stdExtName(ext); + name = ext; + + // LEGACY_SUPPORT CODE + if (showdown.extensions[ext]) { + console.warn('DEPRECATION WARNING: ' + ext + ' is an old extension that uses a deprecated loading method.' + + 'Please inform the developer that the extension should be updated!'); + legacyExtensionLoading(showdown.extensions[ext], ext); + return; + // END LEGACY SUPPORT CODE + + } else if (!showdown.helper.isUndefined(extensions[ext])) { + ext = extensions[ext]; + + } else { + throw Error('Extension "' + ext + '" could not be loaded. It was either not found or is not a valid extension.'); + } + } + + if (typeof ext === 'function') { + ext = ext(); + } + + if (!showdown.helper.isArray(ext)) { + ext = [ext]; + } + + var validExt = validate(ext, name); + if (!validExt.valid) { + throw Error(validExt.error); + } + + for (var i = 0; i < ext.length; ++i) { + switch (ext[i].type) { + + case 'lang': + langExtensions.push(ext[i]); + break; + + case 'output': + outputModifiers.push(ext[i]); + break; + } + if (ext[i].hasOwnProperty('listeners')) { + for (var ln in ext[i].listeners) { + if (ext[i].listeners.hasOwnProperty(ln)) { + listen(ln, ext[i].listeners[ln]); + } + } + } + } + + } + + /** + * LEGACY_SUPPORT + * @param {*} ext + * @param {string} name + */ + function legacyExtensionLoading (ext, name) { + if (typeof ext === 'function') { + ext = ext(new showdown.Converter()); + } + if (!showdown.helper.isArray(ext)) { + ext = [ext]; + } + var valid = validate(ext, name); + + if (!valid.valid) { + throw Error(valid.error); + } + + for (var i = 0; i < ext.length; ++i) { + switch (ext[i].type) { + case 'lang': + langExtensions.push(ext[i]); + break; + case 'output': + outputModifiers.push(ext[i]); + break; + default:// should never reach here + throw Error('Extension loader error: Type unrecognized!!!'); + } + } + } + + /** + * Listen to an event + * @param {string} name + * @param {function} callback + */ + function listen (name, callback) { + if (!showdown.helper.isString(name)) { + throw Error('Invalid argument in converter.listen() method: name must be a string, but ' + typeof name + ' given'); + } + + if (typeof callback !== 'function') { + throw Error('Invalid argument in converter.listen() method: callback must be a function, but ' + typeof callback + ' given'); + } + + if (!listeners.hasOwnProperty(name)) { + listeners[name] = []; + } + listeners[name].push(callback); + } + + function rTrimInputText (text) { + var rsp = text.match(/^\s*/)[0].length, + rgx = new RegExp('^\\s{0,' + rsp + '}', 'gm'); + return text.replace(rgx, ''); + } + + /** + * Dispatch an event + * @private + * @param {string} evtName Event name + * @param {string} text Text + * @param {{}} options Converter Options + * @param {{}} globals + * @returns {string} + */ + this._dispatch = function dispatch (evtName, text, options, globals) { + if (listeners.hasOwnProperty(evtName)) { + for (var ei = 0; ei < listeners[evtName].length; ++ei) { + var nText = listeners[evtName][ei](evtName, text, this, options, globals); + if (nText && typeof nText !== 'undefined') { + text = nText; + } + } + } + return text; + }; + + /** + * Listen to an event + * @param {string} name + * @param {function} callback + * @returns {showdown.Converter} + */ + this.listen = function (name, callback) { + listen(name, callback); + return this; + }; + + /** + * Converts a markdown string into HTML + * @param {string} text + * @returns {*} + */ + this.makeHtml = function (text) { + //check if text is not falsy + if (!text) { + return text; + } + + var globals = { + gHtmlBlocks: [], + gHtmlMdBlocks: [], + gHtmlSpans: [], + gUrls: {}, + gTitles: {}, + gDimensions: {}, + gListLevel: 0, + hashLinkCounts: {}, + langExtensions: langExtensions, + outputModifiers: outputModifiers, + converter: this, + ghCodeBlocks: [], + metadata: { + parsed: {}, + raw: '', + format: '' + } + }; + + // This lets us use ¨ trema as an escape char to avoid md5 hashes + // The choice of character is arbitrary; anything that isn't + // magic in Markdown will work. + text = text.replace(/¨/g, '¨T'); + + // Replace $ with ¨D + // RegExp interprets $ as a special character + // when it's in a replacement string + text = text.replace(/\$/g, '¨D'); + + // Standardize line endings + text = text.replace(/\r\n/g, '\n'); // DOS to Unix + text = text.replace(/\r/g, '\n'); // Mac to Unix + + // Stardardize line spaces + text = text.replace(/\u00A0/g, ' '); + + if (options.smartIndentationFix) { + text = rTrimInputText(text); + } + + // Make sure text begins and ends with a couple of newlines: + text = '\n\n' + text + '\n\n'; + + // detab + text = showdown.subParser('detab')(text, options, globals); + + /** + * Strip any lines consisting only of spaces and tabs. + * This makes subsequent regexs easier to write, because we can + * match consecutive blank lines with /\n+/ instead of something + * contorted like /[ \t]*\n+/ + */ + text = text.replace(/^[ \t]+$/mg, ''); + + //run languageExtensions + showdown.helper.forEach(langExtensions, function (ext) { + text = showdown.subParser('runExtension')(ext, text, options, globals); + }); + + // run the sub parsers + text = showdown.subParser('metadata')(text, options, globals); + text = showdown.subParser('hashPreCodeTags')(text, options, globals); + text = showdown.subParser('githubCodeBlocks')(text, options, globals); + text = showdown.subParser('hashHTMLBlocks')(text, options, globals); + text = showdown.subParser('hashCodeTags')(text, options, globals); + text = showdown.subParser('stripLinkDefinitions')(text, options, globals); + text = showdown.subParser('blockGamut')(text, options, globals); + text = showdown.subParser('unhashHTMLSpans')(text, options, globals); + text = showdown.subParser('unescapeSpecialChars')(text, options, globals); + + // attacklab: Restore dollar signs + text = text.replace(/¨D/g, '$$'); + + // attacklab: Restore tremas + text = text.replace(/¨T/g, '¨'); + + // render a complete html document instead of a partial if the option is enabled + text = showdown.subParser('completeHTMLDocument')(text, options, globals); + + // Run output modifiers + showdown.helper.forEach(outputModifiers, function (ext) { + text = showdown.subParser('runExtension')(ext, text, options, globals); + }); + + // update metadata + metadata = globals.metadata; + return text; + }; + + /** + * Converts an HTML string into a markdown string + * @param src + * @param [HTMLParser] A WHATWG DOM and HTML parser, such as JSDOM. If none is supplied, window.document will be used. + * @returns {string} + */ + this.makeMarkdown = this.makeMd = function (src, HTMLParser) { + + // replace \r\n with \n + src = src.replace(/\r\n/g, '\n'); + src = src.replace(/\r/g, '\n'); // old macs + + // due to an edge case, we need to find this: > < + // to prevent removing of non silent white spaces + // ex: this is sparta + src = src.replace(/>[ \t]+¨NBSP;<'); + + if (!HTMLParser) { + if (window && window.document) { + HTMLParser = window.document; + } else { + throw new Error('HTMLParser is undefined. If in a webworker or nodejs environment, you need to provide a WHATWG DOM and HTML such as JSDOM'); + } + } + + var doc = HTMLParser.createElement('div'); + doc.innerHTML = src; + + var globals = { + preList: substitutePreCodeTags(doc) + }; + + // remove all newlines and collapse spaces + clean(doc); + + // some stuff, like accidental reference links must now be escaped + // TODO + // doc.innerHTML = doc.innerHTML.replace(/\[[\S\t ]]/); + + var nodes = doc.childNodes, + mdDoc = ''; + + for (var i = 0; i < nodes.length; i++) { + mdDoc += showdown.subParser('makeMarkdown.node')(nodes[i], globals); + } + + function clean (node) { + for (var n = 0; n < node.childNodes.length; ++n) { + var child = node.childNodes[n]; + if (child.nodeType === 3) { + if (!/\S/.test(child.nodeValue)) { + node.removeChild(child); + --n; + } else { + child.nodeValue = child.nodeValue.split('\n').join(' '); + child.nodeValue = child.nodeValue.replace(/(\s)+/g, '$1'); + } + } else if (child.nodeType === 1) { + clean(child); + } + } + } + + // find all pre tags and replace contents with placeholder + // we need this so that we can remove all indentation from html + // to ease up parsing + function substitutePreCodeTags (doc) { + + var pres = doc.querySelectorAll('pre'), + presPH = []; + + for (var i = 0; i < pres.length; ++i) { + + if (pres[i].childElementCount === 1 && pres[i].firstChild.tagName.toLowerCase() === 'code') { + var content = pres[i].firstChild.innerHTML.trim(), + language = pres[i].firstChild.getAttribute('data-language') || ''; + + // if data-language attribute is not defined, then we look for class language-* + if (language === '') { + var classes = pres[i].firstChild.className.split(' '); + for (var c = 0; c < classes.length; ++c) { + var matches = classes[c].match(/^language-(.+)$/); + if (matches !== null) { + language = matches[1]; + break; + } + } + } + + // unescape html entities in content + content = showdown.helper.unescapeHTMLEntities(content); + + presPH.push(content); + pres[i].outerHTML = ''; + } else { + presPH.push(pres[i].innerHTML); + pres[i].innerHTML = ''; + pres[i].setAttribute('prenum', i.toString()); + } + } + return presPH; + } + + return mdDoc; + }; + + /** + * Set an option of this Converter instance + * @param {string} key + * @param {*} value + */ + this.setOption = function (key, value) { + options[key] = value; + }; + + /** + * Get the option of this Converter instance + * @param {string} key + * @returns {*} + */ + this.getOption = function (key) { + return options[key]; + }; + + /** + * Get the options of this Converter instance + * @returns {{}} + */ + this.getOptions = function () { + return options; + }; + + /** + * Add extension to THIS converter + * @param {{}} extension + * @param {string} [name=null] + */ + this.addExtension = function (extension, name) { + name = name || null; + _parseExtension(extension, name); + }; + + /** + * Use a global registered extension with THIS converter + * @param {string} extensionName Name of the previously registered extension + */ + this.useExtension = function (extensionName) { + _parseExtension(extensionName); + }; + + /** + * Set the flavor THIS converter should use + * @param {string} name + */ + this.setFlavor = function (name) { + if (!flavor.hasOwnProperty(name)) { + throw Error(name + ' flavor was not found'); + } + var preset = flavor[name]; + setConvFlavor = name; + for (var option in preset) { + if (preset.hasOwnProperty(option)) { + options[option] = preset[option]; + } + } + }; + + /** + * Get the currently set flavor of this converter + * @returns {string} + */ + this.getFlavor = function () { + return setConvFlavor; + }; + + /** + * Remove an extension from THIS converter. + * Note: This is a costly operation. It's better to initialize a new converter + * and specify the extensions you wish to use + * @param {Array} extension + */ + this.removeExtension = function (extension) { + if (!showdown.helper.isArray(extension)) { + extension = [extension]; + } + for (var a = 0; a < extension.length; ++a) { + var ext = extension[a]; + for (var i = 0; i < langExtensions.length; ++i) { + if (langExtensions[i] === ext) { + langExtensions[i].splice(i, 1); + } + } + for (var ii = 0; ii < outputModifiers.length; ++i) { + if (outputModifiers[ii] === ext) { + outputModifiers[ii].splice(i, 1); + } + } + } + }; + + /** + * Get all extension of THIS converter + * @returns {{language: Array, output: Array}} + */ + this.getAllExtensions = function () { + return { + language: langExtensions, + output: outputModifiers + }; + }; + + /** + * Get the metadata of the previously parsed document + * @param raw + * @returns {string|{}} + */ + this.getMetadata = function (raw) { + if (raw) { + return metadata.raw; + } else { + return metadata.parsed; + } + }; + + /** + * Get the metadata format of the previously parsed document + * @returns {string} + */ + this.getMetadataFormat = function () { + return metadata.format; + }; + + /** + * Private: set a single key, value metadata pair + * @param {string} key + * @param {string} value + */ + this._setMetadataPair = function (key, value) { + metadata.parsed[key] = value; + }; + + /** + * Private: set metadata format + * @param {string} format + */ + this._setMetadataFormat = function (format) { + metadata.format = format; + }; + + /** + * Private: set metadata raw text + * @param {string} raw + */ + this._setMetadataRaw = function (raw) { + metadata.raw = raw; + }; +}; + +/** + * Turn Markdown link shortcuts into XHTML tags. + */ +showdown.subParser('anchors', function (text, options, globals) { + + text = globals.converter._dispatch('anchors.before', text, options, globals); + + var writeAnchorTag = function (wholeMatch, linkText, linkId, url, m5, m6, title) { + if (showdown.helper.isUndefined(title)) { + title = ''; + } + linkId = linkId.toLowerCase(); + + // Special case for explicit empty url + if (wholeMatch.search(/\(? ?(['"].*['"])?\)$/m) > -1) { + url = ''; + } else if (!url) { + if (!linkId) { + // lower-case and turn embedded newlines into spaces + linkId = linkText.toLowerCase().replace(/ ?\n/g, ' '); + } + url = '#' + linkId; + + if (!showdown.helper.isUndefined(globals.gUrls[linkId])) { + url = globals.gUrls[linkId]; + if (!showdown.helper.isUndefined(globals.gTitles[linkId])) { + title = globals.gTitles[linkId]; + } + } else { + return wholeMatch; + } + } + + //url = showdown.helper.escapeCharacters(url, '*_', false); // replaced line to improve performance + url = url.replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback); + + var result = ''; + + return result; + }; + + // First, handle reference-style links: [link text] [id] + text = text.replace(/\[((?:\[[^\]]*]|[^\[\]])*)] ?(?:\n *)?\[(.*?)]()()()()/g, writeAnchorTag); + + // Next, inline-style links: [link text](url "optional title") + // cases with crazy urls like ./image/cat1).png + text = text.replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]?<([^>]*)>(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g, + writeAnchorTag); + + // normal cases + text = text.replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]??(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g, + writeAnchorTag); + + // handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link test][1] + // or [link test](/foo) + text = text.replace(/\[([^\[\]]+)]()()()()()/g, writeAnchorTag); + + // Lastly handle GithubMentions if option is enabled + if (options.ghMentions) { + text = text.replace(/(^|\s)(\\)?(@([a-z\d]+(?:[a-z\d.-]+?[a-z\d]+)*))/gmi, function (wm, st, escape, mentions, username) { + if (escape === '\\') { + return st + mentions; + } + + //check if options.ghMentionsLink is a string + if (!showdown.helper.isString(options.ghMentionsLink)) { + throw new Error('ghMentionsLink option must be a string'); + } + var lnk = options.ghMentionsLink.replace(/\{u}/g, username), + target = ''; + if (options.openLinksInNewWindow) { + target = ' rel="noopener noreferrer" target="¨E95Eblank"'; + } + return st + '' + mentions + ''; + }); + } + + text = globals.converter._dispatch('anchors.after', text, options, globals); + return text; +}); + +// url allowed chars [a-z\d_.~:/?#[]@!$&'()*+,;=-] + +var simpleURLRegex = /([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+?\.[^'">\s]+?)()(\1)?(?=\s|$)(?!["<>])/gi, + simpleURLRegex2 = /([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+?)([.!?,()\[\]])?(\1)?(?=\s|$)(?!["<>])/gi, + delimUrlRegex = /()<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)()>()/gi, + simpleMailRegex = /(^|\s)(?:mailto:)?([A-Za-z0-9!#$%&'*+-/=?^_`{|}~.]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)(?=$|\s)/gmi, + delimMailRegex = /<()(?:mailto:)?([-.\w]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, + + replaceLink = function (options) { + return function (wm, leadingMagicChars, link, m2, m3, trailingPunctuation, trailingMagicChars) { + link = link.replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback); + var lnkTxt = link, + append = '', + target = '', + lmc = leadingMagicChars || '', + tmc = trailingMagicChars || ''; + if (/^www\./i.test(link)) { + link = link.replace(/^www\./i, 'http://www.'); + } + if (options.excludeTrailingPunctuationFromURLs && trailingPunctuation) { + append = trailingPunctuation; + } + if (options.openLinksInNewWindow) { + target = ' rel="noopener noreferrer" target="¨E95Eblank"'; + } + return lmc + '' + lnkTxt + '' + append + tmc; + }; + }, + + replaceMail = function (options, globals) { + return function (wholeMatch, b, mail) { + var href = 'mailto:'; + b = b || ''; + mail = showdown.subParser('unescapeSpecialChars')(mail, options, globals); + if (options.encodeEmails) { + href = showdown.helper.encodeEmailAddress(href + mail); + mail = showdown.helper.encodeEmailAddress(mail); + } else { + href = href + mail; + } + return b + '' + mail + ''; + }; + }; + +showdown.subParser('autoLinks', function (text, options, globals) { + + text = globals.converter._dispatch('autoLinks.before', text, options, globals); + + text = text.replace(delimUrlRegex, replaceLink(options)); + text = text.replace(delimMailRegex, replaceMail(options, globals)); + + text = globals.converter._dispatch('autoLinks.after', text, options, globals); + + return text; +}); + +showdown.subParser('simplifiedAutoLinks', function (text, options, globals) { + + if (!options.simplifiedAutoLink) { + return text; + } + + text = globals.converter._dispatch('simplifiedAutoLinks.before', text, options, globals); + + if (options.excludeTrailingPunctuationFromURLs) { + text = text.replace(simpleURLRegex2, replaceLink(options)); + } else { + text = text.replace(simpleURLRegex, replaceLink(options)); + } + text = text.replace(simpleMailRegex, replaceMail(options, globals)); + + text = globals.converter._dispatch('simplifiedAutoLinks.after', text, options, globals); + + return text; +}); + +/** + * These are all the transformations that form block-level + * tags like paragraphs, headers, and list items. + */ +showdown.subParser('blockGamut', function (text, options, globals) { + + text = globals.converter._dispatch('blockGamut.before', text, options, globals); + + // we parse blockquotes first so that we can have headings and hrs + // inside blockquotes + text = showdown.subParser('blockQuotes')(text, options, globals); + text = showdown.subParser('headers')(text, options, globals); + + // Do Horizontal Rules: + text = showdown.subParser('horizontalRule')(text, options, globals); + + text = showdown.subParser('lists')(text, options, globals); + text = showdown.subParser('codeBlocks')(text, options, globals); + text = showdown.subParser('tables')(text, options, globals); + + // We already ran _HashHTMLBlocks() before, in Markdown(), but that + // was to escape raw HTML in the original Markdown source. This time, + // we're escaping the markup we've just created, so that we don't wrap + //

tags around block-level tags. + text = showdown.subParser('hashHTMLBlocks')(text, options, globals); + text = showdown.subParser('paragraphs')(text, options, globals); + + text = globals.converter._dispatch('blockGamut.after', text, options, globals); + + return text; +}); + +showdown.subParser('blockQuotes', function (text, options, globals) { + + text = globals.converter._dispatch('blockQuotes.before', text, options, globals); + + // add a couple extra lines after the text and endtext mark + text = text + '\n\n'; + + var rgx = /(^ {0,3}>[ \t]?.+\n(.+\n)*\n*)+/gm; + + if (options.splitAdjacentBlockquotes) { + rgx = /^ {0,3}>[\s\S]*?(?:\n\n)/gm; + } + + text = text.replace(rgx, function (bq) { + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + bq = bq.replace(/^[ \t]*>[ \t]?/gm, ''); // trim one level of quoting + + // attacklab: clean up hack + bq = bq.replace(/¨0/g, ''); + + bq = bq.replace(/^[ \t]+$/gm, ''); // trim whitespace-only lines + bq = showdown.subParser('githubCodeBlocks')(bq, options, globals); + bq = showdown.subParser('blockGamut')(bq, options, globals); // recurse + + bq = bq.replace(/(^|\n)/g, '$1 '); + // These leading spaces screw with

 content, so we need to fix that:
+    bq = bq.replace(/(\s*
[^\r]+?<\/pre>)/gm, function (wholeMatch, m1) {
+      var pre = m1;
+      // attacklab: hack around Konqueror 3.5.4 bug:
+      pre = pre.replace(/^  /mg, '¨0');
+      pre = pre.replace(/¨0/g, '');
+      return pre;
+    });
+
+    return showdown.subParser('hashBlock')('
\n' + bq + '\n
', options, globals); + }); + + text = globals.converter._dispatch('blockQuotes.after', text, options, globals); + return text; +}); + +/** + * Process Markdown `
` blocks.
+ */
+showdown.subParser('codeBlocks', function (text, options, globals) {
+
+  text = globals.converter._dispatch('codeBlocks.before', text, options, globals);
+
+  // sentinel workarounds for lack of \A and \Z, safari\khtml bug
+  text += '¨0';
+
+  var pattern = /(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=¨0))/g;
+  text = text.replace(pattern, function (wholeMatch, m1, m2) {
+    var codeblock = m1,
+        nextChar = m2,
+        end = '\n';
+
+    codeblock = showdown.subParser('outdent')(codeblock, options, globals);
+    codeblock = showdown.subParser('encodeCode')(codeblock, options, globals);
+    codeblock = showdown.subParser('detab')(codeblock, options, globals);
+    codeblock = codeblock.replace(/^\n+/g, ''); // trim leading newlines
+    codeblock = codeblock.replace(/\n+$/g, ''); // trim trailing newlines
+
+    if (options.omitExtraWLInCodeBlocks) {
+      end = '';
+    }
+
+    codeblock = '
' + codeblock + end + '
'; + + return showdown.subParser('hashBlock')(codeblock, options, globals) + nextChar; + }); + + // strip sentinel + text = text.replace(/¨0/, ''); + + text = globals.converter._dispatch('codeBlocks.after', text, options, globals); + return text; +}); + +/** + * + * * Backtick quotes are used for spans. + * + * * You can use multiple backticks as the delimiters if you want to + * include literal backticks in the code span. So, this input: + * + * Just type ``foo `bar` baz`` at the prompt. + * + * Will translate to: + * + *

Just type foo `bar` baz at the prompt.

+ * + * There's no arbitrary limit to the number of backticks you + * can use as delimters. If you need three consecutive backticks + * in your code, use four for delimiters, etc. + * + * * You can use spaces to get literal backticks at the edges: + * + * ... type `` `bar` `` ... + * + * Turns to: + * + * ... type `bar` ... + */ +showdown.subParser('codeSpans', function (text, options, globals) { + + text = globals.converter._dispatch('codeSpans.before', text, options, globals); + + if (typeof text === 'undefined') { + text = ''; + } + text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, + function (wholeMatch, m1, m2, m3) { + var c = m3; + c = c.replace(/^([ \t]*)/g, ''); // leading whitespace + c = c.replace(/[ \t]*$/g, ''); // trailing whitespace + c = showdown.subParser('encodeCode')(c, options, globals); + c = m1 + '' + c + ''; + c = showdown.subParser('hashHTMLSpans')(c, options, globals); + return c; + } + ); + + text = globals.converter._dispatch('codeSpans.after', text, options, globals); + return text; +}); + +/** + * Create a full HTML document from the processed markdown + */ +showdown.subParser('completeHTMLDocument', function (text, options, globals) { + + if (!options.completeHTMLDocument) { + return text; + } + + text = globals.converter._dispatch('completeHTMLDocument.before', text, options, globals); + + var doctype = 'html', + doctypeParsed = '\n', + title = '', + charset = '\n', + lang = '', + metadata = ''; + + if (typeof globals.metadata.parsed.doctype !== 'undefined') { + doctypeParsed = '\n'; + doctype = globals.metadata.parsed.doctype.toString().toLowerCase(); + if (doctype === 'html' || doctype === 'html5') { + charset = ''; + } + } + + for (var meta in globals.metadata.parsed) { + if (globals.metadata.parsed.hasOwnProperty(meta)) { + switch (meta.toLowerCase()) { + case 'doctype': + break; + + case 'title': + title = '' + globals.metadata.parsed.title + '\n'; + break; + + case 'charset': + if (doctype === 'html' || doctype === 'html5') { + charset = '\n'; + } else { + charset = '\n'; + } + break; + + case 'language': + case 'lang': + lang = ' lang="' + globals.metadata.parsed[meta] + '"'; + metadata += '\n'; + break; + + default: + metadata += '\n'; + } + } + } + + text = doctypeParsed + '\n\n' + title + charset + metadata + '\n\n' + text.trim() + '\n\n'; + + text = globals.converter._dispatch('completeHTMLDocument.after', text, options, globals); + return text; +}); + +/** + * Convert all tabs to spaces + */ +showdown.subParser('detab', function (text, options, globals) { + text = globals.converter._dispatch('detab.before', text, options, globals); + + // expand first n-1 tabs + text = text.replace(/\t(?=\t)/g, ' '); // g_tab_width + + // replace the nth with two sentinels + text = text.replace(/\t/g, '¨A¨B'); + + // use the sentinel to anchor our regex so it doesn't explode + text = text.replace(/¨B(.+?)¨A/g, function (wholeMatch, m1) { + var leadingText = m1, + numSpaces = 4 - leadingText.length % 4; // g_tab_width + + // there *must* be a better way to do this: + for (var i = 0; i < numSpaces; i++) { + leadingText += ' '; + } + + return leadingText; + }); + + // clean up sentinels + text = text.replace(/¨A/g, ' '); // g_tab_width + text = text.replace(/¨B/g, ''); + + text = globals.converter._dispatch('detab.after', text, options, globals); + return text; +}); + +showdown.subParser('ellipsis', function (text, options, globals) { + + text = globals.converter._dispatch('ellipsis.before', text, options, globals); + + text = text.replace(/\.\.\./g, '…'); + + text = globals.converter._dispatch('ellipsis.after', text, options, globals); + + return text; +}); + +/** + * Turn emoji codes into emojis + * + * List of supported emojis: https://github.com/showdownjs/showdown/wiki/Emojis + */ +showdown.subParser('emoji', function (text, options, globals) { + + if (!options.emoji) { + return text; + } + + text = globals.converter._dispatch('emoji.before', text, options, globals); + + var emojiRgx = /:([\S]+?):/g; + + text = text.replace(emojiRgx, function (wm, emojiCode) { + if (showdown.helper.emojis.hasOwnProperty(emojiCode)) { + return showdown.helper.emojis[emojiCode]; + } + return wm; + }); + + text = globals.converter._dispatch('emoji.after', text, options, globals); + + return text; +}); + +/** + * Smart processing for ampersands and angle brackets that need to be encoded. + */ +showdown.subParser('encodeAmpsAndAngles', function (text, options, globals) { + text = globals.converter._dispatch('encodeAmpsAndAngles.before', text, options, globals); + + // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: + // http://bumppo.net/projects/amputator/ + text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, '&'); + + // Encode naked <'s + text = text.replace(/<(?![a-z\/?$!])/gi, '<'); + + // Encode < + text = text.replace(/ + text = text.replace(/>/g, '>'); + + text = globals.converter._dispatch('encodeAmpsAndAngles.after', text, options, globals); + return text; +}); + +/** + * Returns the string, with after processing the following backslash escape sequences. + * + * attacklab: The polite way to do this is with the new escapeCharacters() function: + * + * text = escapeCharacters(text,"\\",true); + * text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); + * + * ...but we're sidestepping its use of the (slow) RegExp constructor + * as an optimization for Firefox. This function gets called a LOT. + */ +showdown.subParser('encodeBackslashEscapes', function (text, options, globals) { + text = globals.converter._dispatch('encodeBackslashEscapes.before', text, options, globals); + + text = text.replace(/\\(\\)/g, showdown.helper.escapeCharactersCallback); + text = text.replace(/\\([`*_{}\[\]()>#+.!~=|-])/g, showdown.helper.escapeCharactersCallback); + + text = globals.converter._dispatch('encodeBackslashEscapes.after', text, options, globals); + return text; +}); + +/** + * Encode/escape certain characters inside Markdown code runs. + * The point is that in code, these characters are literals, + * and lose their special Markdown meanings. + */ +showdown.subParser('encodeCode', function (text, options, globals) { + + text = globals.converter._dispatch('encodeCode.before', text, options, globals); + + // Encode all ampersands; HTML entities are not + // entities within a Markdown code span. + text = text + .replace(/&/g, '&') + // Do the angle bracket song and dance: + .replace(//g, '>') + // Now, escape characters that are magic in Markdown: + .replace(/([*_{}\[\]\\=~-])/g, showdown.helper.escapeCharactersCallback); + + text = globals.converter._dispatch('encodeCode.after', text, options, globals); + return text; +}); + +/** + * Within tags -- meaning between < and > -- encode [\ ` * _ ~ =] so they + * don't conflict with their use in Markdown for code, italics and strong. + */ +showdown.subParser('escapeSpecialCharsWithinTagAttributes', function (text, options, globals) { + text = globals.converter._dispatch('escapeSpecialCharsWithinTagAttributes.before', text, options, globals); + + // Build a regex to find HTML tags. + var tags = /<\/?[a-z\d_:-]+(?:[\s]+[\s\S]+?)?>/gi, + comments = /-]|-[^>])(?:[^-]|-[^-])*)--)>/gi; + + text = text.replace(tags, function (wholeMatch) { + return wholeMatch + .replace(/(.)<\/?code>(?=.)/g, '$1`') + .replace(/([\\`*_~=|])/g, showdown.helper.escapeCharactersCallback); + }); + + text = text.replace(comments, function (wholeMatch) { + return wholeMatch + .replace(/([\\`*_~=|])/g, showdown.helper.escapeCharactersCallback); + }); + + text = globals.converter._dispatch('escapeSpecialCharsWithinTagAttributes.after', text, options, globals); + return text; +}); + +/** + * Handle github codeblocks prior to running HashHTML so that + * HTML contained within the codeblock gets escaped properly + * Example: + * ```ruby + * def hello_world(x) + * puts "Hello, #{x}" + * end + * ``` + */ +showdown.subParser('githubCodeBlocks', function (text, options, globals) { + + // early exit if option is not enabled + if (!options.ghCodeBlocks) { + return text; + } + + text = globals.converter._dispatch('githubCodeBlocks.before', text, options, globals); + + text += '¨0'; + + text = text.replace(/(?:^|\n)(?: {0,3})(```+|~~~+)(?: *)([^\s`~]*)\n([\s\S]*?)\n(?: {0,3})\1/g, function (wholeMatch, delim, language, codeblock) { + var end = (options.omitExtraWLInCodeBlocks) ? '' : '\n'; + + // First parse the github code block + codeblock = showdown.subParser('encodeCode')(codeblock, options, globals); + codeblock = showdown.subParser('detab')(codeblock, options, globals); + codeblock = codeblock.replace(/^\n+/g, ''); // trim leading newlines + codeblock = codeblock.replace(/\n+$/g, ''); // trim trailing whitespace + + codeblock = '
' + codeblock + end + '
'; + + codeblock = showdown.subParser('hashBlock')(codeblock, options, globals); + + // Since GHCodeblocks can be false positives, we need to + // store the primitive text and the parsed text in a global var, + // and then return a token + return '\n\n¨G' + (globals.ghCodeBlocks.push({text: wholeMatch, codeblock: codeblock}) - 1) + 'G\n\n'; + }); + + // attacklab: strip sentinel + text = text.replace(/¨0/, ''); + + return globals.converter._dispatch('githubCodeBlocks.after', text, options, globals); +}); + +showdown.subParser('hashBlock', function (text, options, globals) { + text = globals.converter._dispatch('hashBlock.before', text, options, globals); + text = text.replace(/(^\n+|\n+$)/g, ''); + text = '\n\n¨K' + (globals.gHtmlBlocks.push(text) - 1) + 'K\n\n'; + text = globals.converter._dispatch('hashBlock.after', text, options, globals); + return text; +}); + +/** + * Hash and escape elements that should not be parsed as markdown + */ +showdown.subParser('hashCodeTags', function (text, options, globals) { + text = globals.converter._dispatch('hashCodeTags.before', text, options, globals); + + var repFunc = function (wholeMatch, match, left, right) { + var codeblock = left + showdown.subParser('encodeCode')(match, options, globals) + right; + return '¨C' + (globals.gHtmlSpans.push(codeblock) - 1) + 'C'; + }; + + // Hash naked + text = showdown.helper.replaceRecursiveRegExp(text, repFunc, ']*>', '', 'gim'); + + text = globals.converter._dispatch('hashCodeTags.after', text, options, globals); + return text; +}); + +showdown.subParser('hashElement', function (text, options, globals) { + + return function (wholeMatch, m1) { + var blockText = m1; + + // Undo double lines + blockText = blockText.replace(/\n\n/g, '\n'); + blockText = blockText.replace(/^\n/, ''); + + // strip trailing blank lines + blockText = blockText.replace(/\n+$/g, ''); + + // Replace the element text with a marker ("¨KxK" where x is its key) + blockText = '\n\n¨K' + (globals.gHtmlBlocks.push(blockText) - 1) + 'K\n\n'; + + return blockText; + }; +}); + +showdown.subParser('hashHTMLBlocks', function (text, options, globals) { + text = globals.converter._dispatch('hashHTMLBlocks.before', text, options, globals); + + var blockTags = [ + 'pre', + 'div', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'blockquote', + 'table', + 'dl', + 'ol', + 'ul', + 'script', + 'noscript', + 'form', + 'fieldset', + 'iframe', + 'math', + 'style', + 'section', + 'header', + 'footer', + 'nav', + 'article', + 'aside', + 'address', + 'audio', + 'canvas', + 'figure', + 'hgroup', + 'output', + 'video', + 'p' + ], + repFunc = function (wholeMatch, match, left, right) { + var txt = wholeMatch; + // check if this html element is marked as markdown + // if so, it's contents should be parsed as markdown + if (left.search(/\bmarkdown\b/) !== -1) { + txt = left + globals.converter.makeHtml(match) + right; + } + return '\n\n¨K' + (globals.gHtmlBlocks.push(txt) - 1) + 'K\n\n'; + }; + + if (options.backslashEscapesHTMLTags) { + // encode backslash escaped HTML tags + text = text.replace(/\\<(\/?[^>]+?)>/g, function (wm, inside) { + return '<' + inside + '>'; + }); + } + + // hash HTML Blocks + for (var i = 0; i < blockTags.length; ++i) { + + var opTagPos, + rgx1 = new RegExp('^ {0,3}(<' + blockTags[i] + '\\b[^>]*>)', 'im'), + patLeft = '<' + blockTags[i] + '\\b[^>]*>', + patRight = ''; + // 1. Look for the first position of the first opening HTML tag in the text + while ((opTagPos = showdown.helper.regexIndexOf(text, rgx1)) !== -1) { + + // if the HTML tag is \ escaped, we need to escape it and break + + + //2. Split the text in that position + var subTexts = showdown.helper.splitAtIndex(text, opTagPos), + //3. Match recursively + newSubText1 = showdown.helper.replaceRecursiveRegExp(subTexts[1], repFunc, patLeft, patRight, 'im'); + + // prevent an infinite loop + if (newSubText1 === subTexts[1]) { + break; + } + text = subTexts[0].concat(newSubText1); + } + } + // HR SPECIAL CASE + text = text.replace(/(\n {0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, + showdown.subParser('hashElement')(text, options, globals)); + + // Special case for standalone HTML comments + text = showdown.helper.replaceRecursiveRegExp(text, function (txt) { + return '\n\n¨K' + (globals.gHtmlBlocks.push(txt) - 1) + 'K\n\n'; + }, '^ {0,3}', 'gm'); + + // PHP and ASP-style processor instructions ( and <%...%>) + text = text.replace(/(?:\n\n)( {0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, + showdown.subParser('hashElement')(text, options, globals)); + + text = globals.converter._dispatch('hashHTMLBlocks.after', text, options, globals); + return text; +}); + +/** + * Hash span elements that should not be parsed as markdown + */ +showdown.subParser('hashHTMLSpans', function (text, options, globals) { + text = globals.converter._dispatch('hashHTMLSpans.before', text, options, globals); + + function hashHTMLSpan (html) { + return '¨C' + (globals.gHtmlSpans.push(html) - 1) + 'C'; + } + + // Hash Self Closing tags + text = text.replace(/<[^>]+?\/>/gi, function (wm) { + return hashHTMLSpan(wm); + }); + + // Hash tags without properties + text = text.replace(/<([^>]+?)>[\s\S]*?<\/\1>/g, function (wm) { + return hashHTMLSpan(wm); + }); + + // Hash tags with properties + text = text.replace(/<([^>]+?)\s[^>]+?>[\s\S]*?<\/\1>/g, function (wm) { + return hashHTMLSpan(wm); + }); + + // Hash self closing tags without /> + text = text.replace(/<[^>]+?>/gi, function (wm) { + return hashHTMLSpan(wm); + }); + + /*showdown.helper.matchRecursiveRegExp(text, ']*>', '', 'gi');*/ + + text = globals.converter._dispatch('hashHTMLSpans.after', text, options, globals); + return text; +}); + +/** + * Unhash HTML spans + */ +showdown.subParser('unhashHTMLSpans', function (text, options, globals) { + text = globals.converter._dispatch('unhashHTMLSpans.before', text, options, globals); + + for (var i = 0; i < globals.gHtmlSpans.length; ++i) { + var repText = globals.gHtmlSpans[i], + // limiter to prevent infinite loop (assume 10 as limit for recurse) + limit = 0; + + while (/¨C(\d+)C/.test(repText)) { + var num = RegExp.$1; + repText = repText.replace('¨C' + num + 'C', globals.gHtmlSpans[num]); + if (limit === 10) { + console.error('maximum nesting of 10 spans reached!!!'); + break; + } + ++limit; + } + text = text.replace('¨C' + i + 'C', repText); + } + + text = globals.converter._dispatch('unhashHTMLSpans.after', text, options, globals); + return text; +}); + +/** + * Hash and escape
 elements that should not be parsed as markdown
+ */
+showdown.subParser('hashPreCodeTags', function (text, options, globals) {
+  text = globals.converter._dispatch('hashPreCodeTags.before', text, options, globals);
+
+  var repFunc = function (wholeMatch, match, left, right) {
+    // encode html entities
+    var codeblock = left + showdown.subParser('encodeCode')(match, options, globals) + right;
+    return '\n\n¨G' + (globals.ghCodeBlocks.push({text: wholeMatch, codeblock: codeblock}) - 1) + 'G\n\n';
+  };
+
+  // Hash 

+  text = showdown.helper.replaceRecursiveRegExp(text, repFunc, '^ {0,3}]*>\\s*]*>', '^ {0,3}\\s*
', 'gim'); + + text = globals.converter._dispatch('hashPreCodeTags.after', text, options, globals); + return text; +}); + +showdown.subParser('headers', function (text, options, globals) { + + text = globals.converter._dispatch('headers.before', text, options, globals); + + var headerLevelStart = (isNaN(parseInt(options.headerLevelStart))) ? 1 : parseInt(options.headerLevelStart), + + // Set text-style headers: + // Header 1 + // ======== + // + // Header 2 + // -------- + // + setextRegexH1 = (options.smoothLivePreview) ? /^(.+)[ \t]*\n={2,}[ \t]*\n+/gm : /^(.+)[ \t]*\n=+[ \t]*\n+/gm, + setextRegexH2 = (options.smoothLivePreview) ? /^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm : /^(.+)[ \t]*\n-+[ \t]*\n+/gm; + + text = text.replace(setextRegexH1, function (wholeMatch, m1) { + + var spanGamut = showdown.subParser('spanGamut')(m1, options, globals), + hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"', + hLevel = headerLevelStart, + hashBlock = '' + spanGamut + ''; + return showdown.subParser('hashBlock')(hashBlock, options, globals); + }); + + text = text.replace(setextRegexH2, function (matchFound, m1) { + var spanGamut = showdown.subParser('spanGamut')(m1, options, globals), + hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"', + hLevel = headerLevelStart + 1, + hashBlock = '' + spanGamut + ''; + return showdown.subParser('hashBlock')(hashBlock, options, globals); + }); + + // atx-style headers: + // # Header 1 + // ## Header 2 + // ## Header 2 with closing hashes ## + // ... + // ###### Header 6 + // + var atxStyle = (options.requireSpaceBeforeHeadingText) ? /^(#{1,6})[ \t]+(.+?)[ \t]*#*\n+/gm : /^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm; + + text = text.replace(atxStyle, function (wholeMatch, m1, m2) { + var hText = m2; + if (options.customizedHeaderId) { + hText = m2.replace(/\s?\{([^{]+?)}\s*$/, ''); + } + + var span = showdown.subParser('spanGamut')(hText, options, globals), + hID = (options.noHeaderId) ? '' : ' id="' + headerId(m2) + '"', + hLevel = headerLevelStart - 1 + m1.length, + header = '' + span + ''; + + return showdown.subParser('hashBlock')(header, options, globals); + }); + + function headerId (m) { + var title, + prefix; + + // It is separate from other options to allow combining prefix and customized + if (options.customizedHeaderId) { + var match = m.match(/\{([^{]+?)}\s*$/); + if (match && match[1]) { + m = match[1]; + } + } + + title = m; + + // Prefix id to prevent causing inadvertent pre-existing style matches. + if (showdown.helper.isString(options.prefixHeaderId)) { + prefix = options.prefixHeaderId; + } else if (options.prefixHeaderId === true) { + prefix = 'section-'; + } else { + prefix = ''; + } + + if (!options.rawPrefixHeaderId) { + title = prefix + title; + } + + if (options.ghCompatibleHeaderId) { + title = title + .replace(/ /g, '-') + // replace previously escaped chars (&, ¨ and $) + .replace(/&/g, '') + .replace(/¨T/g, '') + .replace(/¨D/g, '') + // replace rest of the chars (&~$ are repeated as they might have been escaped) + // borrowed from github's redcarpet (some they should produce similar results) + .replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g, '') + .toLowerCase(); + } else if (options.rawHeaderId) { + title = title + .replace(/ /g, '-') + // replace previously escaped chars (&, ¨ and $) + .replace(/&/g, '&') + .replace(/¨T/g, '¨') + .replace(/¨D/g, '$') + // replace " and ' + .replace(/["']/g, '-') + .toLowerCase(); + } else { + title = title + .replace(/[^\w]/g, '') + .toLowerCase(); + } + + if (options.rawPrefixHeaderId) { + title = prefix + title; + } + + if (globals.hashLinkCounts[title]) { + title = title + '-' + (globals.hashLinkCounts[title]++); + } else { + globals.hashLinkCounts[title] = 1; + } + return title; + } + + text = globals.converter._dispatch('headers.after', text, options, globals); + return text; +}); + +/** + * Turn Markdown link shortcuts into XHTML tags. + */ +showdown.subParser('horizontalRule', function (text, options, globals) { + text = globals.converter._dispatch('horizontalRule.before', text, options, globals); + + var key = showdown.subParser('hashBlock')('
', options, globals); + text = text.replace(/^ {0,2}( ?-){3,}[ \t]*$/gm, key); + text = text.replace(/^ {0,2}( ?\*){3,}[ \t]*$/gm, key); + text = text.replace(/^ {0,2}( ?_){3,}[ \t]*$/gm, key); + + text = globals.converter._dispatch('horizontalRule.after', text, options, globals); + return text; +}); + +/** + * Turn Markdown image shortcuts into tags. + */ +showdown.subParser('images', function (text, options, globals) { + + text = globals.converter._dispatch('images.before', text, options, globals); + + var inlineRegExp = /!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g, + crazyRegExp = /!\[([^\]]*?)][ \t]*()\([ \t]?<([^>]*)>(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(?:(["'])([^"]*?)\6))?[ \t]?\)/g, + base64RegExp = /!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g, + referenceRegExp = /!\[([^\]]*?)] ?(?:\n *)?\[([\s\S]*?)]()()()()()/g, + refShortcutRegExp = /!\[([^\[\]]+)]()()()()()/g; + + function writeImageTagBase64 (wholeMatch, altText, linkId, url, width, height, m5, title) { + url = url.replace(/\s/g, ''); + return writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title); + } + + function writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title) { + + var gUrls = globals.gUrls, + gTitles = globals.gTitles, + gDims = globals.gDimensions; + + linkId = linkId.toLowerCase(); + + if (!title) { + title = ''; + } + // Special case for explicit empty url + if (wholeMatch.search(/\(? ?(['"].*['"])?\)$/m) > -1) { + url = ''; + + } else if (url === '' || url === null) { + if (linkId === '' || linkId === null) { + // lower-case and turn embedded newlines into spaces + linkId = altText.toLowerCase().replace(/ ?\n/g, ' '); + } + url = '#' + linkId; + + if (!showdown.helper.isUndefined(gUrls[linkId])) { + url = gUrls[linkId]; + if (!showdown.helper.isUndefined(gTitles[linkId])) { + title = gTitles[linkId]; + } + if (!showdown.helper.isUndefined(gDims[linkId])) { + width = gDims[linkId].width; + height = gDims[linkId].height; + } + } else { + return wholeMatch; + } + } + + altText = altText + .replace(/"/g, '"') + //altText = showdown.helper.escapeCharacters(altText, '*_', false); + .replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback); + //url = showdown.helper.escapeCharacters(url, '*_', false); + url = url.replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback); + var result = '' + altText + 'x "optional title") + + // base64 encoded images + text = text.replace(base64RegExp, writeImageTagBase64); + + // cases with crazy urls like ./image/cat1).png + text = text.replace(crazyRegExp, writeImageTag); + + // normal cases + text = text.replace(inlineRegExp, writeImageTag); + + // handle reference-style shortcuts: ![img text] + text = text.replace(refShortcutRegExp, writeImageTag); + + text = globals.converter._dispatch('images.after', text, options, globals); + return text; +}); + +showdown.subParser('italicsAndBold', function (text, options, globals) { + + text = globals.converter._dispatch('italicsAndBold.before', text, options, globals); + + // it's faster to have 3 separate regexes for each case than have just one + // because of backtracing, in some cases, it could lead to an exponential effect + // called "catastrophic backtrace". Ominous! + + function parseInside (txt, left, right) { + /* + if (options.simplifiedAutoLink) { + txt = showdown.subParser('simplifiedAutoLinks')(txt, options, globals); + } + */ + return left + txt + right; + } + + // Parse underscores + if (options.literalMidWordUnderscores) { + text = text.replace(/\b___(\S[\s\S]*?)___\b/g, function (wm, txt) { + return parseInside (txt, '', ''); + }); + text = text.replace(/\b__(\S[\s\S]*?)__\b/g, function (wm, txt) { + return parseInside (txt, '', ''); + }); + text = text.replace(/\b_(\S[\s\S]*?)_\b/g, function (wm, txt) { + return parseInside (txt, '', ''); + }); + } else { + text = text.replace(/___(\S[\s\S]*?)___/g, function (wm, m) { + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + text = text.replace(/__(\S[\s\S]*?)__/g, function (wm, m) { + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + text = text.replace(/_([^\s_][\s\S]*?)_/g, function (wm, m) { + // !/^_[^_]/.test(m) - test if it doesn't start with __ (since it seems redundant, we removed it) + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + } + + // Now parse asterisks + if (options.literalMidWordAsterisks) { + text = text.replace(/([^*]|^)\B\*\*\*(\S[\s\S]*?)\*\*\*\B(?!\*)/g, function (wm, lead, txt) { + return parseInside (txt, lead + '', ''); + }); + text = text.replace(/([^*]|^)\B\*\*(\S[\s\S]*?)\*\*\B(?!\*)/g, function (wm, lead, txt) { + return parseInside (txt, lead + '', ''); + }); + text = text.replace(/([^*]|^)\B\*(\S[\s\S]*?)\*\B(?!\*)/g, function (wm, lead, txt) { + return parseInside (txt, lead + '', ''); + }); + } else { + text = text.replace(/\*\*\*(\S[\s\S]*?)\*\*\*/g, function (wm, m) { + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + text = text.replace(/\*\*(\S[\s\S]*?)\*\*/g, function (wm, m) { + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + text = text.replace(/\*([^\s*][\s\S]*?)\*/g, function (wm, m) { + // !/^\*[^*]/.test(m) - test if it doesn't start with ** (since it seems redundant, we removed it) + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + } + + + text = globals.converter._dispatch('italicsAndBold.after', text, options, globals); + return text; +}); + +/** + * Form HTML ordered (numbered) and unordered (bulleted) lists. + */ +showdown.subParser('lists', function (text, options, globals) { + + /** + * Process the contents of a single ordered or unordered list, splitting it + * into individual list items. + * @param {string} listStr + * @param {boolean} trimTrailing + * @returns {string} + */ + function processListItems (listStr, trimTrailing) { + // The $g_list_level global keeps track of when we're inside a list. + // Each time we enter a list, we increment it; when we leave a list, + // we decrement. If it's zero, we're not in a list anymore. + // + // We do this because when we're not inside a list, we want to treat + // something like this: + // + // I recommend upgrading to version + // 8. Oops, now this line is treated + // as a sub-list. + // + // As a single paragraph, despite the fact that the second line starts + // with a digit-period-space sequence. + // + // Whereas when we're inside a list (or sub-list), that line will be + // treated as the start of a sub-list. What a kludge, huh? This is + // an aspect of Markdown's syntax that's hard to parse perfectly + // without resorting to mind-reading. Perhaps the solution is to + // change the syntax rules such that sub-lists must start with a + // starting cardinal number; e.g. "1." or "a.". + globals.gListLevel++; + + // trim trailing blank lines: + listStr = listStr.replace(/\n{2,}$/, '\n'); + + // attacklab: add sentinel to emulate \z + listStr += '¨0'; + + var rgx = /(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0| {0,3}([*+-]|\d+[.])[ \t]+))/gm, + isParagraphed = (/\n[ \t]*\n(?!¨0)/.test(listStr)); + + // Since version 1.5, nesting sublists requires 4 spaces (or 1 tab) indentation, + // which is a syntax breaking change + // activating this option reverts to old behavior + if (options.disableForced4SpacesIndentedSublists) { + rgx = /(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0|\2([*+-]|\d+[.])[ \t]+))/gm; + } + + listStr = listStr.replace(rgx, function (wholeMatch, m1, m2, m3, m4, taskbtn, checked) { + checked = (checked && checked.trim() !== ''); + + var item = showdown.subParser('outdent')(m4, options, globals), + bulletStyle = ''; + + // Support for github tasklists + if (taskbtn && options.tasklists) { + bulletStyle = ' class="task-list-item" style="list-style-type: none;"'; + item = item.replace(/^[ \t]*\[(x|X| )?]/m, function () { + var otp = '
  • a
  • + // instead of: + //
    • - - a
    + // So, to prevent it, we will put a marker (¨A)in the beginning of the line + // Kind of hackish/monkey patching, but seems more effective than overcomplicating the list parser + item = item.replace(/^([-*+]|\d\.)[ \t]+[\S\n ]*/g, function (wm2) { + return '¨A' + wm2; + }); + + // m1 - Leading line or + // Has a double return (multi paragraph) or + // Has sublist + if (m1 || (item.search(/\n{2,}/) > -1)) { + item = showdown.subParser('githubCodeBlocks')(item, options, globals); + item = showdown.subParser('blockGamut')(item, options, globals); + } else { + // Recursion for sub-lists: + item = showdown.subParser('lists')(item, options, globals); + item = item.replace(/\n$/, ''); // chomp(item) + item = showdown.subParser('hashHTMLBlocks')(item, options, globals); + + // Colapse double linebreaks + item = item.replace(/\n\n+/g, '\n\n'); + if (isParagraphed) { + item = showdown.subParser('paragraphs')(item, options, globals); + } else { + item = showdown.subParser('spanGamut')(item, options, globals); + } + } + + // now we need to remove the marker (¨A) + item = item.replace('¨A', ''); + // we can finally wrap the line in list item tags + item = '' + item + '\n'; + + return item; + }); + + // attacklab: strip sentinel + listStr = listStr.replace(/¨0/g, ''); + + globals.gListLevel--; + + if (trimTrailing) { + listStr = listStr.replace(/\s+$/, ''); + } + + return listStr; + } + + function styleStartNumber (list, listType) { + // check if ol and starts by a number different than 1 + if (listType === 'ol') { + var res = list.match(/^ *(\d+)\./); + if (res && res[1] !== '1') { + return ' start="' + res[1] + '"'; + } + } + return ''; + } + + /** + * Check and parse consecutive lists (better fix for issue #142) + * @param {string} list + * @param {string} listType + * @param {boolean} trimTrailing + * @returns {string} + */ + function parseConsecutiveLists (list, listType, trimTrailing) { + // check if we caught 2 or more consecutive lists by mistake + // we use the counterRgx, meaning if listType is UL we look for OL and vice versa + var olRgx = (options.disableForced4SpacesIndentedSublists) ? /^ ?\d+\.[ \t]/gm : /^ {0,3}\d+\.[ \t]/gm, + ulRgx = (options.disableForced4SpacesIndentedSublists) ? /^ ?[*+-][ \t]/gm : /^ {0,3}[*+-][ \t]/gm, + counterRxg = (listType === 'ul') ? olRgx : ulRgx, + result = ''; + + if (list.search(counterRxg) !== -1) { + (function parseCL (txt) { + var pos = txt.search(counterRxg), + style = styleStartNumber(list, listType); + if (pos !== -1) { + // slice + result += '\n\n<' + listType + style + '>\n' + processListItems(txt.slice(0, pos), !!trimTrailing) + '\n'; + + // invert counterType and listType + listType = (listType === 'ul') ? 'ol' : 'ul'; + counterRxg = (listType === 'ul') ? olRgx : ulRgx; + + //recurse + parseCL(txt.slice(pos)); + } else { + result += '\n\n<' + listType + style + '>\n' + processListItems(txt, !!trimTrailing) + '\n'; + } + })(list); + } else { + var style = styleStartNumber(list, listType); + result = '\n\n<' + listType + style + '>\n' + processListItems(list, !!trimTrailing) + '\n'; + } + + return result; + } + + /** Start of list parsing **/ + text = globals.converter._dispatch('lists.before', text, options, globals); + // add sentinel to hack around khtml/safari bug: + // http://bugs.webkit.org/show_bug.cgi?id=11231 + text += '¨0'; + + if (globals.gListLevel) { + text = text.replace(/^(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm, + function (wholeMatch, list, m2) { + var listType = (m2.search(/[*+-]/g) > -1) ? 'ul' : 'ol'; + return parseConsecutiveLists(list, listType, true); + } + ); + } else { + text = text.replace(/(\n\n|^\n?)(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm, + function (wholeMatch, m1, list, m3) { + var listType = (m3.search(/[*+-]/g) > -1) ? 'ul' : 'ol'; + return parseConsecutiveLists(list, listType, false); + } + ); + } + + // strip sentinel + text = text.replace(/¨0/, ''); + text = globals.converter._dispatch('lists.after', text, options, globals); + return text; +}); + +/** + * Parse metadata at the top of the document + */ +showdown.subParser('metadata', function (text, options, globals) { + + if (!options.metadata) { + return text; + } + + text = globals.converter._dispatch('metadata.before', text, options, globals); + + function parseMetadataContents (content) { + // raw is raw so it's not changed in any way + globals.metadata.raw = content; + + // escape chars forbidden in html attributes + // double quotes + content = content + // ampersand first + .replace(/&/g, '&') + // double quotes + .replace(/"/g, '"'); + + content = content.replace(/\n {4}/g, ' '); + content.replace(/^([\S ]+): +([\s\S]+?)$/gm, function (wm, key, value) { + globals.metadata.parsed[key] = value; + return ''; + }); + } + + text = text.replace(/^\s*«««+(\S*?)\n([\s\S]+?)\n»»»+\n/, function (wholematch, format, content) { + parseMetadataContents(content); + return '¨M'; + }); + + text = text.replace(/^\s*---+(\S*?)\n([\s\S]+?)\n---+\n/, function (wholematch, format, content) { + if (format) { + globals.metadata.format = format; + } + parseMetadataContents(content); + return '¨M'; + }); + + text = text.replace(/¨M/g, ''); + + text = globals.converter._dispatch('metadata.after', text, options, globals); + return text; +}); + +/** + * Remove one level of line-leading tabs or spaces + */ +showdown.subParser('outdent', function (text, options, globals) { + text = globals.converter._dispatch('outdent.before', text, options, globals); + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + text = text.replace(/^(\t|[ ]{1,4})/gm, '¨0'); // attacklab: g_tab_width + + // attacklab: clean up hack + text = text.replace(/¨0/g, ''); + + text = globals.converter._dispatch('outdent.after', text, options, globals); + return text; +}); + +/** + * + */ +showdown.subParser('paragraphs', function (text, options, globals) { + + text = globals.converter._dispatch('paragraphs.before', text, options, globals); + // Strip leading and trailing lines: + text = text.replace(/^\n+/g, ''); + text = text.replace(/\n+$/g, ''); + + var grafs = text.split(/\n{2,}/g), + grafsOut = [], + end = grafs.length; // Wrap

    tags + + for (var i = 0; i < end; i++) { + var str = grafs[i]; + // if this is an HTML marker, copy it + if (str.search(/¨(K|G)(\d+)\1/g) >= 0) { + grafsOut.push(str); + + // test for presence of characters to prevent empty lines being parsed + // as paragraphs (resulting in undesired extra empty paragraphs) + } else if (str.search(/\S/) >= 0) { + str = showdown.subParser('spanGamut')(str, options, globals); + str = str.replace(/^([ \t]*)/g, '

    '); + str += '

    '; + grafsOut.push(str); + } + } + + /** Unhashify HTML blocks */ + end = grafsOut.length; + for (i = 0; i < end; i++) { + var blockText = '', + grafsOutIt = grafsOut[i], + codeFlag = false; + // if this is a marker for an html block... + // use RegExp.test instead of string.search because of QML bug + while (/¨(K|G)(\d+)\1/.test(grafsOutIt)) { + var delim = RegExp.$1, + num = RegExp.$2; + + if (delim === 'K') { + blockText = globals.gHtmlBlocks[num]; + } else { + // we need to check if ghBlock is a false positive + if (codeFlag) { + // use encoded version of all text + blockText = showdown.subParser('encodeCode')(globals.ghCodeBlocks[num].text, options, globals); + } else { + blockText = globals.ghCodeBlocks[num].codeblock; + } + } + blockText = blockText.replace(/\$/g, '$$$$'); // Escape any dollar signs + + grafsOutIt = grafsOutIt.replace(/(\n\n)?¨(K|G)\d+\2(\n\n)?/, blockText); + // Check if grafsOutIt is a pre->code + if (/^]*>\s*]*>/.test(grafsOutIt)) { + codeFlag = true; + } + } + grafsOut[i] = grafsOutIt; + } + text = grafsOut.join('\n'); + // Strip leading and trailing lines: + text = text.replace(/^\n+/g, ''); + text = text.replace(/\n+$/g, ''); + return globals.converter._dispatch('paragraphs.after', text, options, globals); +}); + +/** + * Run extension + */ +showdown.subParser('runExtension', function (ext, text, options, globals) { + + if (ext.filter) { + text = ext.filter(text, globals.converter, options); + + } else if (ext.regex) { + // TODO remove this when old extension loading mechanism is deprecated + var re = ext.regex; + if (!(re instanceof RegExp)) { + re = new RegExp(re, 'g'); + } + text = text.replace(re, ext.replace); + } + + return text; +}); + +/** + * These are all the transformations that occur *within* block-level + * tags like paragraphs, headers, and list items. + */ +showdown.subParser('spanGamut', function (text, options, globals) { + + text = globals.converter._dispatch('spanGamut.before', text, options, globals); + text = showdown.subParser('codeSpans')(text, options, globals); + text = showdown.subParser('escapeSpecialCharsWithinTagAttributes')(text, options, globals); + text = showdown.subParser('encodeBackslashEscapes')(text, options, globals); + + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + text = showdown.subParser('images')(text, options, globals); + text = showdown.subParser('anchors')(text, options, globals); + + // Make links out of things like `` + // Must come after anchors, because you can use < and > + // delimiters in inline links like [this](). + text = showdown.subParser('autoLinks')(text, options, globals); + text = showdown.subParser('simplifiedAutoLinks')(text, options, globals); + text = showdown.subParser('emoji')(text, options, globals); + text = showdown.subParser('underline')(text, options, globals); + text = showdown.subParser('italicsAndBold')(text, options, globals); + text = showdown.subParser('strikethrough')(text, options, globals); + text = showdown.subParser('ellipsis')(text, options, globals); + + // we need to hash HTML tags inside spans + text = showdown.subParser('hashHTMLSpans')(text, options, globals); + + // now we encode amps and angles + text = showdown.subParser('encodeAmpsAndAngles')(text, options, globals); + + // Do hard breaks + if (options.simpleLineBreaks) { + // GFM style hard breaks + // only add line breaks if the text does not contain a block (special case for lists) + if (!/\n\n¨K/.test(text)) { + text = text.replace(/\n+/g, '
    \n'); + } + } else { + // Vanilla hard breaks + text = text.replace(/ +\n/g, '
    \n'); + } + + text = globals.converter._dispatch('spanGamut.after', text, options, globals); + return text; +}); + +showdown.subParser('strikethrough', function (text, options, globals) { + + function parseInside (txt) { + if (options.simplifiedAutoLink) { + txt = showdown.subParser('simplifiedAutoLinks')(txt, options, globals); + } + return '' + txt + ''; + } + + if (options.strikethrough) { + text = globals.converter._dispatch('strikethrough.before', text, options, globals); + text = text.replace(/(?:~){2}([\s\S]+?)(?:~){2}/g, function (wm, txt) { return parseInside(txt); }); + text = globals.converter._dispatch('strikethrough.after', text, options, globals); + } + + return text; +}); + +/** + * Strips link definitions from text, stores the URLs and titles in + * hash references. + * Link defs are in the form: ^[id]: url "optional title" + */ +showdown.subParser('stripLinkDefinitions', function (text, options, globals) { + + var regex = /^ {0,3}\[(.+)]:[ \t]*\n?[ \t]*\s]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=¨0))/gm, + base64Regex = /^ {0,3}\[(.+)]:[ \t]*\n?[ \t]*?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n\n|(?=¨0)|(?=\n\[))/gm; + + // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug + text += '¨0'; + + var replaceFunc = function (wholeMatch, linkId, url, width, height, blankLines, title) { + linkId = linkId.toLowerCase(); + if (url.match(/^data:.+?\/.+?;base64,/)) { + // remove newlines + globals.gUrls[linkId] = url.replace(/\s/g, ''); + } else { + globals.gUrls[linkId] = showdown.subParser('encodeAmpsAndAngles')(url, options, globals); // Link IDs are case-insensitive + } + + if (blankLines) { + // Oops, found blank lines, so it's not a title. + // Put back the parenthetical statement we stole. + return blankLines + title; + + } else { + if (title) { + globals.gTitles[linkId] = title.replace(/"|'/g, '"'); + } + if (options.parseImgDimensions && width && height) { + globals.gDimensions[linkId] = { + width: width, + height: height + }; + } + } + // Completely remove the definition from the text + return ''; + }; + + // first we try to find base64 link references + text = text.replace(base64Regex, replaceFunc); + + text = text.replace(regex, replaceFunc); + + // attacklab: strip sentinel + text = text.replace(/¨0/, ''); + + return text; +}); + +showdown.subParser('tables', function (text, options, globals) { + + if (!options.tables) { + return text; + } + + var tableRgx = /^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:[-=]){2,}[\s\S]+?(?:\n\n|¨0)/gm, + //singeColTblRgx = /^ {0,3}\|.+\|\n {0,3}\|[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*\n(?: {0,3}\|.+\|\n)+(?:\n\n|¨0)/gm; + singeColTblRgx = /^ {0,3}\|.+\|[ \t]*\n {0,3}\|[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*\n( {0,3}\|.+\|[ \t]*\n)*(?:\n|¨0)/gm; + + function parseStyles (sLine) { + if (/^:[ \t]*--*$/.test(sLine)) { + return ' style="text-align:left;"'; + } else if (/^--*[ \t]*:[ \t]*$/.test(sLine)) { + return ' style="text-align:right;"'; + } else if (/^:[ \t]*--*[ \t]*:$/.test(sLine)) { + return ' style="text-align:center;"'; + } else { + return ''; + } + } + + function parseHeaders (header, style) { + var id = ''; + header = header.trim(); + // support both tablesHeaderId and tableHeaderId due to error in documentation so we don't break backwards compatibility + if (options.tablesHeaderId || options.tableHeaderId) { + id = ' id="' + header.replace(/ /g, '_').toLowerCase() + '"'; + } + header = showdown.subParser('spanGamut')(header, options, globals); + + return '' + header + '\n'; + } + + function parseCells (cell, style) { + var subText = showdown.subParser('spanGamut')(cell, options, globals); + return '' + subText + '\n'; + } + + function buildTable (headers, cells) { + var tb = '\n\n\n', + tblLgn = headers.length; + + for (var i = 0; i < tblLgn; ++i) { + tb += headers[i]; + } + tb += '\n\n\n'; + + for (i = 0; i < cells.length; ++i) { + tb += '\n'; + for (var ii = 0; ii < tblLgn; ++ii) { + tb += cells[i][ii]; + } + tb += '\n'; + } + tb += '\n
    \n'; + return tb; + } + + function parseTable (rawTable) { + var i, tableLines = rawTable.split('\n'); + + for (i = 0; i < tableLines.length; ++i) { + // strip wrong first and last column if wrapped tables are used + if (/^ {0,3}\|/.test(tableLines[i])) { + tableLines[i] = tableLines[i].replace(/^ {0,3}\|/, ''); + } + if (/\|[ \t]*$/.test(tableLines[i])) { + tableLines[i] = tableLines[i].replace(/\|[ \t]*$/, ''); + } + // parse code spans first, but we only support one line code spans + tableLines[i] = showdown.subParser('codeSpans')(tableLines[i], options, globals); + } + + var rawHeaders = tableLines[0].split('|').map(function (s) { return s.trim();}), + rawStyles = tableLines[1].split('|').map(function (s) { return s.trim();}), + rawCells = [], + headers = [], + styles = [], + cells = []; + + tableLines.shift(); + tableLines.shift(); + + for (i = 0; i < tableLines.length; ++i) { + if (tableLines[i].trim() === '') { + continue; + } + rawCells.push( + tableLines[i] + .split('|') + .map(function (s) { + return s.trim(); + }) + ); + } + + if (rawHeaders.length < rawStyles.length) { + return rawTable; + } + + for (i = 0; i < rawStyles.length; ++i) { + styles.push(parseStyles(rawStyles[i])); + } + + for (i = 0; i < rawHeaders.length; ++i) { + if (showdown.helper.isUndefined(styles[i])) { + styles[i] = ''; + } + headers.push(parseHeaders(rawHeaders[i], styles[i])); + } + + for (i = 0; i < rawCells.length; ++i) { + var row = []; + for (var ii = 0; ii < headers.length; ++ii) { + if (showdown.helper.isUndefined(rawCells[i][ii])) ; + row.push(parseCells(rawCells[i][ii], styles[ii])); + } + cells.push(row); + } + + return buildTable(headers, cells); + } + + text = globals.converter._dispatch('tables.before', text, options, globals); + + // find escaped pipe characters + text = text.replace(/\\(\|)/g, showdown.helper.escapeCharactersCallback); + + // parse multi column tables + text = text.replace(tableRgx, parseTable); + + // parse one column tables + text = text.replace(singeColTblRgx, parseTable); + + text = globals.converter._dispatch('tables.after', text, options, globals); + + return text; +}); + +showdown.subParser('underline', function (text, options, globals) { + + if (!options.underline) { + return text; + } + + text = globals.converter._dispatch('underline.before', text, options, globals); + + if (options.literalMidWordUnderscores) { + text = text.replace(/\b___(\S[\s\S]*?)___\b/g, function (wm, txt) { + return '' + txt + ''; + }); + text = text.replace(/\b__(\S[\s\S]*?)__\b/g, function (wm, txt) { + return '' + txt + ''; + }); + } else { + text = text.replace(/___(\S[\s\S]*?)___/g, function (wm, m) { + return (/\S$/.test(m)) ? '' + m + '' : wm; + }); + text = text.replace(/__(\S[\s\S]*?)__/g, function (wm, m) { + return (/\S$/.test(m)) ? '' + m + '' : wm; + }); + } + + // escape remaining underscores to prevent them being parsed by italic and bold + text = text.replace(/(_)/g, showdown.helper.escapeCharactersCallback); + + text = globals.converter._dispatch('underline.after', text, options, globals); + + return text; +}); + +/** + * Swap back in all the special characters we've hidden. + */ +showdown.subParser('unescapeSpecialChars', function (text, options, globals) { + text = globals.converter._dispatch('unescapeSpecialChars.before', text, options, globals); + + text = text.replace(/¨E(\d+)E/g, function (wholeMatch, m1) { + var charCodeToReplace = parseInt(m1); + return String.fromCharCode(charCodeToReplace); + }); + + text = globals.converter._dispatch('unescapeSpecialChars.after', text, options, globals); + return text; +}); + +showdown.subParser('makeMarkdown.blockquote', function (node, globals) { + + var txt = ''; + if (node.hasChildNodes()) { + var children = node.childNodes, + childrenLength = children.length; + + for (var i = 0; i < childrenLength; ++i) { + var innerTxt = showdown.subParser('makeMarkdown.node')(children[i], globals); + + if (innerTxt === '') { + continue; + } + txt += innerTxt; + } + } + // cleanup + txt = txt.trim(); + txt = '> ' + txt.split('\n').join('\n> '); + return txt; +}); + +showdown.subParser('makeMarkdown.codeBlock', function (node, globals) { + + var lang = node.getAttribute('language'), + num = node.getAttribute('precodenum'); + return '```' + lang + '\n' + globals.preList[num] + '\n```'; +}); + +showdown.subParser('makeMarkdown.codeSpan', function (node) { + + return '`' + node.innerHTML + '`'; +}); + +showdown.subParser('makeMarkdown.emphasis', function (node, globals) { + + var txt = ''; + if (node.hasChildNodes()) { + txt += '*'; + var children = node.childNodes, + childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + txt += '*'; + } + return txt; +}); + +showdown.subParser('makeMarkdown.header', function (node, globals, headerLevel) { + + var headerMark = new Array(headerLevel + 1).join('#'), + txt = ''; + + if (node.hasChildNodes()) { + txt = headerMark + ' '; + var children = node.childNodes, + childrenLength = children.length; + + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + } + return txt; +}); + +showdown.subParser('makeMarkdown.hr', function () { + + return '---'; +}); + +showdown.subParser('makeMarkdown.image', function (node) { + + var txt = ''; + if (node.hasAttribute('src')) { + txt += '![' + node.getAttribute('alt') + ']('; + txt += '<' + node.getAttribute('src') + '>'; + if (node.hasAttribute('width') && node.hasAttribute('height')) { + txt += ' =' + node.getAttribute('width') + 'x' + node.getAttribute('height'); + } + + if (node.hasAttribute('title')) { + txt += ' "' + node.getAttribute('title') + '"'; + } + txt += ')'; + } + return txt; +}); + +showdown.subParser('makeMarkdown.links', function (node, globals) { + + var txt = ''; + if (node.hasChildNodes() && node.hasAttribute('href')) { + var children = node.childNodes, + childrenLength = children.length; + txt = '['; + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + txt += ']('; + txt += '<' + node.getAttribute('href') + '>'; + if (node.hasAttribute('title')) { + txt += ' "' + node.getAttribute('title') + '"'; + } + txt += ')'; + } + return txt; +}); + +showdown.subParser('makeMarkdown.list', function (node, globals, type) { + + var txt = ''; + if (!node.hasChildNodes()) { + return ''; + } + var listItems = node.childNodes, + listItemsLenght = listItems.length, + listNum = node.getAttribute('start') || 1; + + for (var i = 0; i < listItemsLenght; ++i) { + if (typeof listItems[i].tagName === 'undefined' || listItems[i].tagName.toLowerCase() !== 'li') { + continue; + } + + // define the bullet to use in list + var bullet = ''; + if (type === 'ol') { + bullet = listNum.toString() + '. '; + } else { + bullet = '- '; + } + + // parse list item + txt += bullet + showdown.subParser('makeMarkdown.listItem')(listItems[i], globals); + ++listNum; + } + + // add comment at the end to prevent consecutive lists to be parsed as one + txt += '\n\n'; + return txt.trim(); +}); + +showdown.subParser('makeMarkdown.listItem', function (node, globals) { + + var listItemTxt = ''; + + var children = node.childNodes, + childrenLenght = children.length; + + for (var i = 0; i < childrenLenght; ++i) { + listItemTxt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + // if it's only one liner, we need to add a newline at the end + if (!/\n$/.test(listItemTxt)) { + listItemTxt += '\n'; + } else { + // it's multiparagraph, so we need to indent + listItemTxt = listItemTxt + .split('\n') + .join('\n ') + .replace(/^ {4}$/gm, '') + .replace(/\n\n+/g, '\n\n'); + } + + return listItemTxt; +}); + + + +showdown.subParser('makeMarkdown.node', function (node, globals, spansOnly) { + + spansOnly = spansOnly || false; + + var txt = ''; + + // edge case of text without wrapper paragraph + if (node.nodeType === 3) { + return showdown.subParser('makeMarkdown.txt')(node, globals); + } + + // HTML comment + if (node.nodeType === 8) { + return '\n\n'; + } + + // process only node elements + if (node.nodeType !== 1) { + return ''; + } + + var tagName = node.tagName.toLowerCase(); + + switch (tagName) { + + // + // BLOCKS + // + case 'h1': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 1) + '\n\n'; } + break; + case 'h2': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 2) + '\n\n'; } + break; + case 'h3': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 3) + '\n\n'; } + break; + case 'h4': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 4) + '\n\n'; } + break; + case 'h5': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 5) + '\n\n'; } + break; + case 'h6': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 6) + '\n\n'; } + break; + + case 'p': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.paragraph')(node, globals) + '\n\n'; } + break; + + case 'blockquote': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.blockquote')(node, globals) + '\n\n'; } + break; + + case 'hr': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.hr')(node, globals) + '\n\n'; } + break; + + case 'ol': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.list')(node, globals, 'ol') + '\n\n'; } + break; + + case 'ul': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.list')(node, globals, 'ul') + '\n\n'; } + break; + + case 'precode': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.codeBlock')(node, globals) + '\n\n'; } + break; + + case 'pre': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.pre')(node, globals) + '\n\n'; } + break; + + case 'table': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.table')(node, globals) + '\n\n'; } + break; + + // + // SPANS + // + case 'code': + txt = showdown.subParser('makeMarkdown.codeSpan')(node, globals); + break; + + case 'em': + case 'i': + txt = showdown.subParser('makeMarkdown.emphasis')(node, globals); + break; + + case 'strong': + case 'b': + txt = showdown.subParser('makeMarkdown.strong')(node, globals); + break; + + case 'del': + txt = showdown.subParser('makeMarkdown.strikethrough')(node, globals); + break; + + case 'a': + txt = showdown.subParser('makeMarkdown.links')(node, globals); + break; + + case 'img': + txt = showdown.subParser('makeMarkdown.image')(node, globals); + break; + + default: + txt = node.outerHTML + '\n\n'; + } + + // common normalization + // TODO eventually + + return txt; +}); + +showdown.subParser('makeMarkdown.paragraph', function (node, globals) { + + var txt = ''; + if (node.hasChildNodes()) { + var children = node.childNodes, + childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + } + + // some text normalization + txt = txt.trim(); + + return txt; +}); + +showdown.subParser('makeMarkdown.pre', function (node, globals) { + + var num = node.getAttribute('prenum'); + return '
    ' + globals.preList[num] + '
    '; +}); + +showdown.subParser('makeMarkdown.strikethrough', function (node, globals) { + + var txt = ''; + if (node.hasChildNodes()) { + txt += '~~'; + var children = node.childNodes, + childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + txt += '~~'; + } + return txt; +}); + +showdown.subParser('makeMarkdown.strong', function (node, globals) { + + var txt = ''; + if (node.hasChildNodes()) { + txt += '**'; + var children = node.childNodes, + childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + txt += '**'; + } + return txt; +}); + +showdown.subParser('makeMarkdown.table', function (node, globals) { + + var txt = '', + tableArray = [[], []], + headings = node.querySelectorAll('thead>tr>th'), + rows = node.querySelectorAll('tbody>tr'), + i, ii; + for (i = 0; i < headings.length; ++i) { + var headContent = showdown.subParser('makeMarkdown.tableCell')(headings[i], globals), + allign = '---'; + + if (headings[i].hasAttribute('style')) { + var style = headings[i].getAttribute('style').toLowerCase().replace(/\s/g, ''); + switch (style) { + case 'text-align:left;': + allign = ':---'; + break; + case 'text-align:right;': + allign = '---:'; + break; + case 'text-align:center;': + allign = ':---:'; + break; + } + } + tableArray[0][i] = headContent.trim(); + tableArray[1][i] = allign; + } + + for (i = 0; i < rows.length; ++i) { + var r = tableArray.push([]) - 1, + cols = rows[i].getElementsByTagName('td'); + + for (ii = 0; ii < headings.length; ++ii) { + var cellContent = ' '; + if (typeof cols[ii] !== 'undefined') { + cellContent = showdown.subParser('makeMarkdown.tableCell')(cols[ii], globals); + } + tableArray[r].push(cellContent); + } + } + + var cellSpacesCount = 3; + for (i = 0; i < tableArray.length; ++i) { + for (ii = 0; ii < tableArray[i].length; ++ii) { + var strLen = tableArray[i][ii].length; + if (strLen > cellSpacesCount) { + cellSpacesCount = strLen; + } + } + } + + for (i = 0; i < tableArray.length; ++i) { + for (ii = 0; ii < tableArray[i].length; ++ii) { + if (i === 1) { + if (tableArray[i][ii].slice(-1) === ':') { + tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii].slice(-1), cellSpacesCount - 1, '-') + ':'; + } else { + tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount, '-'); + } + } else { + tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount); + } + } + txt += '| ' + tableArray[i].join(' | ') + ' |\n'; + } + + return txt.trim(); +}); + +showdown.subParser('makeMarkdown.tableCell', function (node, globals) { + + var txt = ''; + if (!node.hasChildNodes()) { + return ''; + } + var children = node.childNodes, + childrenLength = children.length; + + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals, true); + } + return txt.trim(); +}); + +showdown.subParser('makeMarkdown.txt', function (node) { + + var txt = node.nodeValue; + + // multiple spaces are collapsed + txt = txt.replace(/ +/g, ' '); + + // replace the custom ¨NBSP; with a space + txt = txt.replace(/¨NBSP;/g, ' '); + + // ", <, > and & should replace escaped html entities + txt = showdown.helper.unescapeHTMLEntities(txt); + + // escape markdown magic characters + // emphasis, strong and strikethrough - can appear everywhere + // we also escape pipe (|) because of tables + // and escape ` because of code blocks and spans + txt = txt.replace(/([*_~|`])/g, '\\$1'); + + // escape > because of blockquotes + txt = txt.replace(/^(\s*)>/g, '\\$1>'); + + // hash character, only troublesome at the beginning of a line because of headers + txt = txt.replace(/^#/gm, '\\#'); + + // horizontal rules + txt = txt.replace(/^(\s*)([-=]{3,})(\s*)$/, '$1\\$2$3'); + + // dot, because of ordered lists, only troublesome at the beginning of a line when preceded by an integer + txt = txt.replace(/^( {0,3}\d+)\./gm, '$1\\.'); + + // +, * and -, at the beginning of a line becomes a list, so we need to escape them also (asterisk was already escaped) + txt = txt.replace(/^( {0,3})([+-])/gm, '$1\\$2'); + + // images and links, ] followed by ( is problematic, so we escape it + txt = txt.replace(/]([\s]*)\(/g, '\\]$1\\('); + + // reference URIs must also be escaped + txt = txt.replace(/^ {0,3}\[([\S \t]*?)]:/gm, '\\[$1]:'); + + return txt; +}); + +var root = this; + +// AMD Loader +if ( module.exports) { + module.exports = showdown; + +// Regular Browser loader +} else { + root.showdown = showdown; +} +}).call(commonjsGlobal); + + +}); + +export default showdown; From 31c5f8241ce312d0dd4337fc9d1b975e6e23e5b2 Mon Sep 17 00:00:00 2001 From: John Hobbs Date: Sun, 4 Oct 2020 20:59:43 -0500 Subject: [PATCH 22/30] Inital OpenAPIv3 Spec (#223) --- openapi.yaml | 513 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 513 insertions(+) create mode 100644 openapi.yaml diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 000000000..833c8a013 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,513 @@ +openapi: 3.0.1 +info: + title: Owncast + description: Owncast is a self-hosted live video and web chat server for use with existing popular broadcasting software. + version: '0.0.2' +servers: [] + +tags: + - name: Admin + description: Admin operations requiring authentication. + - name: Chat + description: Endpoints related to the chat interface. + +components: + schemas: + BasicResponse: + type: object + properties: + success: + type: boolean + message: + type: string + InstanceDetails: + type: object + properties: + name: + type: string + title: + type: string + summary: + type: string + description: This is brief summary of whom you are or what the stream is. + logo: + type: object + properties: + large: + type: string + small: + type: string + tags: + type: array + items: + type: string + socialHandles: + type: array + items: + type: object + properties: + platform: + type: string + example: github + url: + type: string + example: http://github.com/owncast/owncast + extraUserInfoFileName: + type: string + description: Path to additional content about the server. + version: + type: string + example: Owncast v0.0.2-macOS (ef3796a033b32a312ebf5b334851cbf9959e7ecb) + S3: + type: object + properties: + enabled: + type: boolean + endpoint: + type: string + servingEndpoint: + type: string + accessKey: + type: string + secret: + type: string + bucket: + type: string + region: + type: string + acl: + type: string + required: + - enabled + StreamQuality: + type: object + properties: + videoPassthrough: + type: boolean + audioPassthrough: + type: boolean + videoBitrate: + type: integer + audioBitrate: + type: integer + scaledWidth: + type: integer + scaledHeight: + type: integer + framerate: + type: integer + encoderPreset: + type: string + TimestampedValue: + type: object + properties: + time: + type: string + format: date-time + value: + type: integer + + + securitySchemes: + AdminBasicAuth: + type: http + scheme: basic + description: The username for admin basic auth is `admin` and the password is the stream key. + + responses: + BasicResponse: + description: Operation Success/Failure Response + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResponse" + examples: + success: + summary: Operation succeeded. + value: {"success": true, "message": "inbound stream disconnected"} + failure: + summary: Operation failed. + value: {"success": false, "message": "no inbound stream connected"} + +paths: + + /api/config: + get: + summary: Information + description: Get the public information about the server. Adds context to the server, as well as information useful for the user interface. + tags: ["Server"] + responses: + '200': + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceDetails" + + /api/status: + get: + summary: Current Status + description: This endpoint is used to discover when a server is broadcasting, the number of active viewers as well as other useful information for updating the user interface. + tags: ["Server"] + responses: + '200': + description: "" + content: + application/json: + schema: + type: object + properties: + lastConnectTime: + type: string + nullable: true + format: date-time + overallMaxViewerCount: + type: integer + sessionMaxViewerCount: + type: integer + online: + type: boolean + viewerCount: + type: integer + lastDisconnectTime: + type: string + nullable: true + format: date-time + examples: + online: + value: + lastConnectTime: "2020-10-03T21:36:22-05:00" + lastDisconnectTime: null + online: true + overallMaxViewerCount: 420 + sessionMaxViewerCount: 12 + viewerCount: 7 + + /api/chat: + get: + summary: Historical Chat Messages + description: Used to get all chat messages prior to connecting to the websocket. + tags: ["Chat"] + responses: + '200': + description: "" + content: + application/json: + schema: + type: array + items: + type: object + properties: + author: + type: string + description: Username of the chat message poster. + body: + type: string + description: Escaped HTML of the chat message content. + image: + type: string + description: URL of the chat user avatar. + id: + type: string + description: Unique ID of the chat message. + visible: + type: boolean + description: "TODO" + timestamp: + type: string + format: date-time + + /api/yp: + get: + summary: Yellow Pages Information + description: Information to be used in the Yellow Pages service, a global directory of Owncast servers. + tags: ["Server"] + responses: + '200': + description: "" + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: + type: string + logo: + type: string + nsfw: + type: boolean + tags: + type: array + items: + type: string + online: + type: boolean + viewerCount: + type: integer + overallMaxViewerCount: + type: integer + sessionMaxViewerCount: + type: integer + lastConnectTime: + type: string + nullable: true + format: date-time + + /api/emoji: + get: + summary: Get Custom Emoji + description: Get a list of custom emoji that are supported in chat. + tags: ["Chat"] + responses: + '200': + description: "" + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + description: The name of the Emoji + emoji: + type: string + description: The relative path to the Emoji image file + examples: + default: + value: + items: + - name: nicolas_cage_party + emoji: /img/emoji/nicolas_cage_party.gif + - name: parrot + emoji: /img/emoji/parrot.gif + + /api/admin/broadcaster: + get: + summary: "Broadcaster Details" + tags: ["Admin"] + security: + - AdminBasicAuth: [] + responses: + '200': + description: Connected Broadcaster Details + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string + broadcaster: + type: object + properties: + remoteAddr: + type: string + time: + type: string + format: date-time + streamDetails: + type: object + properties: + width: + type: integer + height: + type: integer + frameRate: + type: integer + videoBitrate: + type: integer + videoCodec: + type: string + audioBitrate: + type: integer + audioCodec: + type: string + encoder: + type: string + examples: + connected: + summary: "Broadcaster Connected" + value: + success: true + message: "" + broadcaster: + remoteAddr: 127.0.0.1 + time: "TODO" + streamDetails: + width: 640 + height: 480 + frameRate: 24 + videoBitrate: 1500 + videoCodec: "todo" + audioBitrate: 256 + audioCodec: "aac" + encoder: "todo" + not-connected: + summary: "Broadcaster Not Connected" + value: + success: false + message: "no broadcaster connected" + + /api/admin/disconnect: + post: + summary: Disconnect Broadcaster + description: Disconnect the active inbound stream, if one exists, and terminate the broadcast. + tags: ["Admin"] + security: + - AdminBasicAuth: [] + responses: + '200': + $ref: "#/components/responses/BasicResponse" + + + /api/admin/changekey: + post: + summary: Update Stream Key + description: Change the stream key in memory, but not in the config file. This will require all broadcasters to be reconfigured to connect again. + tags: ["Admin"] + security: + - AdminBasicAuth: [] + requestBody: + description: "" + required: true + content: + application/json: + schema: + type: object + properties: + key: + type: string + responses: + '200': + description: Stream was disconnected. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: changed + + /api/admin/serverconfig: + get: + summary: Server Configuration + description: Get the current configuration of the Owncast server. + tags: ["Admin"] + security: + - AdminBasicAuth: [] + responses: + '200': + description: "" + content: + application/json: + schema: + type: object + properties: + instanceDetails: + $ref: "#/components/schemas/InstanceDetails" + ffmpegPath: + type: string + webServerPort: + type: integer + s3: + $ref: "#/components/schemas/S3" + videoSettings: + type: object + properties: + videoQualityVariants: + type: array + items: + $ref: "#/components/schemas/StreamQuality" + segmentLengthSeconds: + type: integer + numberOfPlaylistItems: + type: integer + + /api/admin/viewersOverTime: + get: + summary: Viewers Over Time + description: Get the tracked viewer count over the collected period. + tags: ["Admin"] + security: + - AdminBasicAuth: [] + responses: + '200': + description: "" + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/TimestampedValue" + examples: + default: + value: + - time: "2020-10-03T21:41:00.381996-05:00" + value: 50 + - time: "2020-10-03T21:42:00.381996-05:00" + value: 52 + + + + + /api/admin/hardwarestats: + get: + summary: Hardware Stats + description: "Get the CPU, Memory and Disk utilization levels over the collected period." + tags: ["Admin"] + security: + - AdminBasicAuth: [] + responses: + '200': + description: "" + content: + application/json: + schema: + type: object + properties: + cpu: + type: array + items: + $ref: "#/components/schemas/TimestampedValue" + memory: + type: array + items: + $ref: "#/components/schemas/TimestampedValue" + disk: + type: array + items: + $ref: "#/components/schemas/TimestampedValue" + examples: + default: + value: + cpu: + - time: "2020-10-03T21:41:00.381996-05:00" + value: 23 + - time: "2020-10-03T21:42:00.381996-05:00" + value: 27 + - time: "2020-10-03T21:43:00.381996-05:00" + value: 22 + memory: + - time: "2020-10-03T21:41:00.381996-05:00" + value: 65 + - time: "2020-10-03T21:42:00.381996-05:00" + value: 66 + - time: "2020-10-03T21:43:00.381996-05:00" + value: 72 + disk: + - time: "2020-10-03T21:41:00.381996-05:00" + value: 11 + - time: "2020-10-03T21:42:00.381996-05:00" + value: 11 + - time: "2020-10-03T21:43:00.381996-05:00" + value: 11 From 5d530ca5a67fdb01c2baed3a8a4428859efc5946 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Mon, 5 Oct 2020 00:05:35 -0700 Subject: [PATCH 23/30] Missing showdown module import in app --- webroot/js/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webroot/js/app.js b/webroot/js/app.js index f81f1165f..be2d3d451 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -1,6 +1,7 @@ import { h, Component } from '/js/web_modules/preact.js'; import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); +import showdown from '/js/web_modules/showdown.js'; import { OwncastPlayer } from './components/player.js'; import SocialIcon from './components/social.js'; From dfc03bc027902a07667b73fe6ca8603496f80e2f Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Mon, 5 Oct 2020 08:43:38 -0700 Subject: [PATCH 24/30] Web modules were getting hit by .gitignore. Fixed --- .gitignore | 2 + .../dist/videojs-http-streaming.min.js | 58119 ++++++++++++++++ .../tailwindcss/dist/tailwind.min.css | 1 + .../web_modules/videojs/dist/video-js.min.css | 1 + .../js/web_modules/videojs/dist/video.min.js | 32 + 5 files changed, 58155 insertions(+) create mode 100644 webroot/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js create mode 100644 webroot/js/web_modules/tailwindcss/dist/tailwind.min.css create mode 100644 webroot/js/web_modules/videojs/dist/video-js.min.css create mode 100644 webroot/js/web_modules/videojs/dist/video.min.js diff --git a/.gitignore b/.gitignore index f717b674f..05403027d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ dist/ transcoder.log chat.db .yp.key + +!webroot/js/web_modules/**/dist 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 new file mode 100644 index 000000000..b6a2b4ef8 --- /dev/null +++ b/webroot/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js @@ -0,0 +1,58119 @@ +import { c as createCommonjsModule, a as commonjsGlobal, g as getDefaultExportFromCjs } from '../../../common/_commonjsHelpers-37fa8da4.js'; +import { d as document_1, w as window_1$1 } from '../../../common/window-2f8a9a85.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; + +//[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] +//[5] Name ::= NameStartChar (NameChar)* +var nameStartChar = /[A-Z_a-z\xC0-\xD6\xD8-\xF6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/;//\u10000-\uEFFFF +var nameChar = new RegExp("[\\-\\.0-9"+nameStartChar.source.slice(1,-1)+"\\u00B7\\u0300-\\u036F\\u203F-\\u2040]"); +var tagNamePattern = new RegExp('^'+nameStartChar.source+nameChar.source+'*(?:\:'+nameStartChar.source+nameChar.source+'*)?$'); +//var tagNamePattern = /^[a-zA-Z_][\w\-\.]*(?:\:[a-zA-Z_][\w\-\.]*)?$/ +//var handlers = 'resolveEntity,getExternalSubset,characters,endDocument,endElement,endPrefixMapping,ignorableWhitespace,processingInstruction,setDocumentLocator,skippedEntity,startDocument,startElement,startPrefixMapping,notationDecl,unparsedEntityDecl,error,fatalError,warning,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,comment,endCDATA,endDTD,endEntity,startCDATA,startDTD,startEntity'.split(',') + +//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_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) +var S_ATTR_END = 5;//attr value end and no space(quot end) +var S_TAG_SPACE = 6;//(attr value end || tag end ) && (space offer) +var S_TAG_CLOSE = 7;//closed el + +function XMLReader(){ + +} + +XMLReader.prototype = { + parse:function(source,defaultNSMap,entityMap){ + var domBuilder = this.domBuilder; + domBuilder.startDocument(); + _copy(defaultNSMap ,defaultNSMap = {}); + parse(source,defaultNSMap,entityMap, + domBuilder,this.errorHandler); + domBuilder.endDocument(); + } +}; +function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ + function fixedFromCharCode(code) { + // String.prototype.fromCharCode does not supports + // > 2 bytes unicode chars directly + if (code > 0xffff) { + code -= 0x10000; + var surrogate1 = 0xd800 + (code >> 10) + , surrogate2 = 0xdc00 + (code & 0x3ff); + + return String.fromCharCode(surrogate1, surrogate2); + } else { + return String.fromCharCode(code); + } + } + function entityReplacer(a){ + var k = a.slice(1,-1); + if(k in entityMap){ + return entityMap[k]; + }else if(k.charAt(0) === '#'){ + return fixedFromCharCode(parseInt(k.substr(1).replace('x','0x'))) + }else { + errorHandler.error('entity not found:'+a); + return a; + } + } + function appendText(end){//has some bugs + if(end>start){ + var xt = source.substring(start,end).replace(/&#?\w+;/g,entityReplacer); + locator&&position(start); + domBuilder.characters(xt,0,end-start); + start = end; + } + } + function position(p,m){ + while(p>=lineEnd && (m = linePattern.exec(source))){ + lineStart = m.index; + lineEnd = lineStart + m[0].length; + locator.lineNumber++; + //console.log('line++:',locator,startPos,endPos) + } + locator.columnNumber = p-lineStart+1; + } + var lineStart = 0; + var lineEnd = 0; + var linePattern = /.*(?:\r\n?|\n)|.*$/g; + var locator = domBuilder.locator; + + var parseStack = [{currentNSMap:defaultNSMapCopy}]; + var closeMap = {}; + var start = 0; + while(true){ + try{ + var tagStart = source.indexOf('<',start); + 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; + } + return; + } + if(tagStart>start){ + appendText(tagStart); + } + switch(source.charAt(tagStart+1)){ + case '/': + var end = source.indexOf('>',tagStart+3); + 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 + locator&&position(tagStart); + end = parseInstruction(source,tagStart,domBuilder); + break; + case '!':// start){ + start = end; + }else { + //TODO: 这里有可能sax回退,有位置错误风险 + appendText(Math.max(tagStart,start)+1); + } + } +} +function copyLocator(f,t){ + t.lineNumber = f.lineNumber; + t.columnNumber = f.columnNumber; + return t; +} + +/** + * @see #appendElement(source,elStartEnd,el,selfClosed,entityReplacer,domBuilder,parseStack); + * @return end of the elementStartPart(end of elementEndPart for selfClosed el) + */ +function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,errorHandler){ + var attrName; + var value; + var p = ++start; + var s = S_TAG;//status + while(true){ + var c = source.charAt(p); + switch(c){ + case '=': + if(s === S_ATTR){//attrName + attrName = source.slice(start,p); + s = S_EQ; + }else if(s === S_ATTR_SPACE){ + s = S_EQ; + }else { + //fatalError: equal must after attrName or space after attrName + throw new Error('attribute equal must after attrName'); + } + break; + case '\'': + case '"': + if(s === S_EQ || s === S_ATTR //|| s == S_ATTR_SPACE + ){//equal + if(s === S_ATTR){ + errorHandler.warning('attribute value must after "="'); + attrName = source.slice(start,p); + } + start = p+1; + p = source.indexOf(c,start); + if(p>0){ + value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer); + el.add(attrName,value,start-1); + s = S_ATTR_END; + }else { + //fatalError: no end quot match + throw new Error('attribute value no end \''+c+'\' match'); + } + }else if(s == S_ATTR_NOQUOT_VALUE){ + value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer); + //console.log(attrName,value,start,p) + el.add(attrName,value,start); + //console.dir(el) + errorHandler.warning('attribute "'+attrName+'" missed start quot('+c+')!!'); + start = p+1; + s = S_ATTR_END; + }else { + //fatalError: no equal before + throw new Error('attribute value must after "="'); + } + break; + case '/': + switch(s){ + case S_TAG: + el.setTagName(source.slice(start,p)); + case S_ATTR_END: + case S_TAG_SPACE: + case S_TAG_CLOSE: + s =S_TAG_CLOSE; + el.closed = true; + case S_ATTR_NOQUOT_VALUE: + case S_ATTR: + case S_ATTR_SPACE: + break; + //case S_EQ: + default: + throw new Error("attribute invalid close char('/')") + } + break; + case ''://end document + //throw new Error('unexpected end of input') + errorHandler.error('unexpected end of input'); + if(s == S_TAG){ + el.setTagName(source.slice(start,p)); + } + return p; + case '>': + switch(s){ + case S_TAG: + el.setTagName(source.slice(start,p)); + case S_ATTR_END: + case S_TAG_SPACE: + case S_TAG_CLOSE: + break;//normal + case S_ATTR_NOQUOT_VALUE://Compatible state + case S_ATTR: + value = source.slice(start,p); + if(value.slice(-1) === '/'){ + el.closed = true; + value = value.slice(0,-1); + } + case S_ATTR_SPACE: + if(s === S_ATTR_SPACE){ + value = attrName; + } + if(s == S_ATTR_NOQUOT_VALUE){ + errorHandler.warning('attribute "'+value+'" missed quot(")!!'); + el.add(attrName,value.replace(/&#?\w+;/g,entityReplacer),start); + }else { + if(currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !value.match(/^(?:disabled|checked|selected)$/i)){ + errorHandler.warning('attribute "'+value+'" missed value!! "'+value+'" instead!!'); + } + el.add(value,value,start); + } + break; + case S_EQ: + throw new Error('attribute value missed!!'); + } +// console.log(tagName,tagNamePattern,tagNamePattern.test(tagName)) + return p; + /*xml space '\x20' | #x9 | #xD | #xA; */ + case '\u0080': + c = ' '; + default: + if(c<= ' '){//space + switch(s){ + case S_TAG: + el.setTagName(source.slice(start,p));//tagName + s = S_TAG_SPACE; + break; + case S_ATTR: + attrName = source.slice(start,p); + s = S_ATTR_SPACE; + break; + case S_ATTR_NOQUOT_VALUE: + var value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer); + errorHandler.warning('attribute "'+value+'" missed quot(")!!'); + el.add(attrName,value,start); + case S_ATTR_END: + s = S_TAG_SPACE; + break; + //case S_TAG_SPACE: + //case S_EQ: + //case S_ATTR_SPACE: + // void();break; + //case S_TAG_CLOSE: + //ignore warning + } + }else {//not space +//S_TAG, S_ATTR, S_EQ, S_ATTR_NOQUOT_VALUE +//S_ATTR_SPACE, S_ATTR_END, S_TAG_SPACE, S_TAG_CLOSE + switch(s){ + //case S_TAG:void();break; + //case S_ATTR:void();break; + //case S_ATTR_NOQUOT_VALUE:void();break; + case S_ATTR_SPACE: + var tagName = el.tagName; + if(currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !attrName.match(/^(?:disabled|checked|selected)$/i)){ + errorHandler.warning('attribute "'+attrName+'" missed value!! "'+attrName+'" instead2!!'); + } + el.add(attrName,attrName,start); + start = p; + s = S_ATTR; + break; + case S_ATTR_END: + errorHandler.warning('attribute space is required"'+attrName+'"!!'); + case S_TAG_SPACE: + s = S_ATTR; + start = p; + break; + case S_EQ: + s = S_ATTR_NOQUOT_VALUE; + start = p; + break; + case S_TAG_CLOSE: + throw new Error("elements closed character '/' and '>' must be connected to"); + } + } + }//end outer switch + //console.log('p++',p) + p++; + } +} +/** + * @return true if has new namespace define + */ +function appendElement(el,domBuilder,currentNSMap){ + var tagName = el.tagName; + var localNSMap = null; + //var currentNSMap = parseStack[parseStack.length-1].currentNSMap; + var i = el.length; + while(i--){ + var a = el[i]; + var qName = a.qName; + var value = a.value; + var nsp = qName.indexOf(':'); + if(nsp>0){ + var prefix = a.prefix = qName.slice(0,nsp); + var localName = qName.slice(nsp+1); + var nsPrefix = prefix === 'xmlns' && localName; + }else { + localName = qName; + prefix = null; + nsPrefix = qName === 'xmlns' && ''; + } + //can not set prefix,because prefix !== '' + a.localName = localName ; + //prefix == null for no ns prefix attribute + if(nsPrefix !== false){//hack!! + if(localNSMap == null){ + localNSMap = {}; + //console.log(currentNSMap,0) + _copy(currentNSMap,currentNSMap={}); + //console.log(currentNSMap,1) + } + currentNSMap[nsPrefix] = localNSMap[nsPrefix] = value; + a.uri = 'http://www.w3.org/2000/xmlns/'; + domBuilder.startPrefixMapping(nsPrefix, value); + } + } + var i = el.length; + while(i--){ + a = el[i]; + var prefix = a.prefix; + if(prefix){//no prefix attribute has no namespace + if(prefix === 'xml'){ + a.uri = 'http://www.w3.org/XML/1998/namespace'; + }if(prefix !== 'xmlns'){ + a.uri = currentNSMap[prefix || '']; + + //{console.log('###'+a.qName,domBuilder.locator.systemId+'',currentNSMap,a.uri)} + } + } + } + var nsp = tagName.indexOf(':'); + if(nsp>0){ + prefix = el.prefix = tagName.slice(0,nsp); + localName = el.localName = tagName.slice(nsp+1); + }else { + prefix = null;//important!! + localName = el.localName = tagName; + } + //no prefix element has default namespace + var ns = el.uri = currentNSMap[prefix || '']; + domBuilder.startElement(ns,localName,tagName,el); + //endPrefixMapping and startPrefixMapping have not any help for dom builder + //localNSMap = null + if(el.closed){ + domBuilder.endElement(ns,localName,tagName); + if(localNSMap){ + for(prefix in localNSMap){ + domBuilder.endPrefixMapping(prefix); + } + } + }else { + el.currentNSMap = currentNSMap; + el.localNSMap = localNSMap; + //parseStack.push(el); + return true; + } +} +function parseHtmlSpecialContent(source,elStartEnd,tagName,entityReplacer,domBuilder){ + if(/^(?:script|textarea)$/i.test(tagName)){ + var elEndStart = source.indexOf('',elStartEnd); + var text = source.substring(elStartEnd+1,elEndStart); + if(/[&<]/.test(text)){ + if(/^script$/i.test(tagName)){ + //if(!/\]\]>/.test(text)){ + //lexHandler.startCDATA(); + domBuilder.characters(text,0,text.length); + //lexHandler.endCDATA(); + return elEndStart; + //} + }//}else{//text area + text = text.replace(/&#?\w+;/g,entityReplacer); + domBuilder.characters(text,0,text.length); + return elEndStart; + //} + + } + } + return elStartEnd+1; +} +function fixSelfClosed(source,elStartEnd,tagName,closeMap){ + //if(tagName in closeMap){ + var pos = closeMap[tagName]; + if(pos == null){ + //console.log(tagName) + pos = source.lastIndexOf(''); + if(pos',start+4); + //append comment source.substring(4,end)//"); + case DOCUMENT_TYPE_NODE: + var pubid = node.publicId; + var sysid = node.systemId; + buf.push(''); + }else if(sysid && sysid!='.'){ + buf.push(' SYSTEM "',sysid,'">'); + }else { + var sub = node.internalSubset; + if(sub){ + buf.push(" [",sub,"]"); + } + buf.push(">"); + } + return; + case PROCESSING_INSTRUCTION_NODE: + return buf.push( ""); + case ENTITY_REFERENCE_NODE: + return buf.push( '&',node.nodeName,';'); + //case ENTITY_NODE: + //case NOTATION_NODE: + default: + buf.push('??',node.nodeName); + } +} +function importNode(doc,node,deep){ + var node2; + switch (node.nodeType) { + case ELEMENT_NODE: + node2 = node.cloneNode(false); + node2.ownerDocument = doc; + //var attrs = node2.attributes; + //var len = attrs.length; + //for(var i=0;i','amp':'&','quot':'"','apos':"'"}; + if(locator){ + domBuilder.setDocumentLocator(locator); + } + + sax.errorHandler = buildErrorHandler(errorHandler,domBuilder,locator); + sax.domBuilder = options.domBuilder || domBuilder; + if(/\/x?html?$/.test(mimeType)){ + entityMap.nbsp = '\xa0'; + entityMap.copy = '\xa9'; + defaultNSMap['']= 'http://www.w3.org/1999/xhtml'; + } + defaultNSMap.xml = defaultNSMap.xml || 'http://www.w3.org/XML/1998/namespace'; + if(source){ + sax.parse(source,defaultNSMap,entityMap); + }else { + sax.errorHandler.error("invalid doc source"); + } + return domBuilder.doc; +}; +function buildErrorHandler(errorImpl,domBuilder,locator){ + if(!errorImpl){ + if(domBuilder instanceof DOMHandler){ + return domBuilder; + } + errorImpl = domBuilder ; + } + var errorHandler = {}; + var isCallback = errorImpl instanceof Function; + locator = locator||{}; + function build(key){ + var fn = errorImpl[key]; + if(!fn && isCallback){ + fn = errorImpl.length == 2?function(msg){errorImpl(key,msg);}:errorImpl; + } + errorHandler[key] = fn && function(msg){ + fn('[xmldom '+key+']\t'+msg+_locator(locator)); + }||function(){}; + } + build('warning'); + build('error'); + build('fatalError'); + return errorHandler; +} + +//console.log('#\n\n\n\n\n\n\n####') +/** + * +ContentHandler+ErrorHandler + * +LexicalHandler+EntityResolver2 + * -DeclHandler-DTDHandler + * + * DefaultHandler:EntityResolver, DTDHandler, ContentHandler, ErrorHandler + * DefaultHandler2:DefaultHandler,LexicalHandler, DeclHandler, EntityResolver2 + * @link http://www.saxproject.org/apidoc/org/xml/sax/helpers/DefaultHandler.html + */ +function DOMHandler() { + this.cdata = false; +} +function position(locator,node){ + node.lineNumber = locator.lineNumber; + node.columnNumber = locator.columnNumber; +} +/** + * @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; + } + }, + startElement:function(namespaceURI, localName, qName, attrs) { + var doc = this.doc; + var el = doc.createElementNS(namespaceURI, qName||localName); + 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); + var value = attrs.getValue(i); + var qName = attrs.getQName(i); + var attr = doc.createAttributeNS(namespaceURI, qName); + this.locator &&position(attrs.getLocator(i),attr); + attr.value = attr.nodeValue = value; + el.setAttributeNode(attr); + } + }, + endElement:function(namespaceURI, localName, qName) { + var current = this.currentElement; + var tagName = current.tagName; + this.currentElement = current.parentNode; + }, + startPrefixMapping:function(prefix, uri) { + }, + endPrefixMapping:function(prefix) { + }, + processingInstruction:function(target, data) { + var ins = this.doc.createProcessingInstruction(target, data); + this.locator && position(this.locator,ins); + appendElement(this, ins); + }, + ignorableWhitespace:function(ch, start, length) { + }, + characters:function(chars, start, length) { + chars = _toString.apply(this,arguments); + //console.log(chars) + if(chars){ + if (this.cdata) { + var charNode = this.doc.createCDATASection(chars); + } else { + var charNode = this.doc.createTextNode(chars); + } + if(this.currentElement){ + this.currentElement.appendChild(charNode); + }else if(/^\s*$/.test(chars)){ + this.doc.appendChild(charNode); + //process xml + } + this.locator && position(this.locator,charNode); + } + }, + skippedEntity:function(name) { + }, + endDocument:function() { + this.doc.normalize(); + }, + setDocumentLocator:function (locator) { + if(this.locator = locator){// && !('lineNumber' in locator)){ + locator.lineNumber = 0; + } + }, + //LexicalHandler + comment:function(chars, start, length) { + chars = _toString.apply(this,arguments); + var comm = this.doc.createComment(chars); + this.locator && position(this.locator,comm); + appendElement(this, comm); + }, + + startCDATA:function() { + //used in characters() methods + this.cdata = true; + }, + endCDATA:function() { + this.cdata = false; + }, + + startDTD:function(name, publicId, systemId) { + var impl = this.doc.implementation; + if (impl && impl.createDocumentType) { + var dt = impl.createDocumentType(name, publicId, systemId); + this.locator && position(this.locator,dt); + appendElement(this, dt); + } + }, + /** + * @see org.xml.sax.ErrorHandler + * @link http://www.saxproject.org/apidoc/org/xml/sax/ErrorHandler.html + */ + warning:function(error) { + console.warn('[xmldom warning]\t'+error,_locator(this.locator)); + }, + error:function(error) { + console.error('[xmldom error]\t'+error,_locator(this.locator)); + }, + fatalError:function(error) { + console.error('[xmldom fatalError]\t'+error,_locator(this.locator)); + throw error; + } +}; +function _locator(l){ + if(l){ + return '\n@'+(l.systemId ||'')+'#[line:'+l.lineNumber+',col:'+l.columnNumber+']' + } +} +function _toString(chars,start,length){ + if(typeof chars == 'string'){ + return chars.substr(start,length) + }else {//java sax connect width xmldom on rhino(what about: "? && !(chars instanceof String)") + if(chars.length >= start+length || start){ + return new java.lang.String(chars,start,length)+''; + } + return chars; + } +} + +/* + * @link http://www.saxproject.org/apidoc/org/xml/sax/ext/LexicalHandler.html + * used method of org.xml.sax.ext.LexicalHandler: + * #comment(chars, start, length) + * #startCDATA() + * #endCDATA() + * #startDTD(name, publicId, systemId) + * + * + * IGNORED method of org.xml.sax.ext.LexicalHandler: + * #endDTD() + * #startEntity(name) + * #endEntity(name) + * + * + * @link http://www.saxproject.org/apidoc/org/xml/sax/ext/DeclHandler.html + * IGNORED method of org.xml.sax.ext.DeclHandler + * #attributeDecl(eName, aName, type, mode, value) + * #elementDecl(name, model) + * #externalEntityDecl(name, publicId, systemId) + * #internalEntityDecl(name, value) + * @link http://www.saxproject.org/apidoc/org/xml/sax/ext/EntityResolver2.html + * IGNORED method of org.xml.sax.EntityResolver2 + * #resolveEntity(String name,String publicId,String baseURI,String systemId) + * #resolveEntity(publicId, systemId) + * #getExternalSubset(name, baseURI) + * @link http://www.saxproject.org/apidoc/org/xml/sax/DTDHandler.html + * IGNORED method of org.xml.sax.DTDHandler + * #notationDecl(name, publicId, systemId) {}; + * #unparsedEntityDecl(name, publicId, systemId, notationName) {}; + */ +"endDTD,startEntity,endEntity,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,resolveEntity,getExternalSubset,notationDecl,unparsedEntityDecl".replace(/\w+/g,function(key){ + DOMHandler.prototype[key] = function(){return null}; +}); + +/* Private static helpers treated below as private instance methods, so don't need to add these to the public API; we might use a Relator to also get rid of non-standard public properties */ +function appendElement (hander,node) { + if (!hander.currentElement) { + hander.doc.appendChild(node); + } else { + hander.currentElement.appendChild(node); + } +}//appendChild and setAttributeNS are preformance key + +//if(typeof require == 'function'){ + var XMLReader = sax.XMLReader; + var DOMImplementation = exports.DOMImplementation = dom.DOMImplementation; + exports.XMLSerializer = dom.XMLSerializer ; + exports.DOMParser = DOMParser; +//} +}); + +/*! @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'