add share link at /m/, add share dialog in view menu, fix preview for documents
This commit is contained in:
parent
17801df8de
commit
bea0d301a4
30 changed files with 2704 additions and 4 deletions
|
@ -190,6 +190,12 @@ class MetaClip(object):
|
|||
def __str__(self):
|
||||
return self.public_id
|
||||
|
||||
def get_first_frame(self, resolution=None):
|
||||
if resolution is None:
|
||||
resolution = max(settings.CONFIG['video']['resolutions'])
|
||||
return '/%s/%sp%0.03f.jpg' % (self.item.public_id, resolution, float(self.start))
|
||||
|
||||
|
||||
class Meta:
|
||||
unique_together = ("item", "start", "end")
|
||||
|
||||
|
|
|
@ -567,7 +567,7 @@ class Document(models.Model, FulltextMixin):
|
|||
if len(crop) == 4:
|
||||
path = os.path.join(folder, '%s.jpg' % ','.join(map(str, crop)))
|
||||
if not os.path.exists(path):
|
||||
img = Image.open(src).crop(crop)
|
||||
img = Image.open(src).convert('RGB').crop(crop)
|
||||
img.save(path)
|
||||
else:
|
||||
img = Image.open(path)
|
||||
|
|
|
@ -12,9 +12,10 @@ from oxdjango.decorators import login_required_json
|
|||
from oxdjango.http import HttpFileResponse
|
||||
from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response, HttpErrorJson
|
||||
from django import forms
|
||||
from django.db.models import Count, Sum
|
||||
from django.conf import settings
|
||||
from django.db.models import Count, Sum
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
|
||||
from item import utils
|
||||
from item.models import Item
|
||||
|
@ -512,3 +513,33 @@ def autocompleteDocuments(request, data):
|
|||
response['data']['items'] = [i['value'] for i in qs]
|
||||
return render_to_json_response(response)
|
||||
actions.register(autocompleteDocuments)
|
||||
|
||||
|
||||
def document(request, fragment):
|
||||
context = {}
|
||||
parts = fragment.split('/')
|
||||
id = parts[0]
|
||||
page = None
|
||||
crop = None
|
||||
if len(parts) == 2:
|
||||
rect = parts[1].split(',')
|
||||
if len(rect) == 1:
|
||||
page = rect[0]
|
||||
else:
|
||||
crop = rect
|
||||
document = models.Document.objects.filter(id=ox.fromAZ(id)).first()
|
||||
if document and document.access(request.user):
|
||||
context['title'] = document.data['title']
|
||||
if document.data.get('description'):
|
||||
context['description'] = document.data['description']
|
||||
link = request.build_absolute_uri(document.get_absolute_url())
|
||||
public_id = ox.toAZ(document.id)
|
||||
preview = '/documents/%s/512p.jpg' % public_id
|
||||
if page:
|
||||
preview = '/documents/%s/512p%s.jpg' % (public_id, page)
|
||||
if crop:
|
||||
preview = '/documents/%s/512p%s.jpg' % (public_id, ','.join(crop))
|
||||
context['preview'] = request.build_absolute_uri(preview)
|
||||
context['url'] = request.build_absolute_uri('/documents/' + fragment)
|
||||
context['settings'] = settings
|
||||
return render(request, "document.html", context)
|
||||
|
|
0
pandora/mobile/__init__.py
Normal file
0
pandora/mobile/__init__.py
Normal file
3
pandora/mobile/admin.py
Normal file
3
pandora/mobile/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
5
pandora/mobile/apps.py
Normal file
5
pandora/mobile/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MobileConfig(AppConfig):
|
||||
name = 'mobile'
|
0
pandora/mobile/migrations/__init__.py
Normal file
0
pandora/mobile/migrations/__init__.py
Normal file
3
pandora/mobile/models.py
Normal file
3
pandora/mobile/models.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
3
pandora/mobile/tests.py
Normal file
3
pandora/mobile/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
75
pandora/mobile/views.py
Normal file
75
pandora/mobile/views.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
from django.conf import settings
|
||||
from django.shortcuts import render
|
||||
|
||||
import ox
|
||||
|
||||
|
||||
def index(request, fragment):
|
||||
from item.models import Item
|
||||
from edit.models import Edit
|
||||
from document.models import Document
|
||||
context = {}
|
||||
parts = fragment.split('/')
|
||||
if parts[0] in ('document', 'documents'):
|
||||
type = 'document'
|
||||
id = parts[1]
|
||||
page = None
|
||||
crop = None
|
||||
if len(parts) == 3:
|
||||
rect = parts[2].split(',')
|
||||
if len(rect) == 1:
|
||||
page = rect[0]
|
||||
else:
|
||||
crop = rect
|
||||
document = Document.objects.filter(id=ox.fromAZ(id)).first()
|
||||
if document and document.access(request.user):
|
||||
context['title'] = document.data['title']
|
||||
link = request.build_absolute_uri(document.get_absolute_url())
|
||||
# FIXME: get preview image or fragment parse from url
|
||||
public_id = ox.toAZ(document.id)
|
||||
preview = '/documents/%s/512p.jpg' % public_id
|
||||
if page:
|
||||
preview = '/documents/%s/512p%s.jpg' % (public_id, page)
|
||||
if crop:
|
||||
preview = '/documents/%s/512p%s.jpg' % (public_id, ','.join(crop))
|
||||
context['preview'] = request.build_absolute_uri(preview)
|
||||
|
||||
elif parts[0] == 'edits':
|
||||
type = 'edit'
|
||||
id = parts[1]
|
||||
id = id.split(':')
|
||||
username = id[0]
|
||||
name = ":".join(id[1:])
|
||||
name = name.replace('_', ' ')
|
||||
edit = Edit.objects.filter(user__username=username, name=name).first()
|
||||
if edit and edit.accessible(request.user):
|
||||
link = request.build_absolute_uri('/m' + edit.get_absolute_url())
|
||||
context['title'] = name
|
||||
context['description'] = edit.description.split('\n\n')[0]
|
||||
# FIXME: use sort from parts if needed
|
||||
context['preview'] = request.build_absolute_uri(edit.get_clips().first().get_first_frame())
|
||||
else:
|
||||
type = 'item'
|
||||
id = parts[0]
|
||||
item = Item.objects.filter(public_id=id).first()
|
||||
if item and item.accessible(request.user):
|
||||
link = request.build_absolute_uri(item.get_absolute_url())
|
||||
if len(parts) > 1 and parts[1] in ('editor', 'player'):
|
||||
parts = [parts[0]] + parts[2:]
|
||||
if len(parts) > 1:
|
||||
inout = parts[1]
|
||||
if '-' in inout:
|
||||
inout = inout.split('-')
|
||||
else:
|
||||
inout = inout.split(',')
|
||||
inout = [ox.parse_timecode(p) for p in inout]
|
||||
if len(inout) == 3:
|
||||
inout.pop(1)
|
||||
context['preview'] = link + '/480p%s.jpg' % inout[0]
|
||||
else:
|
||||
context['preview'] = link + '/480p.jpg'
|
||||
context['title'] = item.get('title')
|
||||
if context:
|
||||
context['url'] = request.build_absolute_uri('/m/' + fragment)
|
||||
context['settings'] = settings
|
||||
return render(request, "mobile/index.html", context)
|
|
@ -67,7 +67,8 @@ STATICFILES_DIRS = (
|
|||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
#'django.contrib.staticfiles.finders.DefaultStorageFinder',
|
||||
"sass_processor.finders.CssFinder",
|
||||
"compressor.finders.CompressorFinder",
|
||||
)
|
||||
|
||||
GEOIP_PATH = normpath(join(PROJECT_ROOT, '..', 'data', 'geo'))
|
||||
|
@ -124,6 +125,9 @@ INSTALLED_APPS = (
|
|||
'django_extensions',
|
||||
'django_celery_results',
|
||||
'django_celery_beat',
|
||||
'compressor',
|
||||
'sass_processor',
|
||||
|
||||
'app',
|
||||
'log',
|
||||
'annotation',
|
||||
|
@ -150,6 +154,7 @@ INSTALLED_APPS = (
|
|||
'websocket',
|
||||
'taskqueue',
|
||||
'home',
|
||||
'mobile',
|
||||
)
|
||||
|
||||
AUTH_USER_MODEL = 'system.User'
|
||||
|
|
38
pandora/templates/document.html
Normal file
38
pandora/templates/document.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>{{head_title}}</title>
|
||||
<link rel="alternate" type="application/json+oembed" href="{{base_url}}oembed?url={{current_url|urlencode}}" title="oEmbed Profile"/>
|
||||
{% include "baseheader.html" %}
|
||||
<meta name="title" content="{{title}}"/>
|
||||
{%if description %}<meta name="description" content="{{description}}"/> {%endif%}
|
||||
<meta content="{{ preview }}" name="thumbnail"/>
|
||||
<meta content="{{ preview }}" name="image_src"/>
|
||||
<meta property="og:title" content="{{title}}"/>
|
||||
<meta property="og:url" content="{{url}}"/>
|
||||
<meta property="og:image" content="{{ preview }}"/>
|
||||
<meta property="og:site_name" content="{{settings.SITENAME}}"/>
|
||||
{%if description %}<meta property="og:description" content="{{description}}"/>{%endif%}
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<div style="position: fixed; left: 0; top: 0; right: 0; bottom: 0; background-color: rgb(144, 144, 144);"></div>
|
||||
<div style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; text-align: center; font-family: Lucida Grande, Segoe UI, DejaVu Sans, Lucida Sans Unicode, Helvetica, Arial, sans-serif; font-size: 11px; color: rgb(0, 0, 0); -moz-user-select: none; -o-user-select: none; -webkit-user-select: none">
|
||||
<img src="/{{id}}/{{icon}}128.jpg" alt="" style="padding-top: 64px; -moz-user-drag: none; -o-user-drag: none; -webkit-user-drag: none">
|
||||
<div style="position: absolute; width: 512px; left: 0; right: 0; margin: auto; -moz-user-select: text; -o-user-select: text; -webkit-user-select: text">
|
||||
<div style="padding: 16px 0 8px 0; font-weight: bold; font-size: 13px">{{title}}</div>
|
||||
<div>{{description_html|safe}}</div>
|
||||
{% for c in clips %}
|
||||
<div style="clear:both; padding-top: 4px">
|
||||
<img src="/{{id}}/96p{{c.in}}.jpg" alt="" style="float: left; margin-right: 8px">
|
||||
{{c.annotations|safe}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div style="clear: both; padding-top: 16px; padding-bottom: 16px; font-style: italic">{{settings.SITENAME}} requires JavaScript.</div>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
</body>
|
||||
</html>
|
||||
|
45
pandora/templates/mobile/index.html
Normal file
45
pandora/templates/mobile/index.html
Normal file
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE html>{% load static sass_tags compress %}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% if title %}
|
||||
<title>{{title}}</title>
|
||||
<meta name="title" content="{{title}}"/>
|
||||
<meta property="og:title" content="{{title}}"/>
|
||||
{% endif %}
|
||||
{%if description %}
|
||||
<meta name="description" content="{{description}}"/>
|
||||
<meta property="og:description" content="{{description}}"/>
|
||||
{%endif%}
|
||||
{% if preview %}
|
||||
<meta content="{{ preview }}" name="thumbnail"/>
|
||||
<meta content="{{ preview }}" name="image_src"/>
|
||||
<meta property="og:image" content="{{ preview }}"/>
|
||||
{% endif %}
|
||||
{% if url %}
|
||||
<meta property="og:url" content="{{ url }}"/>
|
||||
{% endif %}
|
||||
<meta property="og:site_name" content="{{ settings.SITENAME }}"/>
|
||||
{% compress css file m %}
|
||||
<link rel="stylesheet" href="{% static 'mobile/css/reset.css' %}"></link>
|
||||
<link rel="stylesheet" href="{% static 'mobile/css/style.css' %}"></link>
|
||||
{% endcompress %}
|
||||
<meta name="google" value="notranslate"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content"></div>
|
||||
{% compress js file m %}
|
||||
<script src="{% static 'mobile/js/utils.js' %}"></script>
|
||||
<script src="{% static 'mobile/js/api.js' %}"></script>
|
||||
<script src="{% static 'mobile/js/icons.js' %}"></script>
|
||||
<script src="{% static 'mobile/js/VideoElement.js' %}"></script>
|
||||
<script src="{% static 'mobile/js/VideoPlayer.js' %}"></script>
|
||||
<script src="{% static 'mobile/js/documents.js' %}"></script>
|
||||
<script src="{% static 'mobile/js/edits.js' %}"></script>
|
||||
<script src="{% static 'mobile/js/item.js' %}"></script>
|
||||
<script src="{% static 'mobile/js/render.js' %}"></script>
|
||||
<script src="{% static 'mobile/js/main.js' %}"></script>
|
||||
{% endcompress %}
|
||||
</body>
|
||||
</html>
|
|
@ -26,6 +26,7 @@ import edit.views
|
|||
import itemlist.views
|
||||
import item.views
|
||||
import item.site
|
||||
import mobile.views
|
||||
import translation.views
|
||||
import urlalias.views
|
||||
|
||||
|
@ -47,6 +48,7 @@ urlpatterns = [
|
|||
re_path(r'^collection/(?P<id>.*?)/icon(?P<size>\d*).jpg$', documentcollection.views.icon),
|
||||
re_path(r'^documents/(?P<id>[A-Z0-9]+)/(?P<size>\d*)p(?P<page>[\d,]*).jpg$', document.views.thumbnail),
|
||||
re_path(r'^documents/(?P<id>[A-Z0-9]+)/(?P<name>.*?\.[^\d]{3})$', document.views.file),
|
||||
re_path(r'^documents/(?P<fragment>.*?)$', document.views.document),
|
||||
re_path(r'^edit/(?P<id>.*?)/icon(?P<size>\d*).jpg$', edit.views.icon),
|
||||
re_path(r'^list/(?P<id>.*?)/icon(?P<size>\d*).jpg$', itemlist.views.icon),
|
||||
re_path(r'^text/(?P<id>.*?)/icon(?P<size>\d*).jpg$', text.views.icon),
|
||||
|
@ -65,6 +67,7 @@ urlpatterns = [
|
|||
re_path(r'^robots.txt$', app.views.robots_txt),
|
||||
re_path(r'^sitemap.xml$', item.views.sitemap_xml),
|
||||
re_path(r'^sitemap(?P<part>\d+).xml$', item.views.sitemap_part_xml),
|
||||
re_path(r'm/(?P<fragment>.*?)$', mobile.views.index),
|
||||
path(r'', item.site.urls),
|
||||
]
|
||||
#sould this not be enabled by default? nginx should handle those
|
||||
|
|
|
@ -5,6 +5,9 @@ celery<5.0,>4.3
|
|||
django-celery-results<2
|
||||
django-celery-beat
|
||||
django-extensions==2.2.9
|
||||
libsass
|
||||
django-compressor
|
||||
django-sass-processor
|
||||
gunicorn==20.0.4
|
||||
html5lib
|
||||
requests<3.0.0,>=2.24.0
|
||||
|
|
|
@ -609,6 +609,8 @@ pandora.ui.mainMenu = function() {
|
|||
pandora.$ui.player.options({fullscreen: true});
|
||||
} else if (data.id == 'embed') {
|
||||
pandora.$ui.embedDialog = pandora.ui.embedDialog().open();
|
||||
} else if (data.id == 'share') {
|
||||
pandora.$ui.shareDialog = pandora.ui.shareDialog().open();
|
||||
} else if (data.id == 'advancedfind') {
|
||||
pandora.$ui.filterDialog = pandora.ui.filterDialog().open();
|
||||
} else if (data.id == 'clearquery') {
|
||||
|
@ -1909,7 +1911,8 @@ pandora.ui.mainMenu = function() {
|
|||
}) }
|
||||
] },
|
||||
{},
|
||||
{ id: 'embed', title: Ox._('Embed...') }
|
||||
{ id: 'embed', title: Ox._('Embed...') },
|
||||
{ id: 'share', title: Ox._('Share...') }
|
||||
]}
|
||||
}
|
||||
|
||||
|
|
37
static/js/shareDialog.js
Normal file
37
static/js/shareDialog.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
'use strict';
|
||||
|
||||
pandora.ui.shareDialog = function(/*[url, ]callback*/) {
|
||||
|
||||
if (arguments.length == 1) {
|
||||
var url, callback = arguments[0];
|
||||
} else {
|
||||
var url = arguments[0], callback = arguments[1];
|
||||
}
|
||||
var url = document.location.href.replace(document.location.hostname, document.location.hostname + '/m'),
|
||||
$content = Ox.Element().append(
|
||||
Ox.Input({
|
||||
height: 100,
|
||||
width: 256,
|
||||
placeholder: 'Share Link',
|
||||
type: 'textarea',
|
||||
disabled: true,
|
||||
value: url
|
||||
})
|
||||
),
|
||||
that = pandora.ui.iconDialog({
|
||||
buttons: [
|
||||
Ox.Button({
|
||||
id: 'close',
|
||||
title: Ox._('Close')
|
||||
}).bindEvent({
|
||||
click: function() {
|
||||
that.close();
|
||||
}
|
||||
}),
|
||||
],
|
||||
keys: {enter: 'close', escape: 'close'},
|
||||
content: $content,
|
||||
title: "Share current view",
|
||||
});
|
||||
return that;
|
||||
}
|
46
static/mobile/css/reset.css
Normal file
46
static/mobile/css/reset.css
Normal file
|
@ -0,0 +1,46 @@
|
|||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
u, i, center,
|
||||
ol,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
171
static/mobile/css/style.css
Normal file
171
static/mobile/css/style.css
Normal file
|
@ -0,0 +1,171 @@
|
|||
body {
|
||||
margin: 0;
|
||||
//background: rgb(240, 240, 240);
|
||||
//background: rgb(144, 144, 144);
|
||||
//color: rgb(0, 0, 0);
|
||||
background: rgb(16, 16, 16);
|
||||
color: rgb(240, 240, 240);
|
||||
font-family: "Noto Sans", "Lucida Grande", "Segoe UI", "DejaVu Sans", "Lucida Sans Unicode", Helvetica, Arial, sans-serif;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(128, 128, 255)
|
||||
}
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.player {
|
||||
max-width: 100%;
|
||||
}
|
||||
video, .poster {
|
||||
border-bottom: 1px solid yellow;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
}
|
||||
.title {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14pt;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid pink;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.byline {
|
||||
padding: 0;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.player {
|
||||
text-align: center;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
@media(max-width:768px) {
|
||||
.title,
|
||||
.byline {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.player {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
.player video {
|
||||
z-index: 0;
|
||||
}
|
||||
.value {
|
||||
padding: 4px;
|
||||
padding-top: 16px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
flex: 1;
|
||||
}
|
||||
@media(max-width:768px) {
|
||||
.value {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
.more {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
text-align: center;
|
||||
font-size: 16pt;
|
||||
}
|
||||
.layer.active {
|
||||
padding-top: 8px;
|
||||
}
|
||||
.layer.active:first-child {
|
||||
padding-top: 0px;
|
||||
}
|
||||
.layer h3 {
|
||||
font-weight: bold;
|
||||
padding: 4px;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
//display: none;
|
||||
}
|
||||
.layer.active h3 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotation {
|
||||
padding: 4px;
|
||||
border-bottom: 1px solid blueviolet;
|
||||
display: none;
|
||||
}
|
||||
.annotation.active {
|
||||
display: block;
|
||||
}
|
||||
.annotation.active.place,
|
||||
.annotation.active.string {
|
||||
display: inline-block;
|
||||
border: 1px solid blueviolet;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@media(max-width:768px) {
|
||||
.annotation a img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.layer h3 {
|
||||
cursor: pointer;
|
||||
}
|
||||
.layer .icon svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
.layer.collapsed .annotation.active {
|
||||
display: none;
|
||||
}
|
||||
.rotate {
|
||||
transform: rotate(90deg) translateY(-100%);
|
||||
transform-origin:bottom left;
|
||||
}
|
||||
|
||||
.document.text {
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.1px;
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
.document.text p {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
.document.text img {
|
||||
max-width: 100vw;
|
||||
margin-left: -4px;
|
||||
margin-right: -4px;
|
||||
}
|
||||
figure {
|
||||
text-align: center;
|
||||
}
|
||||
figure img {
|
||||
max-width: 100vw;
|
||||
margin-left: -4px;
|
||||
margin-right: -4px;
|
||||
}
|
||||
@media(max-width:768px) {
|
||||
.document.text {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
ol li {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.blocked svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
730
static/mobile/js/VideoElement.js
Normal file
730
static/mobile/js/VideoElement.js
Normal file
|
@ -0,0 +1,730 @@
|
|||
'use strict';
|
||||
|
||||
/*@
|
||||
VideoElement <f> VideoElement Object
|
||||
options <o> Options object
|
||||
autoplay <b|false> autoplay
|
||||
items <a|[]> array of objects with src,in,out,duration
|
||||
loop <b|false> loop playback
|
||||
playbackRate <n|1> playback rate
|
||||
position <n|0> start position
|
||||
self <o> Shared private variable
|
||||
([options[, self]]) -> <o:Element> VideoElement Object
|
||||
loadedmetadata <!> loadedmetadata
|
||||
itemchange <!> itemchange
|
||||
seeked <!> seeked
|
||||
seeking <!> seeking
|
||||
sizechange <!> sizechange
|
||||
ended <!> ended
|
||||
@*/
|
||||
|
||||
(function() {
|
||||
var queue = [],
|
||||
queueSize = 100,
|
||||
restrictedElements = [],
|
||||
requiresUserGesture = mediaPlaybackRequiresUserGesture(),
|
||||
unblock = [];
|
||||
|
||||
|
||||
window.VideoElement = function(options) {
|
||||
|
||||
var self = {},
|
||||
that = document.createElement("div");
|
||||
|
||||
self.options = {
|
||||
autoplay: false,
|
||||
items: [],
|
||||
loop: false,
|
||||
muted: false,
|
||||
playbackRate: 1,
|
||||
position: 0,
|
||||
volume: 1
|
||||
}
|
||||
Object.assign(self.options, options);
|
||||
debug(self.options)
|
||||
|
||||
that.style.position = "relative"
|
||||
that.style.width = "100%"
|
||||
that.style.height = "100%"
|
||||
that.style.maxHeight = "100vh"
|
||||
that.style.margin = 'auto'
|
||||
if (self.options.aspectratio) {
|
||||
that.style.aspectRatio = self.options.aspectratio
|
||||
} else {
|
||||
that.style.height = '128px'
|
||||
}
|
||||
that.triggerEvent = function(event, data) {
|
||||
if (event != 'timeupdate') {
|
||||
debug('Video', 'triggerEvent', event, data);
|
||||
}
|
||||
event = new Event(event)
|
||||
event.data = data
|
||||
that.dispatchEvent(event)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
.update({
|
||||
items: function() {
|
||||
self.loadedMetadata = false;
|
||||
loadItems(function() {
|
||||
self.loadedMetadata = true;
|
||||
var update = true;
|
||||
if (self.currentItem >= self.numberOfItems) {
|
||||
self.currentItem = 0;
|
||||
}
|
||||
if (!self.numberOfItems) {
|
||||
self.video.src = '';
|
||||
that.triggerEvent('durationchange', {
|
||||
duration: that.duration()
|
||||
});
|
||||
} else {
|
||||
if (self.currentItemId != self.items[self.currentItem].id) {
|
||||
// check if current item is in new items
|
||||
self.items.some(function(item, i) {
|
||||
if (item.id == self.currentItemId) {
|
||||
self.currentItem = i;
|
||||
loadNextVideo();
|
||||
update = false;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (update) {
|
||||
self.currentItem = 0;
|
||||
self.currentItemId = self.items[self.currentItem].id;
|
||||
}
|
||||
}
|
||||
if (!update) {
|
||||
that.triggerEvent('seeked');
|
||||
that.triggerEvent('durationchange', {
|
||||
duration: that.duration()
|
||||
});
|
||||
} else {
|
||||
setCurrentVideo(function() {
|
||||
that.triggerEvent('seeked');
|
||||
that.triggerEvent('durationchange', {
|
||||
duration: that.duration()
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
playbackRate: function() {
|
||||
self.video.playbackRate = self.options.playbackRate;
|
||||
}
|
||||
})
|
||||
.css({width: '100%', height: '100%'});
|
||||
*/
|
||||
|
||||
debug('Video', 'VIDEO ELEMENT OPTIONS', self.options);
|
||||
|
||||
self.currentItem = -1;
|
||||
self.currentTime = 0;
|
||||
self.currentVideo = 0;
|
||||
self.items = [];
|
||||
self.loadedMetadata = false;
|
||||
that.paused = self.paused = true;
|
||||
self.seeking = false;
|
||||
self.loading = true;
|
||||
self.buffering = true;
|
||||
self.videos = [getVideo(), getVideo()];
|
||||
self.video = self.videos[self.currentVideo];
|
||||
self.video.classList.add("active")
|
||||
self.volume = self.options.volume;
|
||||
self.muted = self.options.muted;
|
||||
self.brightness = document.createElement('div')
|
||||
self.brightness.style.top = '0'
|
||||
self.brightness.style.left = '0'
|
||||
self.brightness.style.width = '100%'
|
||||
self.brightness.style.height = '100%'
|
||||
self.brightness.style.background = 'rgb(0, 0, 0)'
|
||||
self.brightness.style.opacity = '0'
|
||||
self.brightness.style.position = "absolute"
|
||||
that.append(self.brightness)
|
||||
|
||||
self.timeupdate = setInterval(function() {
|
||||
if (!self.paused
|
||||
&& !self.loading
|
||||
&& self.loadedMetadata
|
||||
&& self.items[self.currentItem]
|
||||
&& self.items[self.currentItem].out
|
||||
&& self.video.currentTime >= self.items[self.currentItem].out) {
|
||||
setCurrentItem(self.currentItem + 1);
|
||||
}
|
||||
}, 30);
|
||||
|
||||
// mobile browsers only allow playing media elements after user interaction
|
||||
if (restrictedElements.length > 0) {
|
||||
unblock.push(setSource)
|
||||
setTimeout(function() {
|
||||
that.triggerEvent('requiresusergesture');
|
||||
})
|
||||
} else {
|
||||
setSource();
|
||||
}
|
||||
|
||||
function getCurrentTime() {
|
||||
var item = self.items[self.currentItem];
|
||||
return self.seeking || self.loading
|
||||
? self.currentTime
|
||||
: item ? item.position + self.video.currentTime - item['in'] : 0;
|
||||
}
|
||||
|
||||
function getset(key, value) {
|
||||
var ret;
|
||||
if (isUndefined(value)) {
|
||||
ret = self.video[key];
|
||||
} else {
|
||||
self.video[key] = value;
|
||||
ret = that;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getVideo() {
|
||||
var video = getVideoElement()
|
||||
video.style.display = "none"
|
||||
video.style.width = "100%"
|
||||
video.style.height = "100%"
|
||||
video.style.margin = "auto"
|
||||
video.style.background = '#000'
|
||||
if (self.options.aspectratio) {
|
||||
video.style.aspectRatio = self.options.aspectratio
|
||||
} else {
|
||||
video.style.height = '128px'
|
||||
}
|
||||
video.style.top = 0
|
||||
video.style.left = 0
|
||||
video.style.position = "absolute"
|
||||
video.preload = "metadata"
|
||||
video.addEventListener("ended", event => {
|
||||
if (self.video == video) {
|
||||
setCurrentItem(self.currentItem + 1);
|
||||
}
|
||||
})
|
||||
video.addEventListener("loadedmetadata", event => {
|
||||
//console.log("!!", video.src, "loaded", 'current?', video == self.video)
|
||||
})
|
||||
video.addEventListener("progress", event => {
|
||||
// stop buffering if buffered to end point
|
||||
var item = self.items[self.currentItem],
|
||||
nextItem = mod(self.currentItem + 1, self.numberOfItems),
|
||||
next = self.items[nextItem],
|
||||
nextVideo = self.videos[mod(self.currentVideo + 1, self.videos.length)];
|
||||
if (self.video == video && (video.preload != 'none' || self.buffering)) {
|
||||
if (clipCached(video, item)) {
|
||||
video.preload = 'none';
|
||||
self.buffering = false;
|
||||
if (nextItem != self.currentItem) {
|
||||
nextVideo.preload = 'auto';
|
||||
}
|
||||
} else {
|
||||
if (nextItem != self.currentItem && nextVideo.preload != 'none' && nextVideo.src) {
|
||||
nextVideo.preload = 'none';
|
||||
}
|
||||
}
|
||||
} else if (nextVideo == video && video.preload != 'none' && nextVideo.src) {
|
||||
if (clipCached(video, next)) {
|
||||
video.preload = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function clipCached(video, item) {
|
||||
var cached = false
|
||||
for (var i=0; i<video.buffered.length; i++) {
|
||||
if (video.buffered.start(i) <= item['in']
|
||||
&& video.buffered.end(i) >= item.out) {
|
||||
cached = true
|
||||
}
|
||||
}
|
||||
return cached
|
||||
}
|
||||
})
|
||||
video.addEventListener("volumechange", event => {
|
||||
if (self.video == video) {
|
||||
that.triggerEvent('volumechange')
|
||||
}
|
||||
})
|
||||
video.addEventListener("play", event => {
|
||||
/*
|
||||
if (self.video == video) {
|
||||
that.triggerEvent('play')
|
||||
}
|
||||
*/
|
||||
})
|
||||
video.addEventListener("pause", event => {
|
||||
/*
|
||||
if (self.video == video) {
|
||||
that.triggerEvent('pause')
|
||||
}
|
||||
*/
|
||||
})
|
||||
video.addEventListener("timeupdate", event => {
|
||||
if (self.video == video) {
|
||||
/*
|
||||
var box = self.video.getBoundingClientRect()
|
||||
if (box.width && box.height) {
|
||||
that.style.width = box.width + 'px'
|
||||
that.style.height = box.height + 'px'
|
||||
}
|
||||
*/
|
||||
that.triggerEvent('timeupdate', {
|
||||
currentTime: getCurrentTime()
|
||||
})
|
||||
}
|
||||
})
|
||||
video.addEventListener("seeking", event => {
|
||||
if (self.video == video) {
|
||||
that.triggerEvent('seeking')
|
||||
}
|
||||
})
|
||||
video.addEventListener("stop", event => {
|
||||
if (self.video == video) {
|
||||
//self.video.pause();
|
||||
that.triggerEvent('ended');
|
||||
}
|
||||
})
|
||||
that.append(video)
|
||||
return video
|
||||
}
|
||||
|
||||
function getVideoElement() {
|
||||
var video;
|
||||
if (requiresUserGesture) {
|
||||
if (queue.length) {
|
||||
video = queue.pop();
|
||||
} else {
|
||||
video = document.createElement('video');
|
||||
restrictedElements.push(video);
|
||||
}
|
||||
} else {
|
||||
video = document.createElement('video');
|
||||
}
|
||||
video.playsinline = true
|
||||
video.setAttribute('playsinline', 'playsinline')
|
||||
video.setAttribute('webkit-playsinline', 'webkit-playsinline')
|
||||
video.WebKitPlaysInline = true
|
||||
return video
|
||||
};
|
||||
|
||||
function getVolume() {
|
||||
var volume = 1;
|
||||
if (self.items[self.currentItem] && isNumber(self.items[self.currentItem].volume)) {
|
||||
volume = self.items[self.currentItem].volume;
|
||||
}
|
||||
return self.volume * volume;
|
||||
}
|
||||
|
||||
|
||||
function isReady(video, callback) {
|
||||
if (video.seeking && !self.paused && !self.seeking) {
|
||||
that.triggerEvent('seeking');
|
||||
debug('Video', 'isReady', 'seeking');
|
||||
video.addEventListener('seeked', function(event) {
|
||||
debug('Video', 'isReady', 'seeked');
|
||||
that.triggerEvent('seeked');
|
||||
callback(video);
|
||||
}, {once: true})
|
||||
} else if (video.readyState) {
|
||||
callback(video);
|
||||
} else {
|
||||
that.triggerEvent('seeking');
|
||||
video.addEventListener('loadedmetadata', function(event) {
|
||||
callback(video);
|
||||
}, {once: true});
|
||||
video.addEventListener('seeked', event => {
|
||||
that.triggerEvent('seeked');
|
||||
}, {once: true})
|
||||
}
|
||||
}
|
||||
|
||||
function loadItems(callback) {
|
||||
debug('loadItems')
|
||||
var currentTime = 0,
|
||||
items = self.options.items.map(function(item) {
|
||||
return isObject(item) ? {...item} : {src: item};
|
||||
});
|
||||
|
||||
self.items = items;
|
||||
self.numberOfItems = self.items.length;
|
||||
items.forEach(item => {
|
||||
item['in'] = item['in'] || 0;
|
||||
item.position = currentTime;
|
||||
if (item.out) {
|
||||
item.duration = item.out - item['in'];
|
||||
}
|
||||
if (item.duration) {
|
||||
if (!item.out) {
|
||||
item.out = item.duration;
|
||||
}
|
||||
currentTime += item.duration;
|
||||
item.id = getId(item);
|
||||
} else {
|
||||
getVideoInfo(item.src, function(info) {
|
||||
item.duration = info.duration;
|
||||
if (!item.out) {
|
||||
item.out = item.duration;
|
||||
}
|
||||
currentTime += item.duration;
|
||||
item.id = getId(item);
|
||||
});
|
||||
}
|
||||
})
|
||||
debug('loadItems done')
|
||||
callback && callback();
|
||||
|
||||
function getId(item) {
|
||||
return item.id || item.src + '/' + item['in'] + '-' + item.out;
|
||||
}
|
||||
}
|
||||
|
||||
function loadNextVideo() {
|
||||
if (self.numberOfItems <= 1) {
|
||||
return;
|
||||
}
|
||||
var item = self.items[self.currentItem],
|
||||
nextItem = mod(self.currentItem + 1, self.numberOfItems),
|
||||
next = self.items[nextItem],
|
||||
nextVideo = self.videos[mod(self.currentVideo + 1, self.videos.length)];
|
||||
nextVideo.addEventListener('loadedmetadata', function() {
|
||||
if (self.video != nextVideo) {
|
||||
nextVideo.currentTime = next['in'] || 0;
|
||||
}
|
||||
}, {once: true});
|
||||
nextVideo.src = next.src;
|
||||
nextVideo.preload = 'metadata';
|
||||
}
|
||||
|
||||
function setCurrentItem(item) {
|
||||
debug('Video', 'sCI', item, self.numberOfItems);
|
||||
var interval;
|
||||
if (item >= self.numberOfItems || item < 0) {
|
||||
if (self.options.loop) {
|
||||
item = mod(item, self.numberOfItems);
|
||||
} else {
|
||||
self.seeking = false;
|
||||
self.ended = true;
|
||||
that.paused = self.paused = true;
|
||||
self.video && self.video.pause();
|
||||
that.triggerEvent('ended');
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.video && self.video.pause();
|
||||
self.currentItem = item;
|
||||
self.currentItemId = self.items[self.currentItem].id;
|
||||
setCurrentVideo(function() {
|
||||
if (!self.loadedMetadata) {
|
||||
self.loadedMetadata = true;
|
||||
that.triggerEvent('loadedmetadata');
|
||||
}
|
||||
debug('Video', 'sCI', 'trigger itemchange',
|
||||
self.items[self.currentItem]['in'], self.video.currentTime, self.video.seeking);
|
||||
that.triggerEvent('sizechange');
|
||||
that.triggerEvent('itemchange', {
|
||||
item: self.currentItem
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setCurrentVideo(callback) {
|
||||
var css = {},
|
||||
muted = self.muted,
|
||||
item = self.items[self.currentItem],
|
||||
next;
|
||||
debug('Video', 'sCV', JSON.stringify(item));
|
||||
|
||||
['left', 'top', 'width', 'height'].forEach(function(key) {
|
||||
css[key] = self.videos[self.currentVideo].style[key];
|
||||
});
|
||||
self.currentTime = item.position;
|
||||
self.loading = true;
|
||||
if (self.video) {
|
||||
self.videos[self.currentVideo].style.display = "none"
|
||||
self.videos[self.currentVideo].classList.remove("active")
|
||||
self.video.pause();
|
||||
}
|
||||
self.currentVideo = mod(self.currentVideo + 1, self.videos.length);
|
||||
self.video = self.videos[self.currentVideo];
|
||||
self.video.classList.add("active")
|
||||
self.video.muted = true; // avoid sound glitch during load
|
||||
if (!self.video.attributes.src || self.video.attributes.src.value != item.src) {
|
||||
self.loadedMetadata && debug('Video', 'caching next item failed, reset src');
|
||||
self.video.src = item.src;
|
||||
}
|
||||
self.video.preload = 'metadata';
|
||||
self.video.volume = getVolume();
|
||||
self.video.playbackRate = self.options.playbackRate;
|
||||
Object.keys(css).forEach(key => {
|
||||
self.video.style[key] = css[key]
|
||||
})
|
||||
self.buffering = true;
|
||||
debug('Video', 'sCV', self.video.src, item['in'],
|
||||
self.video.currentTime, self.video.seeking);
|
||||
isReady(self.video, function(video) {
|
||||
var in_ = item['in'] || 0;
|
||||
|
||||
function ready() {
|
||||
debug('Video', 'sCV', 'ready');
|
||||
self.seeking = false;
|
||||
self.loading = false;
|
||||
self.video.muted = muted;
|
||||
!self.paused && self.video.play();
|
||||
self.video.style.display = 'block'
|
||||
callback && callback();
|
||||
loadNextVideo();
|
||||
}
|
||||
if (video.currentTime == in_) {
|
||||
debug('Video', 'sCV', 'already at position', item.id, in_, video.currentTime);
|
||||
ready();
|
||||
} else {
|
||||
self.video.addEventListener("seeked", event => {
|
||||
debug('Video', 'sCV', 'seeked callback');
|
||||
ready();
|
||||
}, {once: true})
|
||||
if (!self.seeking) {
|
||||
debug('Video', 'sCV set in', video.src, in_, video.currentTime, video.seeking);
|
||||
self.seeking = true;
|
||||
video.currentTime = in_;
|
||||
if (self.paused) {
|
||||
var promise = self.video.play();
|
||||
if (promise !== undefined) {
|
||||
promise.then(function() {
|
||||
self.video.pause();
|
||||
self.video.muted = muted;
|
||||
}).catch(function() {
|
||||
self.video.pause();
|
||||
self.video.muted = muted;
|
||||
});
|
||||
} else {
|
||||
self.video.pause();
|
||||
self.video.muted = muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setCurrentItemTime(currentTime) {
|
||||
debug('Video', 'sCIT', currentTime, self.video.currentTime,
|
||||
'delta', currentTime - self.video.currentTime);
|
||||
isReady(self.video, function(video) {
|
||||
if (self.video == video) {
|
||||
if(self.video.seeking) {
|
||||
self.video.addEventListener("seeked", event => {
|
||||
that.triggerEvent('seeked');
|
||||
self.seeking = false;
|
||||
}, {once: true})
|
||||
} else if (self.seeking) {
|
||||
that.triggerEvent('seeked');
|
||||
self.seeking = false;
|
||||
}
|
||||
video.currentTime = currentTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setCurrentTime(time) {
|
||||
debug('Video', 'sCT', time);
|
||||
var currentTime, currentItem;
|
||||
self.items.forEach(function(item, i) {
|
||||
if (time >= item.position
|
||||
&& time < item.position + item.duration) {
|
||||
currentItem = i;
|
||||
currentTime = time - item.position + item['in'];
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (self.items.length) {
|
||||
// Set to end of items if time > duration
|
||||
if (isUndefined(currentItem) && isUndefined(currentTime)) {
|
||||
currentItem = self.items.length - 1;
|
||||
currentTime = self.items[currentItem].duration + self.items[currentItem]['in'];
|
||||
}
|
||||
debug('Video', 'sCT', time, '=>', currentItem, currentTime);
|
||||
if (currentItem != self.currentItem) {
|
||||
setCurrentItem(currentItem);
|
||||
}
|
||||
self.seeking = true;
|
||||
self.currentTime = time;
|
||||
that.triggerEvent('seeking');
|
||||
setCurrentItemTime(currentTime);
|
||||
} else {
|
||||
self.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function setSource() {
|
||||
self.loadedMetadata = false;
|
||||
loadItems(function() {
|
||||
setCurrentTime(self.options.position);
|
||||
self.options.autoplay && setTimeout(function() {
|
||||
that.play();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*@
|
||||
brightness <f> get/set brightness
|
||||
@*/
|
||||
that.brightness = function() {
|
||||
var ret;
|
||||
if (arguments.length == 0) {
|
||||
ret = 1 - parseFloat(self.brightness.style.opacity);
|
||||
} else {
|
||||
self.brightness.style.opacity = 1 - arguments[0]
|
||||
ret = that;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
/*@
|
||||
buffered <f> buffered
|
||||
@*/
|
||||
that.buffered = function() {
|
||||
return self.video.buffered;
|
||||
};
|
||||
|
||||
/*@
|
||||
currentTime <f> get/set currentTime
|
||||
@*/
|
||||
that.currentTime = function() {
|
||||
var ret;
|
||||
if (arguments.length == 0) {
|
||||
ret = getCurrentTime();
|
||||
} else {
|
||||
self.ended = false;
|
||||
setCurrentTime(arguments[0]);
|
||||
ret = that;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
/*@
|
||||
duration <f> duration
|
||||
@*/
|
||||
that.duration = function() {
|
||||
return self.items ? self.items.reduce((duration, item) => {
|
||||
return duration + item.duration;
|
||||
}, 0) : NaN;
|
||||
};
|
||||
|
||||
/*@
|
||||
muted <f> get/set muted
|
||||
@*/
|
||||
that.muted = function(value) {
|
||||
if (!isUndefined(value)) {
|
||||
self.muted = value;
|
||||
}
|
||||
return getset('muted', value);
|
||||
};
|
||||
|
||||
/*@
|
||||
pause <f> pause
|
||||
@*/
|
||||
that.pause = function() {
|
||||
that.paused = self.paused = true;
|
||||
self.video.pause();
|
||||
that.paused && that.triggerEvent('pause')
|
||||
};
|
||||
|
||||
/*@
|
||||
play <f> play
|
||||
@*/
|
||||
that.play = function() {
|
||||
if (self.ended) {
|
||||
that.currentTime(0);
|
||||
}
|
||||
isReady(self.video, function(video) {
|
||||
self.ended = false;
|
||||
that.paused = self.paused = false;
|
||||
self.seeking = false;
|
||||
video.play();
|
||||
that.triggerEvent('play')
|
||||
});
|
||||
};
|
||||
|
||||
that.removeElement = function() {
|
||||
self.currentTime = getCurrentTime();
|
||||
self.loading = true;
|
||||
clearInterval(self.timeupdate);
|
||||
//Chrome does not properly release resources, reset manually
|
||||
//http://code.google.com/p/chromium/issues/detail?id=31014
|
||||
self.videos.forEach(function(video) {
|
||||
video.src = ''
|
||||
});
|
||||
return Ox.Element.prototype.removeElement.apply(that, arguments);
|
||||
};
|
||||
|
||||
/*@
|
||||
videoHeight <f> get videoHeight
|
||||
@*/
|
||||
that.videoHeight = function() {
|
||||
return self.video.videoHeight;
|
||||
};
|
||||
|
||||
/*@
|
||||
videoWidth <f> get videoWidth
|
||||
@*/
|
||||
that.videoWidth = function() {
|
||||
return self.video.videoWidth;
|
||||
};
|
||||
|
||||
/*@
|
||||
volume <f> get/set volume
|
||||
@*/
|
||||
that.volume = function(value) {
|
||||
if (isUndefined(value)) {
|
||||
value = self.volume
|
||||
} else {
|
||||
self.volume = value;
|
||||
self.video.volume = getVolume();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return that;
|
||||
|
||||
};
|
||||
|
||||
// mobile browsers only allow playing media elements after user interaction
|
||||
|
||||
function mediaPlaybackRequiresUserGesture() {
|
||||
// test if play() is ignored when not called from an input event handler
|
||||
var video = document.createElement('video');
|
||||
video.muted = true
|
||||
video.play();
|
||||
return video.paused;
|
||||
}
|
||||
|
||||
|
||||
async function removeBehaviorsRestrictions() {
|
||||
debug('Video', 'remove restrictions on video', self.video, restrictedElements.length, queue.length);
|
||||
if (restrictedElements.length > 0) {
|
||||
var rElements = restrictedElements;
|
||||
restrictedElements = [];
|
||||
rElements.forEach(async function(video) {
|
||||
await video.load();
|
||||
});
|
||||
setTimeout(function() {
|
||||
var u = unblock;
|
||||
unblock = [];
|
||||
u.forEach(function(callback) { callback(); });
|
||||
}, 1000);
|
||||
}
|
||||
while (queue.length < queueSize) {
|
||||
var video = document.createElement('video');
|
||||
video.load();
|
||||
queue.push(video);
|
||||
}
|
||||
}
|
||||
|
||||
if (requiresUserGesture) {
|
||||
window.addEventListener('keydown', removeBehaviorsRestrictions);
|
||||
window.addEventListener('mousedown', removeBehaviorsRestrictions);
|
||||
window.addEventListener('touchstart', removeBehaviorsRestrictions);
|
||||
}
|
||||
})();
|
400
static/mobile/js/VideoPlayer.js
Normal file
400
static/mobile/js/VideoPlayer.js
Normal file
|
@ -0,0 +1,400 @@
|
|||
(function() {
|
||||
|
||||
window.VideoPlayer = function(options) {
|
||||
|
||||
var self = {}, that;
|
||||
self.options = {
|
||||
autoplay: false,
|
||||
controls: true,
|
||||
items: [],
|
||||
loop: false,
|
||||
muted: false,
|
||||
playbackRate: 1,
|
||||
position: 0,
|
||||
volume: 1
|
||||
}
|
||||
Object.assign(self.options, options);
|
||||
that = VideoElement(options);
|
||||
|
||||
self.controls = document.createElement('div')
|
||||
self.controls.classList.add('mx-controls')
|
||||
//self.controls.style.display = "none"
|
||||
if (self.options.controls) {
|
||||
var ratio = `aspect-ratio: ${self.options.aspectratio};`
|
||||
if (!self.options.aspectratio) {
|
||||
ratio = 'height: 128px;'
|
||||
}
|
||||
self.controls.innerHTML = `
|
||||
<style>
|
||||
.fullscreen video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||