[ADD] website_event_track_live: configure Youtube videos / streams on event.tracks

RATIONALE

Events are sometimes held online, gathering a community. In this merge we
improve Event application to better support full-online events with improved
tracks, wishlists, chat rooms, ...

PURPOSE

Allow to include a youtube video / live stream on event tracks and update the
interface to show "online" capabilities.

SPECIFICATIONS

This commit improves the existing event.tracks to include the possibility to
configure a Youtube live stream or video.

The frontend view of the track was adapted accordingly to show the stream /
video instead of the track description when available.

At the end of the video, the track view will automatically suggest a next track
to the attendee in order to keep him looping through interesting tracks.
Those suggestions are based on an algorithm that tries to select the track that
would be the most interesting to this attendee based on his wishlist, similar
tags with current track, same room, ...

In addition, we automatically fallback to a Youtube thumbnail for tracks
website image when available.

We support two youtube modes

  * streaming: start and end of video is detected based on youtube API;
  * replay: boolean flag on track, telling the video is a static one, allowing
    to know by advance the end of video, and to pre-compute thumbnail and give
    early access if necessary;

KNOWN LIMITATIONS

Tracks do not hold any protection for payment-based tickets. Everything that
is published is available using standard ACLs. Content protection based on
tickets is currently not supported.

LINKS

Task ID-2252655 (Main Online Event task)
Task ID-2283735 (Talks -> Session upgrade)
Task ID-2283729 (Live/Youtube Tracks)

