Skip to content

Commit

Permalink
0.10.1: Improve Safari UX; enhance Studio editor (#303)
Browse files Browse the repository at this point in the history
* Bump __version__ to 0.10.0

* Catch API connection exception

* Improve accordion transcripts switch UX in studio view (#300)

* Add multiple transcripts downloading (#301)

* Fix Safari empty transcripts issue

* Fix empty transcripts generator case

* Brightcove retranscode issue (#302)

* Normalize retranscode studio editor section

* Add Brightcove API retranscode error handling

* Hide back transcripts download url

* Prepare CHANGELOG.md for a new release

* Bump __version__ to 0.10.1
  • Loading branch information
wowkalucky authored Feb 13, 2018
1 parent b859c69 commit abb7fae
Show file tree
Hide file tree
Showing 17 changed files with 232 additions and 72 deletions.
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## [0.10.1] - 2018-02-13

## Added

- Multiple transcripts downloading;
- Error handling during Brightcove video re-transcode job submitting;

## Fixed

- Safari `empty transcripts` issue;
- Studio editor improvements:
- transcripts accordion switch;
- re-transcode button styling;

## [0.10.0] - 2018-01-24

## Added
Expand Down Expand Up @@ -282,4 +296,5 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
[0.9.3]: https://github.com/raccoongang/xblock-video/compare/v0.9.2...v0.9.3
[0.9.4]: https://github.com/raccoongang/xblock-video/compare/v0.9.3...v0.9.4
[0.10.0]: https://github.com/raccoongang/xblock-video/compare/v0.9.4...v0.10.0
[Unreleased]: https://github.com/raccoongang/xblock-video/compare/v0.10.0...HEAD
[0.10.1]: https://github.com/raccoongang/xblock-video/compare/v0.10.0...v0.10.1
[Unreleased]: https://github.com/raccoongang/xblock-video/compare/v0.10.1...HEAD
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def package_data(pkg, roots):
dependency_links=[
# At the moment of writing PyPI hosts outdated version of xblock-utils, hence git
# Replace dependency links with numbered versions when it's released on PyPI
'git+https://github.com/edx/[email protected].2#egg=xblock-utils-1.0.5',
'git+https://github.com/edx/[email protected].5#egg=xblock-utils==1.0.5',
],
install_requires=[
'XBlock>=0.4.10,<2.0.0',
Expand Down
2 changes: 1 addition & 1 deletion video_xblock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Video xblock module.
"""

__version__ = '0.10.0'
__version__ = '0.10.1'

# pylint: disable=wildcard-import
from .video_xblock import * # nopep8
45 changes: 35 additions & 10 deletions video_xblock/backends/brightcove.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,14 @@ def _refresh_access_token(self):
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Basic " + auth_string
}
resp = requests.post(url, headers=headers, data=params)
if resp.status_code == httplib.OK:
result = resp.json()
return result['access_token']
try:
resp = requests.post(url, headers=headers, data=params)
if resp.status_code == httplib.OK:
result = resp.json()
return result['access_token']
except IOError:
log.exception(_("Connection issue. Couldn't refresh API access token."))
return None

def get(self, url, headers=None, can_retry=True):
"""
Expand Down Expand Up @@ -153,13 +157,21 @@ def post(self, url, payload, headers=None, can_retry=True):
headers_.update(headers)

resp = requests.post(url, data=payload, headers=headers_)
log.debug("BC response status: {}".format(resp.status_code))
if resp.status_code in (httplib.OK, httplib.CREATED):
return resp.json()
elif resp.status_code == httplib.UNAUTHORIZED and can_retry:
self.access_token = self._refresh_access_token()
return self.post(url, payload, headers, can_retry=False)
else:
raise BrightcoveApiClientError

try:
resp_dict = resp.json()[0]
log.warn("API error code: %s - %s", resp_dict.get(u'error_code'), resp_dict.get(u'message'))
except (ValueError, IndexError):
message = _("Can't parse unexpected response during POST request to Brightcove API!")
log.exception(message)
resp_dict = {"message": message}
return resp_dict


class BrightcoveHlsMixin(object):
Expand All @@ -169,6 +181,8 @@ class BrightcoveHlsMixin(object):
These features are:
1. Video playback autoquality. i.e. adjusting video bitrate depending on client's bandwidth.
2. Video content encryption using short-living keys.
NOTE(wowkalucky): Dynamic Ingest is the legacy ingest system. New Video Cloud accounts use Dynamic Delivery.
"""

DI_PROFILES = {
Expand Down Expand Up @@ -240,6 +254,7 @@ def submit_retranscode_job(self, account_id, video_id, profile_type):
- default - re-transcode using default DI profile;
- autoquality - re-transcode using HLS only profile;
- encryption - re-transcode using HLS with encryption profile;
ref: https://support.brightcove.com/dynamic-ingest-api
"""
url = 'https://ingest.api.brightcove.com/v1/accounts/{account_id}/videos/{video_id}/ingest-requests'.format(
account_id=account_id, video_id=video_id
Expand All @@ -255,9 +270,18 @@ def submit_retranscode_job(self, account_id, video_id, profile_type):
if profile_type != 'default':
retranscode_params['profile'] = self.DI_PROFILES[profile_type]['name']
res = self.api_client.post(url, json.dumps(retranscode_params))
self.xblock.metadata['retranscode-status'] = (
'ReTranscode request submitted {:%Y-%m-%d %H:%M} UTC using profile "{}". Job id: {}'.format(
datetime.utcnow(), retranscode_params.get('profile', 'default'), res['id']))
if u'error_code' in res:
self.xblock.metadata['retranscode-status'] = (
'ReTranscode request encountered error {:%Y-%m-%d %H:%M} UTC using profile "{}".\nMessage: {}'.format(
datetime.utcnow(), retranscode_params.get('profile', 'default'), res['message']
)
)
else:
self.xblock.metadata['retranscode-status'] = (
'ReTranscode request submitted {:%Y-%m-%d %H:%M} UTC using profile "{}". Job id: {}'.format(
datetime.utcnow(), retranscode_params.get('profile', 'default'), res['id']
)
)
return res

def get_video_renditions(self, account_id, video_id):
Expand Down Expand Up @@ -388,6 +412,7 @@ def get_frag(self, **context):
Because of this it doesn't use `super.get_frag()`.
"""
context['player_state'] = json.dumps(context['player_state'])
log.debug('CONTEXT: player_state: %s', context.get('player_state'))

frag = Fragment(
self.render_template('brightcove.html', **context)
Expand Down Expand Up @@ -433,7 +458,7 @@ def get_player_html(self, **context):
'static/js/videojs/videojs-transcript.js'
]
context['vjs_plugins'] = map(self.resource_string, vjs_plugins)
log.debug("[get_player_html] initialized scripts: %s", vjs_plugins)
log.debug("Initialized scripts: %s", vjs_plugins)
return super(BrightcovePlayer, self).get_player_html(**context)

def dispatch(self, _request, suffix):
Expand Down
19 changes: 16 additions & 3 deletions video_xblock/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from xblock.exceptions import NoSuchServiceError
from xblock.fields import Scope, Boolean, Float, String

from .constants import DEFAULT_LANG, TPMApiTranscriptFormatID, TPMApiLanguage, TranscriptSource, Status
from .constants import DEFAULT_LANG, TPMApiTranscriptFormatID, TPMApiLanguage, TranscriptSource, Status, PlayerName
from .utils import import_from, ugettext as _, underscore_to_mixedcase, Transcript

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -125,9 +125,17 @@ def route_transcripts(self):
transcripts = self.get_enabled_transcripts()
for tran in transcripts:
if self.threeplaymedia_streaming:
tran['url'] = self.runtime.handler_url(
# download URL remains hidden behind the handler:
tran['download_url'] = self.runtime.handler_url(
self, 'fetch_from_three_play_media', query="{}={}".format(tran['lang_id'], tran['id'])
)
# NOTE(wowkalucky): for some reason handler's URL doesn't work in combination
# Brightcove player/Safari browser. Safari just doesn't populate text tracks with cues!
# So, we have to expose raw 3PM URL for Brightcove users, for now...
if str(self.player_name) != PlayerName.BRIGHTCOVE:
tran['url'] = self.runtime.handler_url(
self, 'fetch_from_three_play_media', query="{}={}".format(tran['lang_id'], tran['id'])
)
elif not tran['url'].endswith('.vtt'):
tran['url'] = self.runtime.handler_url(
self, 'srt_to_vtt', query=tran['url']
Expand Down Expand Up @@ -251,6 +259,7 @@ def get_3pm_transcripts_list(self, file_id, apikey):
domain=domain, file_id=file_id, api_key=apikey
)
)
log.debug(response._content) # pylint: disable=protected-access
except IOError:
log.exception(failure_message)
return feedback, transcripts_list
Expand Down Expand Up @@ -357,7 +366,7 @@ def fetch_from_three_play_media(self, request, _suffix=''):
transcript = self.fetch_single_3pm_translation(transcript_data={'id': transcript_id, 'language_id': lang_id})
if transcript is None:
return Response()
return Response(transcript.content)
return Response(transcript.content, content_type='text/vtt')

@XBlock.handler
def validate_three_play_media_config(self, request, _suffix=''):
Expand Down Expand Up @@ -531,6 +540,10 @@ def save_player_state(self, request, _suffix=''):
if field_name not in player_state:
player_state[field_name] = request[underscore_to_mixedcase(field_name)]

# make sure player's volume is down when muted:
if player_state['muted']:
player_state['volume'] = 0.000

self.player_state = player_state
return {'success': True}

Expand Down
38 changes: 38 additions & 0 deletions video_xblock/static/css/student-view.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,41 @@
background-color: #1aa1de;
color: #fff;
}

/* Transcripts download dropdown buttons */
.dropdown {
display: inline-block;
position: relative;
bottom: 0px;
}

.dropdown-content {
display: none;
position: absolute;
bottom: 0px;
background-color: #f9f9f9;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
width: 180px;
min-width: 180px;
overflow: visible;
background: transparent;
}

.dropdown-content a {
color: black;
text-decoration: none;
display: block;
min-width: 160px;
}

.dropdown-content a:hover {background-color: #1aa1de}

.dropdown:hover .dropdown-content {
display: block;
}

.dropdown:hover {
background-color: #0075b4 !important;
color: #fff !important;
}
31 changes: 21 additions & 10 deletions video_xblock/static/css/studio-edit-accordion.css
Original file line number Diff line number Diff line change
@@ -1,43 +1,54 @@
/* Style the buttons that are used to open and close the accordion panel */
.accordion-btn {
background-color: #eee;
border: none;
/*background-color: #eee;*/
border: 1px solid #009fe6;
color: #444;
cursor: pointer;
padding: 18px;
text-align: left;
transition: 0.4s;
transition: 1s;
width: 100%;
}

.accordion-btn::after {
content: '\002B';
content: 'OFF';
color: #777;
font-weight: bold;
float: right;
margin-left: 5px;
}

.accordion-btn.active::after {
content: "\2212";
content: "ON";
color: #0D6;
}

/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
.accordion-btn.active, .accordion-btn:hover {
.accordion-btn.active {
background-color: #009fe6;
color: #fff;
}

.accordion-btn.active:hover {
background-color: #009fe6;
}

.accordion-btn:hover {
background-color: #0D6;
}

/* Style the accordion panel. Note: hidden by default */
.accordion-panel {
max-height: 0;
background-color: white;
border: 1px solid #009fe6;
display: none;
border: none;
display: block;
overflow: hidden;
padding: 0 18px;
transition: max-height 0.2s ease-out;
transition: max-height 1.5s;
}

.accordion-panel.active {
display: block;
border: 5px solid #009fe6;
max-height: 1080px;
}
19 changes: 19 additions & 0 deletions video_xblock/static/css/studio-edit.css
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,22 @@
color: #fff;
}

.retranscode-button {
box-sizing: border-box;
display: inline-block;
font-size: 1.2rem;
font-weight: 600;
transition: all 0.15s;
text-align: center;
text-decoration: none;
border-radius: 5px;
border: 1px solid #0075b4;
background-color: #fff;
color: #0075b4;
padding: 10px;
}

.retranscode-button:hover {
background-color: #065683;
color: #fff;
}
1 change: 1 addition & 0 deletions video_xblock/static/html/brightcove.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
class="video-js"
brightcove
controls
crossorigin="anonymous"
>
<!-- height="560" it's height of iframe minus height of control bar -->
{{ transcripts }}
Expand Down
22 changes: 15 additions & 7 deletions video_xblock/static/html/student_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@ <h3 class="hd hd-2">{{display_name}}</h3>
</a>
</li>
{% endif %}
{% if download_transcript_allowed and transcripts and not transcripts_streaming_enabled %}
<li class="video-transcript video-download-button-custom
{% if not transcript_download_link %}is-hidden{% endif %}"
id="download-transcript-button">
<a class="download-transcript"
href="{{ transcript_download_link }}">{% trans 'Download transcript' %}
</a>
{% if download_transcript_allowed and transcripts %}
<li class="video-transcript video-download-button-custom" id="download-transcripts-button">
<div class="dropdown">
<a href="{{ handout }}" class="download-transcript dropdown">
{% trans 'Download transcripts' %}
</a>
<div class="dropdown-content">
{% for transcript in transcripts %}
<a href="{{ transcript.download_url }}"
download="{{ transcript.label|lower }}_{{ transcript.lang }}.vtt">
{{ transcript.label }}
</a>
{% endfor %}
</div>
</div>
</li>
{% endif %}
{% if download_video_url %}
Expand Down
Loading

0 comments on commit abb7fae

Please sign in to comment.