diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'b9927580', + 'core.pkg.css' => '0e3b60db', 'core.pkg.js' => '3f15fa62', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'f3fb8324', @@ -104,7 +104,7 @@ 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => 'd0801452', - 'rsrc/css/core/remarkup.css' => '787105d6', + 'rsrc/css/core/remarkup.css' => '845a390d', 'rsrc/css/core/syntax.css' => '9fc496d5', 'rsrc/css/core/z-index.css' => '5b6fcf3f', 'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa', @@ -149,7 +149,7 @@ 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 'rsrc/css/phui/phui-profile-menu.css' => 'c8557f33', - 'rsrc/css/phui/phui-property-list-view.css' => '1d42ee7c', + 'rsrc/css/phui/phui-property-list-view.css' => '6458f614', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', 'rsrc/css/phui/phui-segment-bar-view.css' => '46342871', 'rsrc/css/phui/phui-spacing.css' => '042804d6', @@ -784,7 +784,7 @@ 'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => 'e67df814', - 'phabricator-remarkup-css' => '787105d6', + 'phabricator-remarkup-css' => '845a390d', 'phabricator-search-results-css' => '7dea472c', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-side-menu-view-css' => 'dd849797', @@ -851,7 +851,7 @@ 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', 'phui-profile-menu-css' => 'c8557f33', - 'phui-property-list-view-css' => '1d42ee7c', + 'phui-property-list-view-css' => '6458f614', 'phui-remarkup-preview-css' => '1a8f2591', 'phui-segment-bar-view-css' => '46342871', 'phui-spacing-css' => '042804d6', diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php --- a/src/applications/files/config/PhabricatorFilesConfigOptions.php +++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php @@ -34,9 +34,16 @@ 'image/x-icon' => 'image/x-icon', 'image/vnd.microsoft.icon' => 'image/x-icon', - 'audio/x-wav' => 'audio/x-wav', + // This is a generic type for both OGG video and OGG audio. 'application/ogg' => 'application/ogg', - 'audio/mpeg' => 'audio/mpeg', + + 'audio/x-wav' => 'audio/x-wav', + 'audio/mpeg' => 'audio/mpeg', + 'audio/ogg' => 'audio/ogg', + + 'video/mp4' => 'video/mp4', + 'video/ogg' => 'video/ogg', + 'video/webm' => 'video/webm', ); $image_default = array( @@ -49,10 +56,29 @@ 'image/vnd.microsoft.icon' => true, ); + + // The "application/ogg" type is listed as both an audio and video type, + // because it may contain either type of content. + $audio_default = array( - 'audio/x-wav' => true, + 'audio/x-wav' => true, + 'audio/mpeg' => true, + 'audio/ogg' => true, + + // These are video or ambiguous types, but can be forced to render as + // audio with `media=audio`, which seems to work properly in browsers. + // (For example, you can embed a music video as audio if you just want + // to set the mood for your task without distracting viewers.) + 'video/mp4' => true, + 'video/ogg' => true, + 'application/ogg' => true, + ); + + $video_default = array( + 'video/mp4' => true, + 'video/ogg' => true, + 'video/webm' => true, 'application/ogg' => true, - 'audio/mpeg' => true, ); // largely lifted from http://en.wikipedia.org/wiki/Internet_media_type @@ -70,6 +96,7 @@ // movie file icon 'video/mpeg' => 'fa-file-movie-o', 'video/mp4' => 'fa-file-movie-o', + 'application/ogg' => 'fa-file-movie-o', 'video/ogg' => 'fa-file-movie-o', 'video/quicktime' => 'fa-file-movie-o', 'video/webm' => 'fa-file-movie-o', @@ -122,8 +149,14 @@ ->setSummary(pht('Configure which MIME types are audio.')) ->setDescription( pht( - 'List of MIME types which can be used to render an `%s` tag.', + 'List of MIME types which can be rendered with an `%s` tag.', '<audio />')), + $this->newOption('files.video-mime-types', 'set', $video_default) + ->setSummary(pht('Configure which MIME types are video.')) + ->setDescription( + pht( + 'List of MIME types which can be rendered with a `%s` tag.', + '<video />')), $this->newOption('files.icon-mime-types', 'wild', $icon_default) ->setLocked(true) ->setSummary(pht('Configure which MIME types map to which icons.')) diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -230,23 +230,34 @@ $cache_string = pht('Not Applicable'); } - $finfo->addProperty(pht('Viewable Image'), $image_string); - $finfo->addProperty(pht('Cacheable'), $cache_string); + $types = array(); + if ($file->isViewableImage()) { + $types[] = pht('Image'); + } - $builtin = $file->getBuiltinName(); - if ($builtin === null) { - $builtin_string = pht('No'); - } else { - $builtin_string = $builtin; + if ($file->isVideo()) { + $types[] = pht('Video'); + } + + if ($file->isAudio()) { + $types[] = pht('Audio'); } - $finfo->addProperty(pht('Builtin'), $builtin_string); + if ($file->getCanCDN()) { + $types[] = pht('Can CDN'); + } + + $builtin = $file->getBuiltinName(); + if ($builtin !== null) { + $types[] = pht('Builtin ("%s")', $builtin); + } - $is_profile = $file->getIsProfileImage() - ? pht('Yes') - : pht('No'); + if ($file->getIsProfileImage()) { + $types[] = pht('Profile'); + } - $finfo->addProperty(pht('Profile'), $is_profile); + $types = implode(', ', $types); + $finfo->addProperty(pht('Attributes'), $types); $storage_properties = new PHUIPropertyListView(); $box->addPropertyList($storage_properties, pht('Storage')); @@ -293,6 +304,23 @@ ->addImageContent($linked_image); $box->addPropertyList($media); + } else if ($file->isVideo()) { + $video = phutil_tag( + 'video', + array( + 'controls' => 'controls', + 'class' => 'phui-property-list-video', + ), + phutil_tag( + 'source', + array( + 'src' => $file->getViewURI(), + 'type' => $file->getMimeType(), + ))); + $media = id(new PHUIPropertyListView()) + ->addImageContent($video); + + $box->addPropertyList($media); } else if ($file->isAudio()) { $audio = phutil_tag( 'audio', diff --git a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php --- a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php +++ b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php @@ -43,12 +43,27 @@ $is_viewable_image = $object->isViewableImage(); $is_audio = $object->isAudio(); + $is_video = $object->isVideo(); $force_link = ($options['layout'] == 'link'); - $options['viewable'] = ($is_viewable_image || $is_audio); + // If a file is both audio and video, as with "application/ogg" by default, + // render it as video but allow the user to specify `media=audio` if they + // want to force it to render as audio. + if ($is_audio && $is_video) { + $media = $options['media']; + if ($media == 'audio') { + $is_video = false; + } else { + $is_audio = false; + } + } + + $options['viewable'] = ($is_viewable_image || $is_audio || $is_video); if ($is_viewable_image && !$force_link) { return $this->renderImageFile($object, $handle, $options); + } else if ($is_video && !$force_link) { + return $this->renderVideoFile($object, $handle, $options); } else if ($is_audio && !$force_link) { return $this->renderAudioFile($object, $handle, $options); } else { @@ -58,12 +73,15 @@ private function getFileOptions($option_string) { $options = array( - 'size' => null, - 'layout' => 'left', - 'float' => false, - 'width' => null, - 'height' => null, + 'size' => null, + 'layout' => 'left', + 'float' => false, + 'width' => null, + 'height' => null, 'alt' => null, + 'media' => null, + 'autoplay' => null, + 'loop' => null, ); if ($option_string) { @@ -201,22 +219,47 @@ PhabricatorFile $file, PhabricatorObjectHandle $handle, array $options) { + return $this->renderMediaFile('audio', $file, $handle, $options); + } + + private function renderVideoFile( + PhabricatorFile $file, + PhabricatorObjectHandle $handle, + array $options) { + return $this->renderMediaFile('video', $file, $handle, $options); + } + + private function renderMediaFile( + $tag, + PhabricatorFile $file, + PhabricatorObjectHandle $handle, + array $options) { + + $is_video = ($tag == 'video'); if (idx($options, 'autoplay')) { $preload = 'auto'; $autoplay = 'autoplay'; } else { - $preload = 'none'; + // If we don't preload video, the user can't see the first frame and + // has no clue what they're looking at, so always preload. + if ($is_video) { + $preload = 'auto'; + } else { + $preload = 'none'; + } $autoplay = null; } return $this->newTag( - 'audio', + $tag, array( 'controls' => 'controls', 'preload' => $preload, 'autoplay' => $autoplay, 'loop' => idx($options, 'loop') ? 'loop' : null, + 'alt' => $options['alt'], + 'class' => 'phabricator-media', ), $this->newTag( 'source', diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -58,6 +58,11 @@ private $originalFile = self::ATTACHABLE; private $transforms = self::ATTACHABLE; + public function getMimeType() { + $type = parent::getMimeType(); + return $type; + } + public static function initializeNewFile() { $app = id(new PhabricatorApplicationQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) @@ -802,6 +807,16 @@ return idx($mime_map, $mime_type); } + public function isVideo() { + if (!$this->isViewableInBrowser()) { + return false; + } + + $mime_map = PhabricatorEnv::getEnvConfig('files.video-mime-types'); + $mime_type = $this->getMimeType(); + return idx($mime_map, $mime_type); + } + public function isTransformableImage() { // NOTE: The way the 'gd' extension works in PHP is that you can install it // with support for only some file types, so it might be able to handle diff --git a/src/docs/user/userguide/remarkup.diviner b/src/docs/user/userguide/remarkup.diviner --- a/src/docs/user/userguide/remarkup.diviner +++ b/src/docs/user/userguide/remarkup.diviner @@ -410,18 +410,29 @@ {F123, layout=left, float, size=full, alt="a duckling"} -Valid options are: +Valid options for all files are: - **layout** left (default), center, right, inline, link (render a link instead of a thumbnail for images) + - **name** with `layout=link` or for non-images, use this name for the link + text + - **alt** Provide alternate text for assistive technologies. + +Image files support these options: + - **float** If layout is set to left or right, the image will be floated so text wraps around it. - **size** thumb (default), full - - **name** with `layout=link` or for non-images, use this name for the link - text - **width** Scale image to a specific width. - **height** Scale image to a specific height. - - **alt** Provide alternate text for assistive technologies. + +Audio and video files support these options: + + - **media**: Specify the media type as `audio` or `video`. This allows you + to disambiguate how file format which may contain either audio or video + should be rendered. + - **loop**: Loop this media. + - **autoplay**: Automatically begin playing this media. == Embedding Countdowns diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -218,6 +218,17 @@ width: 50%; } +video.phabricator-media { + background: {$greybackground}; +} + +.phabricator-remarkup video { + display: block; + margin: 0 auto; + min-width: 240px; + width: 90%; +} + .phabricator-remarkup-mention-exists { font-weight: bold; background: #e6f3ff; diff --git a/webroot/rsrc/css/phui/phui-property-list-view.css b/webroot/rsrc/css/phui/phui-property-list-view.css --- a/webroot/rsrc/css/phui/phui-property-list-view.css +++ b/webroot/rsrc/css/phui/phui-property-list-view.css @@ -160,6 +160,13 @@ min-width: 240px; } +.phui-property-list-video { + display: block; + margin: 0 auto; + width: 90%; + min-width: 240px; +} + /* When tags appear in property lists, give them a little more vertical spacing. */ .phui-property-list-view .phui-tag-view {