Co-Authored-By: Aurélien Warnon <awa@odoo.com>
Co-Authored-By: David Beguin <dbe@odoo.com>
Co-Authored-By: Thibault Delavallée <tde@odoo.com>
This commit is contained in:
Aurélien Warnon
2020-08-04 14:28:29 +00:00
committed by Thibault Delavallée
co-authored by David Beguin Thibault Delavallée
parent e73ec2ddb9
commit fab82c73a5
16 changed files with 546 additions and 0 deletions
@@ -1,2 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
@@ -15,8 +15,16 @@
'website_event_track_session',
],
'data': [
'views/assets.xml',
'views/event_track_templates_list.xml',
'views/event_track_templates_page.xml',
'views/event_track_views.xml',
],
'demo': [
'data/event_track_demo.xml'
],
'qweb': [
'static/src/xml/website_event_track_live_templates.xml'
],
'application': False,
'installable': True,
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import track_live
from . import session
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from odoo.addons.website_event_track_session.controllers.session import WebsiteEventSessionController
from odoo.http import request
class WebsiteEventSessionLiveController(WebsiteEventSessionController):
def _event_track_page_get_values(self, event, track, **options):
if 'widescreen' not in options:
options['widescreen'] = bool(track.youtube_video_url)
values = super(WebsiteEventSessionLiveController, self)._event_track_page_get_values(event, track, **options)
# Youtube disables the chat embed on all mobile devices
# This regex is a naive attempt at matching their behavior (should work for most cases)
values['is_mobile_chat_disabled'] = bool(re.match(
r'^.*(Android|iPad|iPhone).*',
request.httprequest.headers.get('User-Agent', request.httprequest.headers.get('user-agent', ''))))
return values
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.addons.website_event_track_online.controllers.event_track import EventTrackOnlineController
class EventTrackLiveController(EventTrackOnlineController):
@http.route('/event_track/get_track_suggestion', type='json', auth='public', website=True)
def get_next_track_suggestion(self, track_id):
track = self._fetch_track(track_id)
track_suggestion = track._get_track_suggestions(
restrict_domain=[('youtube_video_url', '!=', False), ('is_published', '=', True)],
limit=1)
if not track_suggestion:
return False
track_suggestion_sudo = track_suggestion.sudo()
track_sudo = track.sudo()
return self._prepare_track_suggestion_values(track_sudo, track_suggestion_sudo)
def _prepare_track_suggestion_values(self, track, track_suggestion):
return {
'current_track': {
'name': track.name,
'website_image_url': track.website_image_url,
},
'suggestion': {
'id': track_suggestion.id,
'name': track_suggestion.name,
'speaker_name': track_suggestion.partner_name,
'website_url': track_suggestion.website_url
}
}
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tracks of: "OpenWood: Furniture Collection Online Reveal" -->
<record id="website_event_track.event_7_track_1" model="event.track">
<field name="youtube_video_url">https://www.youtube.com/watch?v=66AIsiezq5g</field>
<field name="is_youtube_replay" eval="True" />
</record>
<record id="website_event_track.event_7_track_2" model="event.track">
<field name="youtube_video_url">https://www.youtube.com/watch?v=IvUU8joBb1Q</field>
<field name="is_youtube_replay" eval="True" />
</record>
<record id="website_event_track.event_7_track_3" model="event.track">
<field name="youtube_video_url">https://www.youtube.com/watch?v=qRrA8al77a0</field>
<field name="is_youtube_replay" eval="True" />
</record>
<record id="website_event_track.event_7_track_4" model="event.track">
<field name="youtube_video_url">https://www.youtube.com/watch?v=bZ-queeu-lk</field>
<field name="is_youtube_replay" eval="True" />
</record>
<record id="website_event_track.event_7_track_5" model="event.track">
<field name="youtube_video_url">https://www.youtube.com/watch?v=gV3g5FzmQiM</field>
<field name="is_youtube_replay" eval="True" />
</record>
<record id="website_event_track.event_7_track_6" model="event.track">
<field name="youtube_video_url">https://www.youtube.com/watch?v=g3IoL9hxABg</field>
<field name="is_youtube_replay" eval="True" />
</record>
<record id="website_event_track.event_7_track_10" model="event.track">
<field name="youtube_video_url">https://www.youtube.com/watch?v=AVZtyXJB-rI</field>
<field name="is_youtube_replay" eval="True" />
</record>
<record id="website_event_track.event_7_track_11" model="event.track">
<field name="youtube_video_url">https://www.youtube.com/watch?v=LHztthKy4RE</field>
<field name="is_youtube_replay" eval="True" />
</record>
<record id="website_event_track.event_7_track_12" model="event.track">
<field name="youtube_video_url">https://www.youtube.com/watch?v=B-nEYsyRlYo</field>
<field name="is_youtube_replay" eval="True" />
</record>
<record id="website_event_track.event_7_track_13" model="event.track">
<field name="youtube_video_url">https://www.youtube.com/watch?v=7X8xuipr4_A</field>
<field name="is_youtube_replay" eval="True" />
</record>
</odoo>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import event_track
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from odoo import api, fields, models
class Track(models.Model):
_inherit = 'event.track'
youtube_video_url = fields.Char('Youtube Video URL',
help="Configure this URL so that event attendees can see your Track in video!")
youtube_video_id = fields.Char('Youtube video ID', compute='_compute_youtube_video_id',
help="Extracted from the video URL and used to infer various links (embed/thumbnail/...)")
is_youtube_replay = fields.Boolean('Is Youtube Replay',
help="Check this option if the video is already available on Youtube to avoid showing 'Direct' options (Chat, ...)")
is_youtube_chat_available = fields.Boolean('Is Chat Available', compute='_compute_is_youtube_chat_available')
@api.depends('youtube_video_url')
def _compute_youtube_video_id(self):
for track in self:
if track.youtube_video_url:
regex = r'^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*'
match = re.match(regex, track.youtube_video_url)
if match and len(match.groups()) == 2 and len(match.group(2)) == 11:
track.youtube_video_id = match.group(2)
if not track.youtube_video_id:
track.youtube_video_id = False
@api.depends('youtube_video_id', 'is_youtube_replay', 'date_end', 'is_track_done')
def _compute_website_image_url(self):
youtube_thumbnail_tracks = self.filtered(lambda track: not track.website_image and track.youtube_video_id)
super(Track, self - youtube_thumbnail_tracks)._compute_website_image_url()
for track in youtube_thumbnail_tracks:
track.website_image_url = f'https://img.youtube.com/vi/{track.youtube_video_id}/maxresdefault.jpg'
@api.depends('youtube_video_url', 'is_youtube_replay', 'date', 'date_end', 'is_track_upcoming', 'is_track_live')
def _compute_is_youtube_chat_available(self):
for track in self:
track.is_youtube_chat_available = track.youtube_video_url and not track.is_youtube_replay and (track.is_track_soon or track.is_track_live)
@@ -0,0 +1,121 @@
var onYouTubeIframeAPIReady;
odoo.define('website_event_track_live.website_event_youtube_embed', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
var TrackSuggestionWidget = require('website_event_track_live.website_event_track_suggestion');
var YOUTUBE_VIDEO_ENDED = 0;
var YOUTUBE_VIDEO_PLAYING = 1;
var YOUTUBE_VIDEO_PAUSED = 2;
publicWidget.registry.websiteEventTrackLive = publicWidget.Widget.extend({
selector: '.o_wevent_event_track_live',
custom_events: _.extend({}, publicWidget.Widget.prototype.custom_events, {
'video-ended': '_onVideoEnded'
}),
start: function () {
var self = this;
return this._super(...arguments).then(function () {
self._setupYoutubePlayer();
self.isFullScreen = !!document.fullscreenElement;
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_onPlayerReady: function () {
$(window).on('resize', this._onResize.bind(this));
this.$('.o_wevent_event_track_live_loading').remove();
},
_onPlayerStateChange: function (event) {
if (event.data === YOUTUBE_VIDEO_ENDED) {
this.trigger('video-ended');
} else if (event.data === YOUTUBE_VIDEO_PLAYING) {
this.trigger('video-playing');
} else if (event.data === YOUTUBE_VIDEO_PAUSED) {
this.trigger('video-paused');
}
},
_onVideoEnded: function () {
var self = this;
this._rpc({
route: '/event_track/get_track_suggestion',
params: {
track_id: this.$el.data('trackId'),
}
}).then(function (suggestion) {
self.nextSuggestion = suggestion;
self._showSuggestion();
});
},
_onReplay: function () {
this.youtubePlayer.seekTo(0);
this.youtubePlayer.playVideo();
if (this.trackSuggestion) {
delete this.trackSuggestion;
}
},
/**
* The 'fullscreenchange' event is probably a better fit but it is unfortunately
* not triggered when Youtube enters fullscreen mode.
* However, the global window 'resize' is.
*/
_onResize: function () {
this.isFullScreen = !!document.fullscreenElement;
this._showSuggestion();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_setupYoutubePlayer: function () {
var self = this;
var youtubeId = self.$el.data('youtubeVideoId');
var $youtubeElement = $('<script/>', {src: 'https://www.youtube.com/iframe_api'});
$(document.head).append($youtubeElement);
onYouTubeIframeAPIReady = function () {
self.youtubePlayer = new YT.Player('o_wevent_youtube_iframe_container', {
height: '100%',
width: '100%',
videoId: youtubeId,
playerVars: {
autoplay: 1,
enablejsapi: 1,
rel: 0,
origin: window.location.origin,
widget_referrer: window.location.origin,
},
events: {
'onReady': self._onPlayerReady.bind(self),
'onStateChange': self._onPlayerStateChange.bind(self)
}
});
};
},
/**
* Automatically launches the next suggested track when this one ends
* and the user is not in fullscreen mode.
*/
_showSuggestion: function () {
if (this.nextSuggestion && !this.isFullScreen && !this.trackSuggestion) {
this.trackSuggestion = new TrackSuggestionWidget(this, this.nextSuggestion);
this.trackSuggestion.appendTo(this.$el);
this.trackSuggestion.on('replay', null, this._onReplay.bind(this));
}
}
});
});
@@ -0,0 +1,81 @@
odoo.define('website_event_track_live.website_event_track_suggestion', function (require) {
'use strict';
var Widget = require('web.Widget');
var WebsiteEventTrackSuggestion = Widget.extend({
template: 'website_event_track_live.website_event_track_suggestion',
xmlDependencies: ['/website_event_track_live/static/src/xml/website_event_track_live_templates.xml'],
events: {
'click .owevent_track_suggestion_next': '_onNextTrackClick',
'click .owevent_track_suggestion_close': '_onCloseClick',
'click .owevent_track_suggestion_replay': '_onReplayClick'
},
init: function (parent, options) {
this._super(...arguments);
this.currentTrack = {
'name': options.current_track.name,
'imageSrc': options.current_track.website_image_url,
};
this.suggestion = {
'name': options.suggestion.name,
'speakerName': options.suggestion.speaker_name,
'trackUrl': options.suggestion.website_url,
};
},
start: function () {
var self = this;
this._super(...arguments).then(function () {
self.timerInterval = setInterval(self._updateTimer.bind(self), 1000);
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* If the user clicks on replay, remove this suggestion window and send an
* event to the parent so that it can rewind the video to the beginning.
*/
_onReplayClick: function () {
this.trigger_up('replay');
clearInterval(this.timerInterval);
this.destroy();
},
_onCloseClick: function () {
clearInterval(this.timerInterval);
this.$('.owevent_track_suggestion_next').addClass('invisible');
},
_onNextTrackClick: function (ev) {
if ($(ev.target).hasClass('owevent_track_suggestion_close')) {
return;
}
window.location = this.suggestion.trackUrl;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_updateTimer: function () {
var secondsLeft = parseInt(this.$('.owevent_track_suggestion_timer_text').text());
if (secondsLeft > 1) {
secondsLeft -= 1;
this.$('.owevent_track_suggestion_timer_text').text(secondsLeft);
} else {
window.location = this.suggestion.trackUrl;
}
}
});
return WebsiteEventTrackSuggestion;
});
@@ -0,0 +1,42 @@
.o_wesession_track_aside_tab_chat {
height: 70vh;
}
#o_wevent_youtube_iframe_container, .o_wevent_event_track_live_loading {
height: 85vh;
}
.o_wevent_event_track_live_loading {
background-color: #888888;
i {
bottom: 1px;
}
}
.owevent_track_suggestion {
background-color: #888888;
background-repeat: no-repeat;
background-size: 100% 100%;
height: 85vh;
top: 0px;
left: 0px;
.owevent_track_suggestion_content {
background-color: rgba(0,0,0,.5);
.owevent_track_suggestion_next {
background-color: rgba(0,0,0,.5);
transition: background-color .3s linear;
&:hover {
cursor: pointer;
background-color: rgba(0,0,0,.9);
}
.owevent_track_suggestion_close {
top: .5rem;
right: .5rem;
}
}
}
}
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<div t-name="website_event_track_live.website_event_track_suggestion"
class="owevent_track_suggestion position-absolute w-100"
t-attf-style="background-image: url('#{widget.currentTrack.imageSrc}');">
<div class="h-100 w-100 p-4 d-flex flex-column align-items-center owevent_track_suggestion_content">
<div class="flex-grow-1 pt-5 d-flex flex-column align-items-center justify-content-center">
<div class="text-white">You just watched:</div>
<div class="h1 text-white" t-esc="widget.currentTrack.name"/>
<div>
<a href="#" class="owevent_track_suggestion_replay font-weight-bold">
<span>Replay Video</span>
</a>
</div>
</div>
<div class="d-flex w-100 flex-column align-items-end">
<div class="p-2 rounded owevent_track_suggestion_next position-relative">
<div class="text-white text-right owevent_track_suggestion_close_wrapper">
<i class="fa fa-remove owevent_track_suggestion_close position-absolute"/>
</div>
<span class="text-white">Up Next:</span>
<span class="text-primary font-weight-bold pr-4" t-esc="widget.suggestion.name"/>
<div class="text-white owevent_track_suggestion_timer_text_wrapper">
<span>Starts in</span>
<span class="owevent_track_suggestion_timer_text">30</span>
</div>
</div>
</div>
</div>
</div>
</templates>
@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<odoo>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Event Track Live Assets Frontend">
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" href="/website_event_track_live/static/src/scss/website_event_track_live.scss"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website_event_track_live/static/src/js/website_event_track_suggestion.js"></script>
<script type="text/javascript" src="/website_event_track_live/static/src/js/website_event_track_live.js"></script>
</xpath>
</template>
</odoo>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="tracks_display_list" inherit_id="website_event_track_session.tracks_display_list">
<!-- TRACK LIST: ADD REPLAY TAG FOR FINISHED TRACKS -->
<xpath expr="//div[@name='o_wesession_list_item_organization']//span[@t-elif='not track.is_track_done and not track.is_track_soon']" position="after">
<a t-elif="track.youtube_video_url"
t-att-href="track.website_url" class="badge badge-danger">Replay
</a>
</xpath>
<!-- ADD YOUTUBE ICON -->
<xpath expr="//span[@name='o_wesession_list_item_topic']/a/span[@t-field='track.name']" position="before">
<i t-if="track.youtube_video_url and (track.is_track_soon or track.is_track_live or track.is_youtube_replay)"
class="fa fa-youtube-play text-danger mr-1"/>
</xpath>
<xpath expr="//span[@name='o_wesession_list_item_topic']/t/span[@t-field='track.name']" position="before">
<i t-if="track.youtube_video_url and (track.is_track_soon or track.is_track_live or track.is_youtube_replay)"
class="fa fa-youtube-play text-danger mr-1"/>
</xpath>
</template>
</odoo>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- MAIN: REMOVE HEADER/FOOTER IF WIDESCREEN -->
<template id="event_track_main" inherit_id="website_event_track_session.event_track_main">
<xpath expr="//t[@t-call='website_event.layout']" position="before">
<t t-set="no_header" t-value="option_widescreen"/>
<t t-set="no_footer" t-value="option_widescreen"/>
</xpath>
</template>
<!-- MAIN: ADD VIDEO -->
<template id="event_track_content" inherit_id="website_event_track_session.event_track_content">
<!-- Do not display "starting in" in case of replay mode as video is already available -->
<xpath expr="//div[@t-elif='track.track_start_remaining']" position="attributes">
<attribute name="t-elif">track.track_start_remaining and not track.is_youtube_replay</attribute>
</xpath>
<!-- Add video -->
<xpath expr="//div[@name='o_wesession_track_main']//div" position="before">
<t t-set="show_youtube_frame" t-value="track.youtube_video_url and (track.is_youtube_replay or track.is_track_soon or track.is_track_live or track.is_track_done)" />
<div t-if="show_youtube_frame" class="flex-grow-1 o_wevent_event_track_live"
t-att-data-track-id="track.id"
t-att-data-youtube-video-id="track.youtube_video_id">
<div class="position-absolute o_wevent_event_track_live_loading w-100 d-flex align-items-center justify-content-center text-white h4">
<i class="fa fa-spin fa-spinner position-relative"/>
<span class="pl-2">Loading Video...</span>
</div>
<div id="o_wevent_youtube_iframe_container" class="w-100"/>
</div>
</xpath>
</template>
<!-- ASIDE: ADD CHAT TAB FOR VIDEOS -->
<template id="event_track_aside" inherit_id="website_event_track_session.event_track_aside">
<xpath expr="//ul[hasclass('o_wesession_track_aside_nav')]" position="inside">
<li t-if="track.is_youtube_chat_available and not is_mobile_chat_disabled" class="nav-item flex-grow-1">
<a href="#track_chat" aria-controls="track_chat" class="nav-link" role="tab" data-toggle="tab">
Chat
</a>
</li>
</xpath>
<xpath expr="//div[hasclass('o_wesession_track_aside_tabs')]" position="inside">
<div t-if="track.is_youtube_chat_available and not is_mobile_chat_disabled" id="track_chat"
class="tab-pane fade o_wesession_track_aside_tab_chat" role="tabpanel">
<iframe t-attf-src="https://www.youtube.com/live_chat?v=#{track.youtube_video_id}&amp;embed_domain=#{hostname}"
height="100%"
width="100%"
frameborder="0" allowfullscreen="allowfullscreen"/>
</div>
</xpath>
</template>
</odoo>
@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<odoo>
<record id="event_track_view_form" model="ir.ui.view">
<field name="name">event.track.view.form.inherit.live</field>
<field name="model">event.track</field>
<field name="inherit_id" ref="website_event_track.view_event_track_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='active']" position="after">
<field name="youtube_video_url" widget="url" />
<field name="is_youtube_replay" attrs="{'invisible': [('youtube_video_url', '=', '')]}" />
</xpath>
</field>
</record>
</odoo>