add share link at /m/, add share dialog in view menu, fix preview for documents

This commit is contained in:
j 2023-07-15 12:04:04 +05:30
parent 17801df8de
commit bea0d301a4
30 changed files with 2704 additions and 4 deletions

View file

@ -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")

View file

@ -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)

View file

@ -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)

View file

3
pandora/mobile/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
pandora/mobile/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class MobileConfig(AppConfig):
name = 'mobile'

View file

3
pandora/mobile/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
pandora/mobile/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

75
pandora/mobile/views.py Normal file
View 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)

View file

@ -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'

View 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>

View 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>

View file

@ -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

View file

@ -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

View file

@ -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
View 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;
}

View 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
View 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;
}

View 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);
}
})();

View 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%;