Allow uploading subtitle tracks to videos
when you upload a video, you will now see a [cc] box in the player for that video if you are the uploader of that video and in a modern browser. it will have a link to upload a caption track in SRT or WebVTT format in any language you specify. once you attach a caption track to any of your videos, other people that view it will be able to choose that track while playing it. helpful for people that are deaf or that speak another language! this change also adds google analytics tracking to videos so we know when a given media_id was played, paused, and ended. Test Plan: * record a video, go to play the video * you should see a [cc] button * click the "upload subtitles" link, a dialog should appear that lets you choose a language and file to upload. here's one you can test with: http://mediaelementjs.com/media/NT113_u008_v005_transcript.srt * refresh, you (and anyone else that can see video) should now have the option to select that track for subtitles. (if using in HTML5 player) * you should see a delete 'x' on it to delete it, when you click it it should go away. reload to make sure it is not there. * The user who created the video, or any admins of the course the video is in should be able to manage these captions closes CNVS-324 Change-Id: Id6d4abcb581f0daf101d601221dc45edaad6eaa8 Reviewed-on: https://gerrit.instructure.com/16882 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Bracken Mosbacker <bracken@instructure.com> QA-Review: Adam Phillipps <adam@instructure.com>
This commit is contained in:
parent
4bb2413cfd
commit
26ec655a8d
|
@ -1,3 +1,4 @@
|
|||
#mediaComment.coffee
|
||||
define [
|
||||
'i18n!media_comments'
|
||||
'underscore'
|
||||
|
@ -20,11 +21,14 @@ define [
|
|||
# track events in google analytics
|
||||
mejs.MepDefaults.features.push('googleanalytics')
|
||||
|
||||
getSources = (id) ->
|
||||
getSourcesAndTracks = (id) ->
|
||||
dfd = new $.Deferred
|
||||
$.getJSON "/media_objects/#{id}/info", (data) ->
|
||||
sources = _.map data.media_sources, (source) -> "<source type='#{source.content_type}' src='#{source.url}' />"
|
||||
dfd.resolve {sources, can_add_captions: false}
|
||||
tracks = _.map data.media_tracks, (track) ->
|
||||
languageName = mejs.language.codes[track.locale] || track.locale
|
||||
"<track kind='#{track.kind}' label='#{languageName}' src='#{track.url}' srclang='#{track.locale}' />"
|
||||
dfd.resolve {sources, tracks, can_add_captions: data.can_add_captions}
|
||||
dfd
|
||||
|
||||
mediaCommentActions =
|
||||
|
@ -49,13 +53,13 @@ define [
|
|||
showInline = (id) ->
|
||||
width = Math.min ($holder.closest("div,p,table").width() || VIDEO_WIDTH), VIDEO_WIDTH
|
||||
height = Math.round width / 336 * 240
|
||||
getSources(id).done (sources) ->
|
||||
if sources.sources.length
|
||||
getSourcesAndTracks(id).done (sourcesAndTracks) ->
|
||||
if sourcesAndTracks.sources.length
|
||||
$("#{if mediaType is 'video' then "<video width='#{width}' height='#{height}'" else '<audio'} controls preload autoplay />")
|
||||
.append(sources.sources.join(''))
|
||||
.append(sourcesAndTracks.sources.concat(sourcesAndTracks.tracks).join(''))
|
||||
.appendTo($holder.html(''))
|
||||
.mediaelementplayer
|
||||
can_add_captions: false
|
||||
can_add_captions: sourcesAndTracks.can_add_captions
|
||||
mediaCommendId: id
|
||||
googleAnalyticsTitle: id
|
||||
else
|
||||
|
@ -95,14 +99,15 @@ define [
|
|||
resizable: false
|
||||
close: -> $this.data('mediaelementplayer').pause()
|
||||
|
||||
$dialog.disableWhileLoading getSources(id).done (sources) ->
|
||||
if sources.sources.length
|
||||
$dialog.disableWhileLoading getSourcesAndTracks(id).done (sourcesAndTracks) ->
|
||||
if sourcesAndTracks.sources.length
|
||||
$mediaElement = $("#{if mediaType is 'video' then "<video width='#{width}' height='#{height - spaceNeededForControls}'" else '<audio'} controls preload autoplay />")
|
||||
.append(sourcesAndTracks.sources.concat(sourcesAndTracks.tracks).join(''))
|
||||
.appendTo($dialog)
|
||||
|
||||
$this.data
|
||||
mediaelementplayer: new MediaElementPlayer $mediaElement,
|
||||
can_add_captions: sources.can_add_captions
|
||||
can_add_captions: sourcesAndTracks.can_add_captions
|
||||
mediaCommendId: id
|
||||
googleAnalyticsTitle: id
|
||||
media_comment_dialog: $dialog
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
define [
|
||||
'i18n!media_comments'
|
||||
'underscore'
|
||||
'jst/widget/UploadMediaTrackForm'
|
||||
'vendor/mediaelement-and-player'
|
||||
'jquery'
|
||||
], (I18n, _, template, mejs, $) ->
|
||||
|
||||
class UploadMediaTrackForm
|
||||
|
||||
# video url needs to be the url to mp4 version of the video.
|
||||
# it will be passed along to universalsubtitles.org
|
||||
constructor: (@mediaCommentId, @video_url) ->
|
||||
templateVars =
|
||||
languages: _.map(mejs.language.codes, (name, code) -> {name, code})
|
||||
video_url: @video_url
|
||||
is_amazon_url: @video_url.search(/.mp4/) != -1
|
||||
@$dialog = $(template(templateVars))
|
||||
.appendTo('body')
|
||||
.dialog
|
||||
width: 650
|
||||
resizable: false
|
||||
buttons: [
|
||||
'data-text-while-loading' : I18n.t 'cancel', 'Cancel'
|
||||
text : I18n.t 'cancel', 'Cancel'
|
||||
click: => @$dialog.remove()
|
||||
,
|
||||
class : 'btn-primary'
|
||||
'data-text-while-loading' : I18n.t 'uploading', 'Uploading...'
|
||||
text: I18n.t 'upload', 'Upload'
|
||||
click: @onSubmit
|
||||
]
|
||||
|
||||
onSubmit: =>
|
||||
submitDfd = new $.Deferred()
|
||||
submitDfd.fail =>
|
||||
@$dialog.find('.invalidInputMsg').show()
|
||||
|
||||
@$dialog.disableWhileLoading submitDfd
|
||||
@getFileContent().fail(-> submitDfd.reject()).done (content) =>
|
||||
|
||||
params =
|
||||
content: content
|
||||
locale: @$dialog.find('[name="locale"]').val()
|
||||
|
||||
return submitDfd.reject() unless params.content && params.locale
|
||||
|
||||
|
||||
$.ajaxJSON "/media_objects/#{@mediaCommentId}/media_tracks", 'POST', params, =>
|
||||
submitDfd.resolve()
|
||||
@$dialog.dialog('close')
|
||||
$.flashMessage I18n.t 'track_uploaded_successfully', "Track uploaded successfuly, refresh to see it."
|
||||
|
||||
getFileContent: ->
|
||||
dfd = new $.Deferred
|
||||
file = @$dialog.find('input[name="content"]')[0].files[0]
|
||||
if file
|
||||
reader = new FileReader()
|
||||
reader.onload = (e) ->
|
||||
content = e.target.result
|
||||
dfd.resolve content
|
||||
reader.readAsText file
|
||||
else
|
||||
dfd.reject()
|
||||
dfd
|
|
@ -23,6 +23,24 @@
|
|||
#
|
||||
# @object Media Object
|
||||
# {
|
||||
# // whether or not the current user can upload media_tracks (subtitles) to this Media Object
|
||||
# "can_add_captions": true,
|
||||
# // an array of all the media_tracks uploaded to this Media Object
|
||||
# "media_tracks": [{
|
||||
# "kind": "captions",
|
||||
# "created_at": "2012-09-27T16:46:50-06:00",
|
||||
# "updated_at": "2012-09-27T16:46:50-06:00",
|
||||
# "url": "http://<canvas>/media_objects/0_r949z9lk/media_tracks/1",
|
||||
# "id": 1,
|
||||
# "locale": "af"
|
||||
# }, {
|
||||
# "kind": "subtitles",
|
||||
# "created_at": "2012-09-27T20:29:17-06:00",
|
||||
# "updated_at": "2012-09-27T20:29:17-06:00",
|
||||
# "url": "http://<canvas>/media_objects/0_r949z9lk/media_tracks/14",
|
||||
# "id": 14,
|
||||
# "locale": "cs"
|
||||
# }],
|
||||
# // an array of all the transcoded files (flavors) available for this Media Object
|
||||
# "media_sources": [{
|
||||
# "height": "240",
|
||||
|
@ -55,7 +73,7 @@ class MediaObjectsController < ApplicationController
|
|||
# Returns the Details of the given Media Object.
|
||||
#
|
||||
# @example_request
|
||||
# curl https://<canvas>/api/v1/media_objects/<media_object_id> \
|
||||
# curl https://<canvas>/media_objects/<media_object_id> \
|
||||
# -H 'Authorization: Bearer <token>'
|
||||
#
|
||||
# @returns Media Object
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
#
|
||||
# Copyright (C) 2012 Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas 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, version 3 of the License.
|
||||
#
|
||||
# Canvas 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# @API Media Objects
|
||||
class MediaTracksController < ApplicationController
|
||||
include Api::V1::MediaObject
|
||||
|
||||
TRACK_SETTABLE_ATTRIBUTES = [:kind, :locale, :content]
|
||||
|
||||
# @{not an}API Create a media track
|
||||
#
|
||||
# Create a new media track to be used as captions for different languages or deaf users. for more info, {https://developer.mozilla.org/en-US/docs/HTML/HTML_Elements/track read the MDN docs}
|
||||
#
|
||||
# @argument kind one of: [subtitles, captions, descriptions, chapters, metadata]. default: 'subtitles'
|
||||
# @argument locale Language code of the track being uploaded, examples: ["en", "es", "ru"]
|
||||
# @argument content The contets of the track, in SRT or WebVTT format
|
||||
#
|
||||
# @example_request
|
||||
# curl https://<canvas>/media_objects/<media_object_id>/media_tracks \
|
||||
# -F kind='subtitles' \
|
||||
# -F locale='es' \
|
||||
# -F content='0\n00:00:00,000 --> 00:00:01,000\nInstructor…This is the first sentance\n\n\n1\n00:00:01,000 --> 00:00:04,000\nand a second...' \
|
||||
# -H 'Authorization: Bearer <token>'
|
||||
#
|
||||
# @returns Media Object
|
||||
def create
|
||||
@media_object = MediaObject.active.by_media_id(params[:media_object_id]).first
|
||||
if authorized_action(@media_object, @current_user, :add_captions)
|
||||
track = @media_object.media_tracks.find_or_initialize_by_user_id_and_locale(@current_user.id, params[:locale])
|
||||
track.update_attributes! params.slice(*TRACK_SETTABLE_ATTRIBUTES)
|
||||
render :json => media_object_api_json(@media_object, @current_user, session)
|
||||
end
|
||||
end
|
||||
|
||||
# @{not an}API Get the content of a Media Track
|
||||
#
|
||||
# returns the actual content of the uploaded media track.
|
||||
#
|
||||
# @example_request
|
||||
# curl https://<canvas>/media_objects/<media_object_id>/media_tracks/<media_track_id> \
|
||||
# -H 'Authorization: Bearer <token>'
|
||||
#
|
||||
def show
|
||||
@media_track = MediaTrack.find params[:id]
|
||||
if stale? :etag => @media_track, :last_modified => @media_track.updated_at.utc
|
||||
render :text => @media_track.content
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
# @{not an}API Delete a Media Track
|
||||
#
|
||||
# Deletes the media track.
|
||||
#
|
||||
# @example_request
|
||||
# curl -X DELETE https://<canvas>/media_objects/<media_object_id>/media_tracks/<media_track_id> \
|
||||
# -H 'Authorization: Bearer <token>'
|
||||
#
|
||||
# @returns Media Object
|
||||
def destroy
|
||||
@media_object = MediaObject.by_media_id(params[:media_object_id]).first
|
||||
if authorized_action(@media_object, @current_user, :delete_captions)
|
||||
@track = @media_object.media_tracks.find(params[:media_track_id])
|
||||
if @track.destroy
|
||||
render :json => media_object_api_json(@media_object, @current_user, session)
|
||||
else
|
||||
render :json => @track.errors.to_json, :status => :bad_request
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -23,6 +23,7 @@ class MediaObject < ActiveRecord::Base
|
|||
belongs_to :attachment
|
||||
belongs_to :root_account, :class_name => 'Account'
|
||||
validates_presence_of :media_id
|
||||
has_many :media_tracks, :dependent => :destroy, :order => 'locale'
|
||||
after_create :retrieve_details_later
|
||||
after_save :update_title_on_kaltura_later
|
||||
serialize :data
|
||||
|
@ -48,6 +49,12 @@ class MediaObject < ActiveRecord::Base
|
|||
super
|
||||
end
|
||||
|
||||
|
||||
set_policy do
|
||||
given { |user| self.user && (self.user == user || (self.context && self.context.grants_right?(user, nil, :manage_content))) }
|
||||
can :add_captions and can :delete_captions
|
||||
end
|
||||
|
||||
# if wait_for_completion is true, this will wait SYNCHRONOUSLY for the bulk
|
||||
# upload to complete. Wrap it in a timeout if you ever want it to give up
|
||||
# waiting.
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class MediaTrack < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :media_object, :touch => true
|
||||
validates_presence_of :media_object_id, :content
|
||||
attr_accessible :user_id, :kind, :locale, :content
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
.uploadMediaTrackForm{
|
||||
.media-track-video-url{
|
||||
background-color: #F4F3EE;
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.media-track-content-box{
|
||||
padding: 10px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
form{
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.media-track-form-button{
|
||||
padding: 10px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
<div title="Create/Add Subtitles" class="uploadMediaTrackForm bootstrap-form form-horizontal">
|
||||
|
||||
{{#if is_amazon_url}}
|
||||
<p class="alert alert-info">{{#t "upload_media_track_info"}}<strong>Instructions:</strong>
|
||||
Follow these three steps to create a subtitle file for your video, then upload it here.
|
||||
If you all ready have a srf subtitle file you can skip to step 3.{{/t}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<div class="content-box border border-trbl border-round">
|
||||
{{#if is_amazon_url}}
|
||||
<div class="media-track-step">
|
||||
<p class="uploadMediaTrackFormDescription">
|
||||
<strong>{{#t "upload_media_track_form_step1_label"}}Step 1:{{/t}}</strong>
|
||||
{{#t "upload_media_track_form_description_1"}}Copy this video url{{/t}}
|
||||
</p>
|
||||
<div class="content-callout media-track-video-url">{{video_url}}</div>
|
||||
</div><!-- media track step -->
|
||||
|
||||
<div class="media-track-step">
|
||||
<p class="uploadMediaTrackFormDescription">
|
||||
<strong>{{#t "upload_media_track_form_step2_label"}}Step 2:{{/t}}</strong>
|
||||
{{#t "upload_media_track_form_description_2"}}Create a subtitle file by clicking linking this link and following its instructions.{{/t}}
|
||||
</p>
|
||||
|
||||
<div class="content-box media-track-content-box">
|
||||
<form action="http://www.universalsubtitles.org/en/videos/create/" method="POST" target="_blank" >
|
||||
<input type="hidden" name="video_url" value="{{video_url}}">
|
||||
<button class="btn btn-small media-track-form-button" type="submit" value="Begin">{{#t "media_track_form_button"}}Go to subtitle creation tool{{/t}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div><!-- media track step -->
|
||||
{{/if}}
|
||||
|
||||
<div class="media-track-step">
|
||||
|
||||
<p class="uploadMediaTrackFormDescription">
|
||||
{{#if is_amazon_url}}
|
||||
<strong>{{#t "upload_media_track_form_step3_label"}}Step 3:{{/t}}</strong>
|
||||
{{#t "upload_media_track_form_description"}}
|
||||
Once you have a subtitle track in either the SRT or <a target="_blank" href="http://dev.w3.org/html5/webvtt/">WebVTT</a> format,
|
||||
you can upload it here.
|
||||
{{/t}}
|
||||
{{ else }}
|
||||
{{#t "upload_media_track_form_description_3"}}
|
||||
Upload a subtitle track in either the SRT or <a target="_blank" href="http://dev.w3.org/html5/webvtt/">WebVTT</a> format.
|
||||
{{/t}}
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
<div class="content-box media-track-content-box">
|
||||
<div class="invalidInputMsg alert alert-error" style="display:none;">
|
||||
{{#t "error_message"}}<strong>Error:</strong> You must choose a language and a valid track file.{{/t}}
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="umtf_locale">{{#t "language"}}Language{{/t}}</label>
|
||||
<div class="controls">
|
||||
<select name="locale" id="umtf_locale">
|
||||
<option value=''>{{#t "choose_a_language"}}--Choose a Language--{{/t}}</option>
|
||||
{{#each languages}}
|
||||
<option value="{{code}}">{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div><!-- control group end -->
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="umtf_content">{{#t "file"}}File{{/t}}</label>
|
||||
<div class="controls">
|
||||
<input class="input-file" id="umtf_content" name="content" type="file">
|
||||
</div>
|
||||
</div><!-- control group end -->
|
||||
</div><!-- content-box end -->
|
||||
</div><!-- media track step -->
|
||||
</div><!-- end border border-tbl -->
|
||||
</div>
|
|
@ -343,6 +343,10 @@ ActionController::Routing::Routes.draw do |map|
|
|||
map.media_object_thumbnail 'media_objects/:id/thumbnail', :controller => 'context', :action => 'media_object_thumbnail'
|
||||
map.media_object_info 'media_objects/:media_object_id/info', :controller => 'media_objects', :action => 'show'
|
||||
|
||||
map.show_media_tracks "media_objects/:media_object_id/media_tracks/:id", :controller => :media_tracks, :action => :show, :conditions => {:method => :get}
|
||||
map.create_media_tracks 'media_objects/:media_object_id/media_tracks', :controller => :media_tracks, :action => :create, :conditions => {:method => :post}
|
||||
map.delete_media_tracks "media_objects/:media_object_id/media_tracks/:media_track_id", :controller => :media_tracks, :action => :destroy, :conditions => {:method => :delete}
|
||||
|
||||
map.external_content_success 'external_content/success/:service', :controller => 'external_content', :action => 'success'
|
||||
map.external_content_oembed_retrieve 'external_content/retrieve/oembed', :controller => 'external_content', :action => 'oembed_retrieve'
|
||||
map.external_content_cancel 'external_content/cancel/:service', :controller => 'external_content', :action => 'cancel'
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
class CreateMediaTracks < ActiveRecord::Migration
|
||||
tag :predeploy
|
||||
|
||||
def self.up
|
||||
create_table :media_tracks do |t|
|
||||
t.integer :user_id, :limit => 8
|
||||
t.integer :media_object_id, :limit => 8
|
||||
t.string :kind, :default => "subtitles"
|
||||
t.string :locale, :default => "en"
|
||||
t.text :content
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :media_tracks, [:media_object_id, :locale], :name => 'media_object_id_locale'
|
||||
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :media_tracks
|
||||
end
|
||||
end
|
|
@ -20,7 +20,13 @@ module Api::V1::MediaObject
|
|||
|
||||
def media_object_api_json(media_object, current_user, session)
|
||||
hash = {}
|
||||
hash['can_add_captions'] = media_object.grants_right?(current_user, session, :add_captions)
|
||||
hash['media_sources'] = media_object.media_sources
|
||||
hash['media_tracks'] = media_object.media_tracks.map do |track|
|
||||
api_json(track, current_user, session, :only => %w(kind created_at updated_at id locale)).tap do |json|
|
||||
json.merge! :url => show_media_tracks_url(media_object.media_id, track.id)
|
||||
end
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ describe MediaObjectsController do
|
|||
|
||||
get 'show', :media_object_id => missing_media_id
|
||||
json_parse(response.body).should == {
|
||||
'can_add_captions' => false,
|
||||
'media_tracks' => [],
|
||||
'media_sources' => []
|
||||
}
|
||||
MediaObject.by_media_id(missing_media_id).first.media_id.should == missing_media_id
|
||||
|
|
Loading…
Reference in New Issue