diff --git a/src/brackets.js b/src/brackets.js index bdb2d060b9..8cc2e89296 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -197,6 +197,7 @@ define(function (require, exports, module) { require("editor/EditorOptionHandlers"); require("editor/EditorStatusBar"); require("editor/ImageViewer"); + require("editor/MediaViewer"); // Preload so extensions can synchronously brackets.getModule it (e.g. DocCommentHints snippets). require("editor/TabstopManager"); require("extensibility/ExtensionDownloader"); diff --git a/src/editor/MediaViewer.js b/src/editor/MediaViewer.js new file mode 100644 index 0000000000..75f92a7852 --- /dev/null +++ b/src/editor/MediaViewer.js @@ -0,0 +1,331 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +define(function (require, exports, module) { + + + const DocumentManager = require("document/DocumentManager"), + MediaViewTemplate = require("text!htmlContent/media-view.html"), + ProjectManager = require("project/ProjectManager"), + LanguageManager = require("language/LanguageManager"), + MainViewFactory = require("view/MainViewFactory"), + Strings = require("strings"), + StringUtils = require("utils/StringUtils"), + FileSystem = require("filesystem/FileSystem"), + FileSystemError = require("filesystem/FileSystemError"), + FileUtils = require("file/FileUtils"), + _ = require("thirdparty/lodash"), + Mustache = require("thirdparty/mustache/mustache"); + + const _viewers = {}; + + // Mime types for the media extensions we can play in the embedded browser engine + const _MIME_TYPES = { + "mp4": "video/mp4", + "m4v": "video/mp4", + "webm": "video/webm", + "mkv": "video/webm", + "ogv": "video/ogg", + "mov": "video/quicktime", + "mp3": "audio/mpeg", + "wav": "audio/wav", + "ogg": "audio/ogg", + "m4a": "audio/mp4", + "flac": "audio/flac", + "aac": "audio/aac", + "aif": "audio/aiff", + "aiff": "audio/aiff" + }; + + function _getMimeType(fullPath, isAudio) { + const extension = FileUtils.getFileExtension(fullPath).toLowerCase(); + return _MIME_TYPES[extension] || (isAudio ? "audio/mpeg" : "video/mp4"); + } + + // blob: URLs are rejected by the media loader on the custom app protocol in native builds + // ("Media load rejected by URL safety check"), so we use a data URI like ImageViewer does. + function _mediaToDataURI(file, isAudio, cb) { + file.read({encoding: window.fs.BYTE_ARRAY_ENCODING}, function (err, content) { + if (err) { + cb(err); + return; + } + const bytes = new Uint8Array(content), + chunkSize = 0x8000, + chunks = []; + for (let i = 0; i < bytes.length; i += chunkSize) { + chunks.push(String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize))); + } + const dataURI = "data:" + _getMimeType(file.fullPath, isAudio) + ";base64," + window.btoa(chunks.join("")); + cb(null, dataURI); + }); + } + + /** + * Format a duration in seconds as h:mm:ss / m:ss for display + * @param {number} seconds + * @return {string} + * @private + */ + function _formatDuration(seconds) { + if (!isFinite(seconds)) { + return ""; + } + const totalSeconds = Math.round(seconds), + hrs = Math.floor(totalSeconds / 3600), + mins = Math.floor((totalSeconds % 3600) / 60), + secs = totalSeconds % 60, + paddedSecs = (secs < 10 ? "0" : "") + secs; + if (hrs) { + const paddedMins = (mins < 10 ? "0" : "") + mins; + return hrs + ":" + paddedMins + ":" + paddedSecs; + } + return mins + ":" + paddedSecs; + } + + /** + * MediaView objects are constructed when a video or audio file is opened + * @see {@link Pane} for more information about where MediaViews are rendered + * + * @constructor + * @param {!File} file - The media file object to render + * @param {!jQuery} $container - The container to render the media view in + */ + function MediaView(file, $container) { + this.file = file; + this._isAudio = (LanguageManager.getLanguageForPath(file.fullPath).getId() === "audio"); + this.$el = $(Mustache.render(MediaViewTemplate, {isAudio: this._isAudio})); + $container.append(this.$el); + + this._naturalWidth = 0; + this._naturalHeight = 0; + + this.relPath = ProjectManager.makeProjectRelativeIfPossible(this.file.fullPath); + + this.$mediaPath = this.$el.find(".media-path"); + this.$mediaPreview = this.$el.find(".media-preview"); + this.$mediaData = this.$el.find(".media-data"); + this.$mediaError = this.$el.find(".media-error"); + + this.$mediaPath.text(this.relPath).attr("title", this.relPath); + this.$mediaPreview.on("loadedmetadata", _.bind(this._onMediaLoaded, this)); + this.$mediaPreview.on("error", _.bind(this._onMediaError, this)); + + _viewers[file.fullPath] = this; + + DocumentManager.on("fileNameChange.MediaView", _.bind(this._onFilenameChange, this)); + + this._loadMedia(); + } + + /** + * Reads the media file and points the media element at its content + * @private + */ + MediaView.prototype._loadMedia = function () { + const self = this; + _mediaToDataURI(this.file, this._isAudio, function (err, dataURI) { + if (err) { + self._showError(err === FileSystemError.EXCEEDS_MAX_FILE_SIZE + ? StringUtils.format(Strings.MEDIA_VIEWER_FILE_TOO_LARGE, "16 MB") + : Strings.MEDIA_VIEWER_FORMAT_NOT_SUPPORTED); + return; + } + self.$mediaError.hide(); + self.$mediaPreview.show(); + self.$mediaPreview[0].src = dataURI; + }); + }; + + /** + * Shows an error message instead of the media element + * @param {string} message + * @private + */ + MediaView.prototype._showError = function (message) { + this.$mediaPreview.hide(); + this.$mediaError.text(message).show(); + }; + + /** + * DocumentManger.fileNameChange handler - when a media file is renamed, we must + * update the view + * + * @param {jQuery.Event} e - event + * @param {!string} oldPath - the name of the file that's changing changing + * @param {!string} newPath - the name of the file that's changing changing + * @private + */ + MediaView.prototype._onFilenameChange = function (e, oldPath, newPath) { + if (this.file.fullPath === newPath) { + this.relPath = ProjectManager.makeProjectRelativeIfPossible(newPath); + this.$mediaPath.text(this.relPath).attr("title", this.relPath); + } + }; + + /** + *