Commit 95b3055f by Mushtaq Ali Committed by GitHub

Merge pull request #15178 from edx/course-video-image

Course video image
parents 7b910953 d2b420a9
......@@ -1975,7 +1975,7 @@ class RerunCourseTest(ContentStoreTestCase):
create_video(
dict(
edx_video_id="tree-hugger",
courses=[source_course.id],
courses=[unicode(source_course.id)],
status='test',
duration=2,
encoded_videos=[]
......
......@@ -440,6 +440,10 @@ ADVANCED_PROBLEM_TYPES = ENV_TOKENS.get('ADVANCED_PROBLEM_TYPES', ADVANCED_PROBL
VIDEO_UPLOAD_PIPELINE = ENV_TOKENS.get('VIDEO_UPLOAD_PIPELINE', VIDEO_UPLOAD_PIPELINE)
################ VIDEO IMAGE STORAGE ###############
VIDEO_IMAGE_SETTINGS = ENV_TOKENS.get('VIDEO_IMAGE_SETTINGS', VIDEO_IMAGE_SETTINGS)
################ PUSH NOTIFICATIONS ###############
PARSE_KEYS = AUTH_TOKENS.get("PARSE_KEYS", {})
......
......@@ -103,6 +103,8 @@ from lms.envs.common import (
CONTACT_EMAIL,
DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH,
# Video Image settings
VIDEO_IMAGE_SETTINGS,
)
from path import Path as path
from warnings import simplefilter
......@@ -1344,3 +1346,24 @@ PROFILE_IMAGE_SIZES_MAP = {
'medium': 50,
'small': 30
}
###################### VIDEO IMAGE STORAGE ######################
VIDEO_IMAGE_DEFAULT_FILENAME = 'images/video-images/default_video_image.png'
VIDEO_IMAGE_SUPPORTED_FILE_FORMATS = {
'.bmp': 'image/bmp',
'.bmp2': 'image/x-ms-bmp', # PIL gives x-ms-bmp format
'.gif': 'image/gif',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png'
}
VIDEO_IMAGE_MAX_FILE_SIZE_MB = '2 MB'
VIDEO_IMAGE_MIN_FILE_SIZE_KB = '2 KB'
VIDEO_IMAGE_MAX_WIDTH = 1280
VIDEO_IMAGE_MAX_HEIGHT = 720
VIDEO_IMAGE_MIN_WIDTH = 640
VIDEO_IMAGE_MIN_HEIGHT = 360
VIDEO_IMAGE_ASPECT_RATIO = 16 / 9.0
VIDEO_IMAGE_ASPECT_RATIO_TEXT = '16:9'
VIDEO_IMAGE_ASPECT_RATIO_ERROR_MARGIN = 0.1
......@@ -335,3 +335,15 @@ FEATURES['CUSTOM_COURSES_EDX'] = True
# API access management -- needed for simple-history to run.
INSTALLED_APPS += ('openedx.core.djangoapps.api_admin',)
########################## VIDEO IMAGE STORAGE ############################
VIDEO_IMAGE_SETTINGS = dict(
VIDEO_IMAGE_MAX_BYTES=2 * 1024 * 1024, # 2 MB
VIDEO_IMAGE_MIN_BYTES=2 * 1024, # 2 KB
STORAGE_KWARGS=dict(
location=MEDIA_ROOT,
base_url=MEDIA_URL,
),
DIRECTORY_PREFIX='video-images/',
)
VIDEO_IMAGE_DEFAULT_FILENAME = 'default_video_image.png'
......@@ -258,6 +258,7 @@
'js/spec/utils/module_spec',
'js/spec/views/active_video_upload_list_spec',
'js/spec/views/previous_video_upload_spec',
'js/spec/views/video_thumbnail_spec',
'js/spec/views/previous_video_upload_list_spec',
'js/spec/views/assets_spec',
'js/spec/views/baseview_spec',
......
......@@ -5,13 +5,16 @@ define([
'use strict';
var VideosIndexFactory = function(
$contentWrapper,
videoImageUploadURL,
videoHandlerUrl,
encodingsDownloadUrl,
defaultVideoImageURL,
concurrentUploadLimit,
uploadButton,
previousUploads,
videoSupportedFileFormats,
videoUploadMaxFileSizeInGB
videoUploadMaxFileSizeInGB,
videoImageSettings
) {
var activeView = new ActiveVideoUploadListView({
postUrl: videoHandlerUrl,
......@@ -19,6 +22,7 @@ define([
uploadButton: uploadButton,
videoSupportedFileFormats: videoSupportedFileFormats,
videoUploadMaxFileSizeInGB: videoUploadMaxFileSizeInGB,
videoImageSettings: videoImageSettings,
onFileUploadDone: function(activeVideos) {
$.ajax({
url: videoHandlerUrl,
......@@ -34,18 +38,24 @@ define([
isActive[0].get('status') === ActiveVideoUpload.STATUS_COMPLETE;
}),
updatedView = new PreviousVideoUploadListView({
videoImageUploadURL: videoImageUploadURL,
defaultVideoImageURL: defaultVideoImageURL,
videoHandlerUrl: videoHandlerUrl,
collection: updatedCollection,
encodingsDownloadUrl: encodingsDownloadUrl
encodingsDownloadUrl: encodingsDownloadUrl,
videoImageSettings: videoImageSettings
});
$contentWrapper.find('.wrapper-assets').replaceWith(updatedView.render().$el);
});
}
}),
previousView = new PreviousVideoUploadListView({
videoImageUploadURL: videoImageUploadURL,
defaultVideoImageURL: defaultVideoImageURL,
videoHandlerUrl: videoHandlerUrl,
collection: new Backbone.Collection(previousUploads),
encodingsDownloadUrl: encodingsDownloadUrl
encodingsDownloadUrl: encodingsDownloadUrl,
videoImageSettings: videoImageSettings
});
$contentWrapper.append(activeView.render().$el);
$contentWrapper.append(previousView.render().$el);
......
......@@ -25,7 +25,8 @@ define(
);
var view = new PreviousVideoUploadListView({
collection: collection,
videoHandlerUrl: videoHandlerUrl
videoHandlerUrl: videoHandlerUrl,
videoImageSettings: {}
});
return view.render().$el;
},
......@@ -43,10 +44,10 @@ define(
$el = render(numVideos),
firstVideoId = 'dummy_id_0',
requests = AjaxHelpers.requests(test),
firstVideoSelector = '.js-table-body tr:first-child';
firstVideoSelector = '.js-table-body .video-row:first-child';
// total number of videos should be 5 before remove
expect($el.find('.js-table-body tr').length).toEqual(numVideos);
expect($el.find('.js-table-body .video-row').length).toEqual(numVideos);
// get first video element
firstVideo = $el.find(firstVideoSelector);
......@@ -71,7 +72,7 @@ define(
}
// verify total number of videos after Remove/Cancel
expect($el.find('.js-table-body tr').length).toEqual(numVideos);
expect($el.find('.js-table-body .video-row').length).toEqual(numVideos);
// verify first video id after Remove/Cancel
firstVideo = $el.find(firstVideoSelector);
......@@ -81,13 +82,13 @@ define(
it('should render an empty collection', function() {
var $el = render(0);
expect($el.find('.js-table-body').length).toEqual(1);
expect($el.find('.js-table-body tr').length).toEqual(0);
expect($el.find('.js-table-body .video-row').length).toEqual(0);
});
it('should render a non-empty collection', function() {
var $el = render(5);
expect($el.find('.js-table-body').length).toEqual(1);
expect($el.find('.js-table-body tr').length).toEqual(5);
expect($el.find('.js-table-body .video-row').length).toEqual(5);
});
it('removes video upon click on Remove button', function() {
......
......@@ -14,7 +14,8 @@ define(
},
view = new PreviousVideoUploadView({
model: new Backbone.Model($.extend({}, defaultData, modelData)),
videoHandlerUrl: '/videos/course-v1:org.0+course_0+Run_0'
videoHandlerUrl: '/videos/course-v1:org.0+course_0+Run_0',
videoImageSettings: {}
});
return view.render().$el;
};
......@@ -30,24 +31,6 @@ define(
expect($el.find('.name-col').text()).toEqual(testName);
});
_.each(
[
{desc: 'zero as pending', seconds: 0, expected: 'Pending'},
{desc: 'less than one second as zero', seconds: 0.75, expected: '0:00'},
{desc: 'with minutes and without seconds', seconds: 900, expected: '15:00'},
{desc: 'with seconds and without minutes', seconds: 15, expected: '0:15'},
{desc: 'with minutes and seconds', seconds: 915, expected: '15:15'},
{desc: 'with seconds padded', seconds: 5, expected: '0:05'},
{desc: 'longer than an hour as many minutes', seconds: 7425, expected: '123:45'}
],
function(caseInfo) {
it('should render duration ' + caseInfo.desc, function() {
var $el = render({duration: caseInfo.seconds});
expect($el.find('.duration-col').text()).toEqual(caseInfo.expected);
});
}
);
it('should render created timestamp correctly', function() {
var fakeDate = 'fake formatted date';
spyOn(Date.prototype, 'toLocaleString').and.callFake(
......
define(
['underscore', 'gettext', 'js/utils/date_utils', 'js/views/baseview', 'common/js/components/views/feedback_prompt',
'common/js/components/views/feedback_notification', 'common/js/components/utils/view_utils',
'edx-ui-toolkit/js/utils/html-utils', 'text!templates/previous-video-upload.underscore'],
function(_, gettext, DateUtils, BaseView, PromptView, NotificationView, ViewUtils, HtmlUtils,
'common/js/components/views/feedback_notification', 'js/views/video_thumbnail',
'common/js/components/utils/view_utils', 'edx-ui-toolkit/js/utils/html-utils',
'text!templates/previous-video-upload.underscore'],
function(_, gettext, DateUtils, BaseView, PromptView, NotificationView, VideoThumbnailView, ViewUtils, HtmlUtils,
previousVideoUploadTemplate) {
'use strict';
var PreviousVideoUploadView = BaseView.extend({
tagName: 'tr',
tagName: 'div',
className: 'video-row',
events: {
'click .remove-video-button.action-button': 'removeVideo'
......@@ -16,22 +19,21 @@ define(
initialize: function(options) {
this.template = HtmlUtils.template(previousVideoUploadTemplate);
this.videoHandlerUrl = options.videoHandlerUrl;
},
this.videoImageUploadEnabled = options.videoImageSettings.video_image_upload_enabled;
renderDuration: function(seconds) {
var minutes = Math.floor(seconds / 60);
var seconds = Math.floor(seconds - minutes * 60);
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
if (this.videoImageUploadEnabled) {
this.videoThumbnailView = new VideoThumbnailView({
model: this.model,
imageUploadURL: options.videoImageUploadURL,
defaultVideoImageURL: options.defaultVideoImageURL,
videoImageSettings: options.videoImageSettings
});
}
},
render: function() {
var duration = this.model.get('duration');
var renderedAttributes = {
// Translators: This is listed as the duration for a video
// that has not yet reached the point in its processing by
// the servers where its duration is determined.
duration: duration > 0 ? this.renderDuration(duration) : gettext('Pending'),
videoImageUploadEnabled: this.videoImageUploadEnabled,
created: DateUtils.renderDate(this.model.get('created')),
status: this.model.get('status')
};
......@@ -41,12 +43,15 @@ define(
_.extend({}, this.model.attributes, renderedAttributes)
)
);
if (this.videoImageUploadEnabled) {
this.videoThumbnailView.setElement(this.$('.thumbnail-col')).render();
}
return this;
},
removeVideo: function(event) {
var videoView = this;
event.preventDefault();
ViewUtils.confirmThenRunOperation(
......
......@@ -9,9 +9,13 @@ define(
initialize: function(options) {
this.template = this.loadTemplate('previous-video-upload-list');
this.encodingsDownloadUrl = options.encodingsDownloadUrl;
this.videoImageUploadEnabled = options.videoImageSettings.video_image_upload_enabled;
this.itemViews = this.collection.map(function(model) {
return new PreviousVideoUploadView({
videoImageUploadURL: options.videoImageUploadURL,
defaultVideoImageURL: options.defaultVideoImageURL,
videoHandlerUrl: options.videoHandlerUrl,
videoImageSettings: options.videoImageSettings,
model: model
});
});
......@@ -20,7 +24,10 @@ define(
render: function() {
var $el = this.$el,
$tabBody;
$el.html(this.template({encodingsDownloadUrl: this.encodingsDownloadUrl}));
$el.html(this.template({
encodingsDownloadUrl: this.encodingsDownloadUrl,
videoImageUploadEnabled: this.videoImageUploadEnabled
}));
$tabBody = $el.find('.js-table-body');
_.each(this.itemViews, function(view) {
$tabBody.append(view.render().$el);
......
......@@ -36,7 +36,7 @@
font-size: 80%;
word-wrap: break-word;
th {
th, .video-head-col {
@extend %t-copy-sub2;
background-color: $gray-l5;
padding: 0 ($baseline/2) ($baseline*0.75) ($baseline/2);
......@@ -60,18 +60,18 @@
}
}
td {
td, .video-col {
padding: ($baseline/2);
vertical-align: middle;
text-align: left;
}
tbody {
tbody, .js-table-body {
box-shadow: 0 2px 2px $shadow-l1;
border: 1px solid $gray-l4;
background: $white;
tr {
tr, .video-row {
@include transition(all $tmg-f2 ease-in-out 0s);
border-top: 1px solid $gray-l4;
......@@ -106,7 +106,7 @@
}
&:hover {
background-color: $blue-l5;
background-color: $blue-l5 !important;
.date-col,
.embed-col,
......
......@@ -147,6 +147,11 @@
}
.assets-library {
.js-table-body .video-id-col {
word-break: break-all;
}
.assets-title {
display: inline-block;
width: flex-grid(5, 9);
......@@ -163,4 +168,185 @@
@extend %actions-list;
}
}
.video-table {
.video-row {
display: table;
table-layout: fixed;
width: 100%;
.video-col {
display: table-cell;
}
.name-col {
width: 25%;
}
.thumbnail-col, .video-id-col {
width: 15%;
}
.date-col, .status-col {
width: 10%;
}
.actions-col {
width: 5%;
}
.video-head-col.thumbnail-col {
width: 17% !important;
}
}
}
.thumbnail-error-wrapper {
display: table-row;
white-space: nowrap;
color: $red;
.icon {
margin: ($baseline*0.75) ($baseline/4) 0 ($baseline/2);
}
}
$thumbnail-width: ($baseline*7.5);
$thumbnail-height: ($baseline*5);
.thumbnail-wrapper {
position: relative;
max-width: $thumbnail-width;
max-height: $thumbnail-height;
img {
width: $thumbnail-width;
height: $thumbnail-height;
}
* {
cursor: pointer;
}
&.upload,
&.requirements {
border: 1px dashed $gray-l3;
}
&.requirements {
.requirements-text {
font-weight: 600;
}
.requirements-instructions {
font-size: 15px;
font-family: "Open Sans";
text-align: left;
color: $gray-d2;
line-height: 1.5;
}
.video-duration {
opacity: 0;
}
}
&.edit {
background: black;
&:hover,
&:focus,
&.focused {
img, .video-duration {
@include transition(all 0.3s linear);
opacity: 0.1;
}
}
}
&.progress {
background: white;
img {
@include transition(all 0.5s linear);
opacity: 0.15;
}
.action-icon {
display: block;
}
}
&.upload .thumbnail-action {
color: $blue;
}
&.progress .thumbnail-action {
.action-icon {
@include font-size(20);
}
}
&.edit {
background-color: #4e4e4e;
}
&.edit .thumbnail-action .action-icon.edit {
display: none;
}
&.edit .thumbnail-action .edit-container {
background-color: $white;
padding: ($baseline/4);
border-radius: ($baseline/5);
margin-top: ($baseline/2);
display: none;
}
&.edit .action-text {
color: $white;
}
.thumbnail-action {
@include font-size(14);
}
.thumbnail-overlay > :not(.upload-image-input) {
position: absolute;
text-align: center;
top: 50%;
left: 5px;;
right: 5px;
@include transform(translateY(-50%));
z-index: 1;
}
.upload-image-input {
position: absolute;
top: 0;
left: 0;
right: 0;
opacity: 0;
z-index: 6;
width: $thumbnail-width;
height: $thumbnail-height;
}
.video-duration {
position: absolute;
text-align: center;
bottom: 1px;
@include right(1px);
width: auto;
min-width: 25%;
color: white;
padding: ($baseline/10) ($baseline/5);
background-color: black;
}
&.focused {
box-shadow: 0 0 ($baseline/5) 1px $blue;
}
&.error {
box-shadow: 0 0 ($baseline/5) 1px $red;
}
}
}
......@@ -5,17 +5,19 @@
<%- gettext("Download available encodings (.csv)") %>
</a>
</div>
<table class="assets-table">
<thead>
<tr>
<th><%- gettext("Name") %></th>
<th><%- gettext("Duration") %></th>
<th><%- gettext("Date Added") %></th>
<th><%- gettext("Video ID") %></th>
<th><%- gettext("Status") %></th>
<th><%- gettext("Action") %></th>
</tr>
</thead>
<tbody class="js-table-body"></tbody>
</table>
<div class="assets-table video-table">
<div class="js-table-head">
<div class="video-row">
<% if (videoImageUploadEnabled) { %>
<div class="video-head-col video-col thumbnail-col"><%- gettext("Thumbnail") %></div>
<% } %>
<div class="video-head-col video-col name-col"><%- gettext("Name") %></div>
<div class="video-head-col video-col date-col"><%- gettext("Date Added") %></div>
<div class="video-head-col video-col video-id-col"><%- gettext("Video ID") %></div>
<div class="video-head-col video-col status-col"><%- gettext("Status") %></div>
<div class="video-head-col video-col actions-col"><%- gettext("Action") %></div>
</div>
</div>
<div class="js-table-body"></div>
</div>
</div>
<td class="name-col"><%- client_video_id %></td>
<td class="duration-col"><%- duration %></td>
<td class="date-col"><%- created %></td>
<td class="video-id-col"><%- edx_video_id %></td>
<td class="status-col"><%- status %></td>
<td class="actions-col">
<div class="video-row-container">
<% if (videoImageUploadEnabled) { %>
<div class="video-col thumbnail-col"></div>
<% } %>
<div class="video-col name-col"><%- client_video_id %></div>
<div class="video-col date-col"><%- created %></div>
<div class="video-col video-id-col"><%- edx_video_id %></div>
<div class="video-col status-col"><%- status %></div>
<div class="video-col actions-col">
<ul class="actions-list">
<li class="action-item action-remove">
<a href="#" data-tooltip="<%- gettext('Remove this video') %>" class="remove-video-button action-button">
......@@ -12,4 +15,5 @@
</a>
</li>
</ul>
</td>
</div>
</div>
<div class="thumbnail-error-wrapper thumbnail-error" data-video-id="<%- videoId %>">
<span class="icon fa fa-exclamation-triangle" aria-hidden="true"></span>
<span class="action-text"><%- errorText %></span>
</div>
<div class="thumbnail-wrapper <%- action === 'upload' ? 'upload' : '' %>" tabindex="-1">
<img src="<%- thumbnailURL %>" alt="<%- imageAltText %>">
<div class="thumbnail-overlay">
<input id="thumb-<%- videoId %>" class="upload-image-input" type="file" name="file" accept=".bmp, .jpg, .jpeg, .png, .gif"/>
<label for="thumb-<%- videoId %>" class="thumbnail-action">
<span class="main-icon action-icon <%- actionInfo.name %>" aria-hidden="true"><%- actionInfo.icon %></span>
<span class="action-text-sr sr"></span>
<span class="action-text"><%- actionInfo.text %></span>
<div class="edit-container">
<span class="action-icon" aria-hidden="true"><%- actionInfo.icon %></span>
<span class="edit-action-text"><%- actionInfo.actionText %></span>
</div>
</label>
<span class="requirements-text-sr sr">
<%- edx.StringUtils.interpolate(
gettext("Recommended image resolution is {imageResolution}, maximum image file size should be {maxFileSize} and format must be one of {supportedImageFormats}."),
{imageResolution: videoImageResolution, maxFileSize: videoImageMaxSize.humanize, supportedImageFormats: videoImageSupportedFileFormats.humanize}
) %>
</span>
</div>
<% if(duration) { %>
<div class="video-duration">
<span class="duration-text-human sr"><%- duration.humanize %></span>
<span class="duration-text-machine" aria-hidden="true"><%- duration.machine %></span>
</div>
<% } %>
</div>
......@@ -29,13 +29,16 @@
var $contentWrapper = $(".content-primary");
VideosIndexFactory(
$contentWrapper,
"${image_upload_url | n, js_escaped_string}",
"${video_handler_url | n, js_escaped_string}",
"${encodings_download_url | n, js_escaped_string}",
"${default_video_image_url | n, js_escaped_string}",
${concurrent_upload_limit | n, dump_js_escaped_json},
$(".nav-actions .upload-button"),
$contentWrapper.data("previous-uploads"),
${video_supported_file_formats | n, dump_js_escaped_json},
${video_upload_max_file_size | n, dump_js_escaped_json}
${video_upload_max_file_size | n, dump_js_escaped_json},
${video_image_settings | n, dump_js_escaped_json}
);
});
</%block>
......
from django.conf import settings
from django.conf.urls import include, patterns, url
from django.conf.urls.static import static
# There is a course creators admin table.
from ratelimitbackend import admin
......@@ -112,6 +113,7 @@ urlpatterns += patterns(
url(r'^textbooks/{}$'.format(settings.COURSE_KEY_PATTERN), 'textbooks_list_handler'),
url(r'^textbooks/{}/(?P<textbook_id>\d[^/]*)$'.format(settings.COURSE_KEY_PATTERN), 'textbooks_detail_handler'),
url(r'^videos/{}(?:/(?P<edx_video_id>[-\w]+))?$'.format(settings.COURSE_KEY_PATTERN), 'videos_handler'),
url(r'^video_images/{}(?:/(?P<edx_video_id>[-\w]+))?$'.format(settings.COURSE_KEY_PATTERN), 'video_images_handler'),
url(r'^video_encodings_download/{}$'.format(settings.COURSE_KEY_PATTERN), 'video_encodings_download'),
url(r'^group_configurations/{}$'.format(settings.COURSE_KEY_PATTERN), 'group_configurations_list_handler'),
url(r'^group_configurations/{}/(?P<group_configuration_id>\d+)(/)?(?P<group_id>\d+)?$'.format(
......@@ -189,6 +191,11 @@ if settings.DEBUG:
except ImportError:
pass
urlpatterns += static(
settings.VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['base_url'],
document_root=settings.VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['location']
)
if 'debug_toolbar' in settings.INSTALLED_APPS:
import debug_toolbar
urlpatterns += (
......
......@@ -4,7 +4,7 @@
<div
id="video_id"
class="video closed"
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["/base/fixtures/hls/hls.m3u8", "/base/fixtures/test.mp4","/base/fixtures/test.webm"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "/base/fixtures/youtube_iframe_api.js", "ytImageUrl": "", "ytTestTimeout": "1500", "ytMetadataUrl": "www.googleapis.com/youtube/v3/videos/", "source": ""}'
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["/base/fixtures/hls/hls.m3u8", "/base/fixtures/test.mp4","/base/fixtures/test.webm"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "/base/fixtures/youtube_iframe_api.js", "ytImageUrl": "", "ytTestTimeout": "1500", "ytMetadataUrl": "www.googleapis.com/youtube/v3/videos/", "source": "", "poster": "/media/video-images/poster.png"}'
>
<div class="focus_grabber first"></div>
......
......@@ -4,7 +4,7 @@
<div
id="video_id"
class="video closed"
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["/base/fixtures/test.mp4","/base/fixtures/test.webm","/base/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "/base/fixtures/youtube_iframe_api.js", "ytImageUrl": "", "ytTestTimeout": "1500", "ytMetadataUrl": "www.googleapis.com/youtube/v3/videos/", "source": "", "html5_sources": ["http://youtu.be/3_yD_cEKoCk.mp4"]}'
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["/base/fixtures/test.mp4","/base/fixtures/test.webm","/base/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "/base/fixtures/youtube_iframe_api.js", "ytImageUrl": "", "ytTestTimeout": "1500", "ytMetadataUrl": "www.googleapis.com/youtube/v3/videos/", "source": "", "html5_sources": ["http://youtu.be/3_yD_cEKoCk.mp4"], "poster": "/media/video-images/poster.png"}'
>
<div class="focus_grabber first"></div>
......
......@@ -4,7 +4,8 @@
var state,
oldOTBD,
playbackRates = [0.75, 1.0, 1.25, 1.5],
describeInfo;
describeInfo,
POSTER_URL = '/media/video-images/poster.png';
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
......@@ -320,6 +321,15 @@
}).done(done);
});
});
describe('poster', function() {
it('has url in player config', function() {
expect(state.videoPlayer.player.config.poster).toEqual(POSTER_URL);
expect(state.videoPlayer.player.videoEl).toHaveAttrs({
poster: POSTER_URL
});
});
});
});
describe('non-hls encoding', function() {
......@@ -338,6 +348,28 @@
jasmine.getEnv().describe(describeInfo.description, describeInfo.specDefinitions);
});
it('does not show poster for html5 video if url is not present', function() {
state = jasmine.initializePlayer(
'video_html5.html',
{
poster: null
}
);
expect(state.videoPlayer.player.config.poster).toEqual(null);
expect(state.videoPlayer.player.videoEl).not.toHaveAttr('poster');
});
it('does not show poster for hls video if url is not present', function() {
state = jasmine.initializePlayer(
'video_hls.html',
{
poster: null
}
);
expect(state.videoPlayer.player.config.poster).toEqual(null);
expect(state.videoPlayer.player.videoEl).not.toHaveAttr('poster');
});
it('native controls are used on iPhone', function() {
window.onTouchBasedDevice.and.returnValue(['iPhone']);
......
......@@ -44,6 +44,9 @@ function(_) {
* // video format of the source. Supported
* // video formats are: 'mp4', 'webm', and
* // 'ogg'.
* poster: Video poster URL
*
* browserIsSafari: Flag to tell if current browser is Safari
*
* events: { // Object's properties identify the
* // events that the API fires, and the
......@@ -320,6 +323,11 @@ function(_) {
this.videoEl.prop('controls', true);
}
// Set video poster
if (this.config.poster) {
this.videoEl.prop('poster', this.config.poster);
}
// Place the <video> element on the page.
this.videoEl.appendTo(el.find('.video-player > div:first-child'));
};
......
......@@ -162,6 +162,7 @@ function(HTML5Video, HTML5HLSVideo, Resizer, HLS, _) {
commonPlayerConfig = {
playerVars: state.videoPlayer.playerVars,
videoSources: state.config.sources,
poster: state.config.poster,
browserIsSafari: state.browserIsSafari,
events: {
onReady: state.videoPlayer.onReady,
......
......@@ -18,7 +18,7 @@ import datetime
from uuid import uuid4
from lxml import etree
from mock import ANY, Mock, patch
from mock import ANY, Mock, patch, MagicMock
import ddt
from django.conf import settings
......@@ -673,7 +673,7 @@ class VideoExportTestCase(VideoDescriptorTestBase):
"""
Test that we write the correct XML on export.
"""
def mock_val_export(edx_video_id):
def mock_val_export(edx_video_id, course_id):
"""Mock edxval.api.export_to_xml"""
return etree.Element(
'video_asset',
......@@ -695,6 +695,7 @@ class VideoExportTestCase(VideoDescriptorTestBase):
self.descriptor.download_video = True
self.descriptor.transcripts = {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'}
self.descriptor.edx_video_id = 'test_edx_video_id'
self.descriptor.runtime.course_id = MagicMock()
xml = self.descriptor.definition_to_xml(None) # We don't use the `resource_fs` parameter
parser = etree.XMLParser(remove_blank_text=True)
......@@ -718,6 +719,7 @@ class VideoExportTestCase(VideoDescriptorTestBase):
mock_val_api.ValVideoNotFoundError = _MockValVideoNotFoundError
mock_val_api.export_to_xml = Mock(side_effect=mock_val_api.ValVideoNotFoundError)
self.descriptor.edx_video_id = 'test_edx_video_id'
self.descriptor.runtime.course_id = MagicMock()
xml = self.descriptor.definition_to_xml(None)
parser = etree.XMLParser(remove_blank_text=True)
......
......@@ -310,7 +310,10 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
'streams': self.youtube_streams,
'sub': self.sub,
'sources': sources,
'poster': edxval_api and edxval_api.get_course_video_image_url(
course_id=self.runtime.course_id.for_branch(None),
edx_video_id=self.edx_video_id.strip()
),
# This won't work when we move to data that
# isn't on the filesystem
'captionDataDir': getattr(self, 'data_dir', None),
......@@ -653,7 +656,10 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
if self.edx_video_id and edxval_api:
try:
xml.append(edxval_api.export_to_xml(self.edx_video_id))
xml.append(edxval_api.export_to_xml(
self.edx_video_id,
unicode(self.runtime.course_id.for_branch(None)))
)
except edxval_api.ValVideoNotFoundError:
pass
......
......@@ -796,6 +796,9 @@ XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {})
XBLOCK_SETTINGS.setdefault("VideoDescriptor", {})["licensing_enabled"] = FEATURES.get("LICENSING", False)
XBLOCK_SETTINGS.setdefault("VideoModule", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.get('YOUTUBE_API_KEY', YOUTUBE_API_KEY)
##### VIDEO IMAGE STORAGE #####
VIDEO_IMAGE_SETTINGS = ENV_TOKENS.get('VIDEO_IMAGE_SETTINGS', VIDEO_IMAGE_SETTINGS)
##### CDN EXPERIMENT/MONITORING FLAGS #####
CDN_VIDEO_URLS = ENV_TOKENS.get('CDN_VIDEO_URLS', CDN_VIDEO_URLS)
ONLOAD_BEACON_SAMPLE_RATE = ENV_TOKENS.get('ONLOAD_BEACON_SAMPLE_RATE', ONLOAD_BEACON_SAMPLE_RATE)
......
......@@ -2564,6 +2564,22 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60
TIME_ZONE_DISPLAYED_FOR_DEADLINES = 'UTC'
########################## VIDEO IMAGE STORAGE ############################
VIDEO_IMAGE_SETTINGS = dict(
VIDEO_IMAGE_MAX_BYTES=2 * 1024 * 1024, # 2 MB
VIDEO_IMAGE_MIN_BYTES=2 * 1024, # 2 KB
# Backend storage
# STORAGE_CLASS='storages.backends.s3boto.S3BotoStorage',
# STORAGE_KWARGS=dict(bucket='video-image-bucket'),
STORAGE_KWARGS=dict(
location=MEDIA_ROOT,
base_url=MEDIA_URL,
),
DIRECTORY_PREFIX='video-images/',
)
# Source:
# http://loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt according to http://en.wikipedia.org/wiki/ISO_639-1
# Note that this is used as the set of choices to the `code` field of the
......
......@@ -7,7 +7,6 @@
<input class="upload-button-input" type="file" name="<%= inputName %>"/>
</label>
<button class="upload-submit" type="button" hidden="true"><%= uploadButtonTitle %></button>
<button class="u-field-remove-button" type="button">
<span class="remove-button-icon" aria-hidden="true"><%= removeButtonIcon %></span>
<span class="remove-button-title" aria-live="polite"><%= removeButtonTitle %></span>
......
......@@ -11,7 +11,7 @@ from PIL import Image
@contextmanager
def make_image_file(dimensions=(320, 240), extension=".jpeg", force_size=None, orientation=None):
def make_image_file(dimensions=(320, 240), prefix='tmp', extension='.jpeg', force_size=None, orientation=None):
"""
Yields a named temporary file created with the specified image type and
options.
......@@ -21,9 +21,13 @@ def make_image_file(dimensions=(320, 240), extension=".jpeg", force_size=None, o
The temporary file will be closed and deleted automatically upon exiting
the `with` block.
prefix - To add prefix to random image file name, after adding will be like <custom-prefix><random-name>.png
otherwise by default `tmp` is added making file name tmp<random-name>.png.
"""
image = Image.new('RGB', dimensions, "green")
image_file = NamedTemporaryFile(suffix=extension)
image_file = NamedTemporaryFile(prefix=prefix, suffix=extension)
try:
if orientation and orientation in xrange(1, 9):
exif_bytes = piexif.dump({'0th': {piexif.ImageIFD.Orientation: orientation}})
......
......@@ -77,7 +77,7 @@ git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002
git+https://github.com/edx/edx-ora2.git@1.4.3#egg=ora2==1.4.3
-e git+https://github.com/edx/edx-submissions.git@2.0.0#egg=edx-submissions==2.0.0
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
git+https://github.com/edx/edx-val.git@0.0.13#egg=edxval==0.0.13
git+https://github.com/edx/edx-val.git@0.0.16#egg=edxval==0.0.16
git+https://github.com/pmitros/RecommenderXBlock.git@v1.2#egg=recommender-xblock==1.2
git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1
-e git+https://github.com/pmitros/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment