diff --git a/app/settings.py b/app/settings.py index 9a45592..c55fcd8 100755 --- a/app/settings.py +++ b/app/settings.py @@ -146,6 +146,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' GEOIP_PATH = BASE_DIR / 'geo' DEFAULT_PANDORA_API = "https://archive.njp.ma/api/" +TIMELINE_QUERY = {'conditions': []} URL_PREFIX = '' diff --git a/app/static/css/partials/_film.scss b/app/static/css/partials/_film.scss index a0807e4..acdd2e9 100755 --- a/app/static/css/partials/_film.scss +++ b/app/static/css/partials/_film.scss @@ -81,12 +81,12 @@ main > .film { } } - .videos { + .items { display: flex; flex-wrap: wrap; gap: 16px; justify-content: center; - .video { + .item { width: calc(100% / 4 - 16px); a { color: #000; @@ -102,7 +102,7 @@ main > .film { } } @media screen and (max-width: 799px) { - .video { + .item { width: 100%; } } @@ -147,7 +147,7 @@ main > .film { } main > .film { - .info-meta, .bio-block, .play, .summary-block, .texts { + .info-meta, .bio-block, .play, .summary-block, .texts, .items { background: rgba(0, 0, 0, 0.55); border-radius: 2px; box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.6); diff --git a/app/static/js/ascroll.js b/app/static/js/ascroll.js index f2e35cc..623c0cc 100755 --- a/app/static/js/ascroll.js +++ b/app/static/js/ascroll.js @@ -1,11 +1,10 @@ var cache = cache || {} var layer = 'descriptions' -var baseURL = 'https://pad.ma' var imageResolution = 480 var videoExtension async function pandoraAPI(action, data) { - var url = baseURL + '/api/' + var url = pandoraURL + '/api/' //var url = '/pandoraAPI/' var key = JSON.stringify([action, data]) if (!cache[key]) { @@ -233,13 +232,13 @@ function renderAnnotation(config, video, ascroll, annotation) { var director = annotation.director[0] } var txt = `${title} (${director})` - figcaption = `
${txt}
` + figcaption = `
${txt}
` } - annotation.value = annotation.value.replace(/src="\//g, `src="${baseURL}/`).replace(/href="\//g, `href="${baseURL}/`) + annotation.value = annotation.value.replace(/src="\//g, `src="${pandoraURL}/`).replace(/href="\//g, `href="${pandoraURL}/`) div.innerHTML = `
- + ${figcaption}
@@ -250,7 +249,7 @@ function renderAnnotation(config, video, ascroll, annotation) { document.addEventListener('scroll', onVisibilityChange(div.querySelector('.frame'), function(visible) { var src if (config.edit) { - src = `${baseURL}/${annotation.id.split('/')[0]}/480p.webm` + src = `${pandoraURL}/${annotation.id.split('/')[0]}/480p.webm` } if (config.loaded && visible) { updatePlayer(video, frame, annotation['in'], annotation['out'], src) @@ -270,7 +269,7 @@ function renderAnnotations(config) { video.WebKitPlaysInline = true video.muted = true if (config.item) { - setVideoSrc(video, `${baseURL}/${config.item}/480p.webm`) + setVideoSrc(video, `${pandoraURL}/${config.item}/480p.webm`) } video.addEventListener('timeupdate', timeupdate) video.addEventListener('touchstart', showOverlay) @@ -290,7 +289,7 @@ function renderAnnotations(config) { if (frame) { var src if (config.edit) { - src = `${baseURL}/${config.first.id.split('/')[0]}/480p.webm` + src = `${pandoraURL}/${config.first.id.split('/')[0]}/480p.webm` } updatePlayer(video, frame, config.first['in'], config.first['out'], src) } diff --git a/app/static/js/play.js b/app/static/js/play.js new file mode 100644 index 0000000..1cbd416 --- /dev/null +++ b/app/static/js/play.js @@ -0,0 +1,439 @@ +var cache = cache || {} +var layer = 'descriptions' +var imageResolution = 480 +var videoExtension + +async function pandoraAPI(action, data) { + var url = pandoraURL + '/api/' + //var url = '/pandoraAPI/' + var key = JSON.stringify([action, data]) + if (!cache[key]) { + var response = await fetch(url, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + action: action, + data: data + }) + }) + cache[key] = await response.json() + } + return cache[key] +} + +function setVideoSrc(video, src) { + var ext + if (!videoExtension) { + [ + ['video/mp4; codecs="avc1.42E01E, mp4a.40.2"', '.mp4'], + ['video/webm; codecs="vp8, vorbis"', '.webm'], + ].forEach(opt => { + if (videoExtension) { return } + if (video.canPlayType(opt[0]).replace('no', '')) { + videoExtension = opt[1] + } + }) + } + src = src.replace('.webm', videoExtension) + if (src != video.src) { + video.src = src + } +} + +function resize() { + var video = document.querySelector('video') + if (video && video._frame) { + var rect = video._frame.getBoundingClientRect(); + video.style.top = (rect.top + window.scrollY) + 'px' + } +} + +function updatePlayer(video, frame, currentTime, out, src) { + var rect = frame.getBoundingClientRect(); + video.style.opacity = 0 + video.style.top = (rect.top + window.scrollY) + 'px' + video.style.display = 'block'; + if (src) { + setVideoSrc(video, src) + } + //video.poster = frame.querySelector('img').src + var muted = video.muted + // video.muted = true + video.currentTime = currentTime + video.dataset.in = currentTime + video.dataset.out = out + video._frame = frame + const show = event => { + video.style.opacity = 1 + video.muted = muted + video.removeEventListener('seeked', show) + } + video.addEventListener('seeked', show) + video.play() +} + +function isElementInViewport (el) { + var rect = el.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */ + Math.floor(rect.right) <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */ + ); +} + +function onVisibilityChange(el, callback) { + var old_visible; + return function () { + var visible = isElementInViewport(el); + if (visible != old_visible) { + old_visible = visible; + if (visible) { + el.classList.add('visible') + } else { + el.classList.remove('visible') + } + if (typeof callback == 'function') { + callback(visible); + } + } + } +} + +function scrollTo(element) { + var delta = element.offsetTop - document.scrollingElement.scrollTop, + duration = 1000, t = 40, n = duration / t, + step = delta / n; + + function scroll() { + if (document.scrollingElement.scrollTop + step > element.offsetTop) { + document.scrollingElement.scrollTop = element.offsetTop + n = 0 + } else { + document.scrollingElement.scrollTop += step + } + n-- + if (n) setTimeout(scroll, t) + } + scroll() +} + +function timeupdate(event) { + if (event.target.dataset.out && event.target.dataset.in) { + var in_= parseFloat(event.target.dataset.in) + var out_= parseFloat(event.target.dataset.out) + if (event.target.currentTime >= out_) { + /* + var next + if (event.target._frame) { + next = event.target._frame.parentElement.nextSibling + if (next) { + next = next.querySelector('.frame') + } + } + if (next) { + scrollTo(next) + } else { + event.target.pause() + } + */ + if (config.pause) { + event.target.pause() + } else { + event.target.currentTime = in_ + } + } + } +} + +function formatInfo(config, ascroll) { + var info = document.createElement('div') + var h1 = document.createElement('h1') + h1.innerHTML = config.title + info.appendChild(h1) + var h2 = document.createElement('h2') + h2.innerHTML = config.byline + info.appendChild(h2) + var div = document.createElement('div') + div.classList.add('intro') + div.innerHTML = config.body + info.appendChild(div) + ascroll.appendChild(info) + return info +} + +function showOverlay(event) { + event.stopPropagation() + event.preventDefault() + document.querySelectorAll('#video-overlay').forEach(element => element.remove()) + var video = event.target + var rect = video.getBoundingClientRect(); + var overlay = document.createElement('div') + overlay.id = 'video-overlay' + overlay.style.top = video.style.top + overlay.style.width = rect.width + 'px' + overlay.style.height = rect.height + 'px' + overlay.style.position = 'absolute' + overlay.style.display = 'flex' + overlay.style.alignItems = 'center' + overlay.style.justifyContent = 'center' + //overlay.style.fontSize = '45px' + video.controls = false + + var off = `` + var on = `` + + if (video.muted) { + overlay.innerHTML = off + } else { + overlay.innerHTML = on + } + overlay.addEventListener('click', event=> { + video.muted = !video.muted + if (video.muted) { + overlay.innerHTML = off + } else { + overlay.innerHTML = on + } + }) + var timeout = setTimeout(() => { + video.controls = false + overlay.remove() + }, 3000) + overlay.addEventListener('mousemove', event=> { + clearTimeout(timeout) + timeout = setTimeout(() => { + video.controls = false + overlay.remove() + }, 500) + }) + video.parentElement.appendChild(overlay) + +} + +function renderAnnotation(config, video, ascroll, annotation) { + var div = document.createElement('div') + div.classList.add('annotation') + + var color1 = `hsl(${annotation.color1.hue}, 70%, 75%)` + var color2 = `hsl(${annotation.color2.hue}, 70%, 75%)` + if (!config.first) { + config.first = annotation + config.info.style.background = color1 + } + div.style.background = `linear-gradient(to bottom, ${color1}, ${color2})`; + var figcaption = '' + if (annotation.title) { + if (config.language == 'zh') { + var title = annotation.title.split('/')[1] || annotation.title + var director = annotation.director[1] + } else { + var title = annotation.title.split('/')[0] + var director = annotation.director[0] + } + var txt = `${title} (${director})` + figcaption = `
${txt}
` + } + annotation.value = annotation.value.replace(/src="\//g, `src="${pandoraURL}/`).replace(/href="\//g, `href="${pandoraURL}/`) + div.innerHTML = ` +
+
+ + ${figcaption} +
+
+
${annotation.value}
+ ` + ascroll.appendChild(div) + var frame = div.querySelector('.frame') + document.addEventListener('scroll', onVisibilityChange(div.querySelector('.frame'), function(visible) { + var src + if (config.edit) { + src = `${pandoraURL}/${annotation.id.split('/')[0]}/480p.webm` + } + if (config.loaded && visible) { + updatePlayer(video, frame, annotation['in'], annotation['out'], src) + } + + })) +} + +function renderAnnotations(config) { + var ascroll = document.querySelector('#ascroll') + config.loaded = false + var video = document.createElement('video') + video.classList.add('player') + video.playsinline = true + video.setAttribute('playsinline', 'playsinline') + video.setAttribute('webkit-playsinline', 'webkit-playsinline') + video.WebKitPlaysInline = true + video.muted = true + if (config.item) { + setVideoSrc(video, `${pandoraURL}/${config.item}/480p.webm`) + } + video.addEventListener('timeupdate', timeupdate) + video.addEventListener('touchstart', showOverlay) + video.addEventListener('mouseover', showOverlay) + var box = document.createElement('div') + box.classList.add('vbox') + box.appendChild(video) + ascroll.appendChild(box) + + config.info = formatInfo(config, ascroll) + config.annotations.forEach(annotation => { + renderAnnotation(config, video, ascroll, annotation) + }) + config.loaded = true + if (config.first) { + let frame = ascroll.querySelector('.annotation .frame') + if (frame) { + var src + if (config.edit) { + src = `${pandoraURL}/${config.first.id.split('/')[0]}/480p.webm` + } + updatePlayer(video, frame, config.first['in'], config.first['out'], src) + } + } + if (config.item_url || config.cited) { + var box = document.createElement('div') + var color = config.annotations[config.annotations.length - 1].color2 + box.style.background = `hsl(${color.hue}, 70%, 75%)` + var div = document.createElement('div') + div.classList.add('related-film') + if (config.item_url) { + if (config.language == 'zh') { + div.innerHTML = ` + 引用影片: ${config.item_title_zh} (${config.item_director[1]}) + ` + } else { + div.innerHTML = ` + Film cited: ${config.item_title} (${config.item_director[0]})
+ ` + } + } else { + var html = [] + config.cited.forEach(film => { + var title_en = film.title.split(' / ')[0] + var title_zh = film.title.split(' / ')[1] || title_en + if (config.language == 'en') { + var director = film.director ? ` (${film.director[0]})` : '' + html.push(`${title_en}${director}`) + } else { + var director = film.director ? ` (${film.director[1]})` : '' + html.push(`${title_zh}${director}`) + } + }) + var films = html.length == 1 ? 'Film' : 'Films' + html= html.join(', ') + if (config.language == 'zh') { + div.innerHTML = ` 引用影片: ${html}` + } else { + div.innerHTML = `${films} cited: ${html}` + } + } + box.appendChild(div) + ascroll.appendChild(box) + } +} + +async function loadClips(annotations) { + var items = annotations.map(annotation => annotation.id.split('/')[0]) + items = [...new Set(items)] + return pandoraAPI('findClips', {itemsQuery: { + conditions: [{key: 'id', operator: '&', value: items}] + }, range: [0, 10000], keys: [ + 'id', 'hue', 'saturation', 'lightness' + ]}).then(response => { + var colors = {} + response.data.items.forEach(clip => { + colors[clip.id] = clip + }) + var previous + annotations.forEach(annotation => { + var clipId = annotation.id.split('/')[0] + '/' + annotation.in.toFixed(3) + '-'+ annotation.out.toFixed(3) + annotation.color1 = colors[clipId] + if(previous) { + previous.color2 = annotation.color1 + } + previous = annotation + }) + if (annotations.length) { + annotations[annotations.length - 1].color2 = annotations[0].color1 + } + return annotations + }) +} + +function loadAnnotations(config) { + if (config.item) { + pandoraAPI('get', {id: config.item, keys: [ + 'layers' + ]}).then(response => { + var annotations = response.data.layers[config.layer].filter(annotation => { + return !(config.user && annotation.user != config.user) + }) + loadClips(annotations).then(annotations => { + config.annotations = annotations.filter(annotation => { + if (config.only_e) { + if (annotation.value.slice(0, 2) == 'E:') { + annotation.value = annotation.value.slice(2).trim() + return true + } else { + return false + } + } else { + return annotation.value.slice(0, 2) != 'E:' + } + }) + renderAnnotations(config) + }) + }) + } else { + var cited = {} + pandoraAPI('getEdit', {id: config.edit, keys: []}).then(response => { + var annotations = [] + response.data.clips.forEach(clip => { + cited[clip.item] = { + title: clip.title, + director: clip.director, + id: clip.item + } + clip.layers[config.layer].forEach(annotation => { + if (config.user && annotation.user != config.user) { + return + } + ;['title', 'director', 'date'].forEach(key => { + annotation[key] = clip[key] + }) + if (config.only_e) { + if (annotation.value.slice(0, 2) == 'E:') { + annotation.value = annotation.value.slice(2).trim() + annotations.push(annotation) + } + } else { + if (annotation.value.slice(0, 2) != 'E:') { + annotations.push(annotation) + } + } + }) + }) + loadClips(annotations).then(annotations => { + config.annotations = annotations + config.cited = Object.values(cited) + renderAnnotations(config) + }) + }) + } +} + +config.layer = config.layer || layer + +if (config.annotations) { + renderAnnotations(config) +} else { + loadAnnotations(config) +} + + +window.addEventListener('resize', resize, false); + diff --git a/app/templates/film.html b/app/templates/film.html index 56f3b29..a113307 100644 --- a/app/templates/film.html +++ b/app/templates/film.html @@ -43,7 +43,7 @@ body { -
+
-
+
{% for item in film.data.items %} -
- +
+
@@ -99,6 +99,9 @@ body { + {% comment %} + {% endcomment %} {% endblock %} diff --git a/app/templates/film_play.html b/app/templates/film_play.html index 9072c23..e4ff5d0 100644 --- a/app/templates/film_play.html +++ b/app/templates/film_play.html @@ -4,9 +4,8 @@ {% endblock %} {% block end %} - + {% endblock %} diff --git a/app/templates/text.html b/app/templates/text.html index ccf2f3d..3a409a7 100644 --- a/app/templates/text.html +++ b/app/templates/text.html @@ -37,6 +37,7 @@ {% block end %} {% endblock %} diff --git a/app/text/views.py b/app/text/views.py index 44688e4..7a1616d 100644 --- a/app/text/views.py +++ b/app/text/views.py @@ -48,4 +48,5 @@ def text(request, slug): else: context['text'] = get_object_or_404(models.Text, slug=slug, public=True) context['stream_prefix'] = get_stream_prefix(request) + context['pandora_url'] = settings.DEFAULT_PANDORA_API.replace('/api/', '') return render(request, 'text.html', context) diff --git a/app/urls.py b/app/urls.py index 48d8281..1db446a 100755 --- a/app/urls.py +++ b/app/urls.py @@ -25,7 +25,7 @@ urlpatterns = [ path('admin/', admin.site.urls), #path('pandoraAPI/', video.pandoraAPI, name='pandoraAPI'), path(settings.URL_PREFIX + 'films/', video.films, name='films'), - path(settings.URL_PREFIX + 'film//play/', video.film_play, name='film_play'), + path(settings.URL_PREFIX + 'film//play/', video.film_play, name='film_play'), path(settings.URL_PREFIX + 'film//', video.film, name='film'), path(settings.URL_PREFIX + 'edits/', video.edits, name='edits'), path(settings.URL_PREFIX + 'edit//play/', video.edit_play, name='edit_play'), diff --git a/app/video/views.py b/app/video/views.py index 7bb7c6e..81a2b37 100644 --- a/app/video/views.py +++ b/app/video/views.py @@ -1,4 +1,5 @@ import logging +import json from django.shortcuts import render, redirect, get_object_or_404 from django.views.decorators.csrf import csrf_exempt @@ -20,12 +21,16 @@ def get_ip(request): return ip def get_stream_prefix(request): - prefix = "https://v2.phantas.ma/" + return settings.DEFAULT_PANDORA_API.replace('/api/', '/') + ''' + domain = settings.DEFAULT_PANDORA_API.split('/')[3] + prefix = "https://v2.%s/" % domain cdn = { - 'Eastern Asia': "https://v1.phantas.ma/", - 'Southern Asia': "https://v1.phantas.ma/", - 'Asia': "https://v1.phantas.ma/", + 'Eastern Asia': "https://v1.%s/" % domain, + 'Southern Asia': "https://v1.%s/" % domain, + 'Asia': "https://v1.%s/" % domain, } + ''' ip = get_ip(request) try: g = GeoIP2() @@ -54,13 +59,17 @@ def film(request, slug): context['film'] = get_object_or_404(models.Film, slug=slug) context['settings'] = settings context['stream_prefix'] = get_stream_prefix(request) + context['pandora_url'] = settings.DEFAULT_PANDORA_API.replace('/api/', '') return render(request, 'film.html', context) -def film_play(request, slug, lang): +def film_play(request, slug, id): context = {} - context['film'] = get_object_or_404(models.Film, slug=slug) - context['lang'] = lang context['settings'] = settings + context['config'] = json.dumps({ + 'stream_prefix': get_stream_prefix(request), + 'item': id + }) + context['pandora_url'] = settings.DEFAULT_PANDORA_API.replace('/api/', '') return render(request, 'film_play.html', context) def edits(request):