From 5b9afdb5f46d462e550fe247a508e7a41bd0d9f9 Mon Sep 17 00:00:00 2001
From: uhensler <urs.hensler@frentix.com>
Date: Wed, 29 May 2019 16:45:49 +0200
Subject: [PATCH] OO-4043: Automatic GUI update ("push") when live stream has
 started, ended or changed

---
 .../nodes/livestream/LiveStreamEvent.java     |   4 +
 .../manager/LIveStreamServiceImpl.java        |   2 +
 .../livestream/model/LiveStreamEventImpl.java |  46 +++
 .../ui/LiveStreamMetadataController.java      | 115 +++++++
 .../ui/LiveStreamVideoController.java         |  78 +++++
 .../ui/LiveStreamViewerController.java        | 111 ++-----
 .../ui/LiveStreamViewersController.java       | 303 ++++++++++++++++++
 .../livestream/ui/LiveStreamsController.java  |  36 ++-
 .../livestream/ui/_content/metadata.html      |  29 ++
 .../nodes/livestream/ui/_content/streams.html |   2 +-
 .../nodes/livestream/ui/_content/video.html   |  10 +
 .../nodes/livestream/ui/_content/viewer.html  |  44 +--
 .../nodes/livestream/ui/_content/viewers.html |  18 ++
 13 files changed, 656 insertions(+), 142 deletions(-)
 create mode 100644 src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamMetadataController.java
 create mode 100644 src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamVideoController.java
 create mode 100644 src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamViewersController.java
 create mode 100644 src/main/java/org/olat/course/nodes/livestream/ui/_content/metadata.html
 create mode 100644 src/main/java/org/olat/course/nodes/livestream/ui/_content/video.html
 create mode 100644 src/main/java/org/olat/course/nodes/livestream/ui/_content/viewers.html

diff --git a/src/main/java/org/olat/course/nodes/livestream/LiveStreamEvent.java b/src/main/java/org/olat/course/nodes/livestream/LiveStreamEvent.java
index ec304b7fc15..680225310f2 100644
--- a/src/main/java/org/olat/course/nodes/livestream/LiveStreamEvent.java
+++ b/src/main/java/org/olat/course/nodes/livestream/LiveStreamEvent.java
@@ -28,6 +28,8 @@ import java.util.Date;
  *
  */
 public interface LiveStreamEvent {
+	
+	String getId();
 
 	String getSubject();
 
@@ -37,6 +39,8 @@ public interface LiveStreamEvent {
 
 	Date getEnd();
 
+	boolean isAllDayEvent();
+
 	String getLocation();
 	
 	String getLiveStreamUrl();
diff --git a/src/main/java/org/olat/course/nodes/livestream/manager/LIveStreamServiceImpl.java b/src/main/java/org/olat/course/nodes/livestream/manager/LIveStreamServiceImpl.java
index 490e9b6e90b..ff9ec538050 100644
--- a/src/main/java/org/olat/course/nodes/livestream/manager/LIveStreamServiceImpl.java
+++ b/src/main/java/org/olat/course/nodes/livestream/manager/LIveStreamServiceImpl.java
@@ -114,8 +114,10 @@ public class LIveStreamServiceImpl implements LiveStreamService {
 
 	private LiveStreamEventImpl toLiveStreamEvent(KalendarEvent event, boolean timeOnly) {
 		LiveStreamEventImpl liveStreamEvent = new LiveStreamEventImpl();
+		liveStreamEvent.setId(event.getID());
 		liveStreamEvent.setBegin(event.getBegin());
 		liveStreamEvent.setEnd(event.getEnd());
+		liveStreamEvent.setAllDayEvent(event.isAllDayEvent());
 		liveStreamEvent.setLiveStreamUrl(event.getLiveStreamUrl());
 		if (!timeOnly) {
 			liveStreamEvent.setSubject(event.getSubject());
diff --git a/src/main/java/org/olat/course/nodes/livestream/model/LiveStreamEventImpl.java b/src/main/java/org/olat/course/nodes/livestream/model/LiveStreamEventImpl.java
index a73cde75bd5..0a5f6219152 100644
--- a/src/main/java/org/olat/course/nodes/livestream/model/LiveStreamEventImpl.java
+++ b/src/main/java/org/olat/course/nodes/livestream/model/LiveStreamEventImpl.java
@@ -32,13 +32,25 @@ import org.olat.course.nodes.livestream.LiveStreamEvent;
  */
 public class LiveStreamEventImpl implements LiveStreamEvent {
 
+	private String id;
 	private String subject;
 	private String description;
 	private Date begin;
 	private Date end;
+	private boolean allDayEvent;
 	private String location;
 	private String liveStreamUrl;
 
+	
+	@Override
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
 	@Override
 	public String getSubject() {
 		return subject;
@@ -80,6 +92,15 @@ public class LiveStreamEventImpl implements LiveStreamEvent {
 		return location;
 	}
 
+	@Override
+	public boolean isAllDayEvent() {
+		return allDayEvent;
+	}
+
+	public void setAllDayEvent(boolean allDayEvent) {
+		this.allDayEvent = allDayEvent;
+	}
+
 	public void setLocation(String location) {
 		this.location = location;
 	}
@@ -93,4 +114,29 @@ public class LiveStreamEventImpl implements LiveStreamEvent {
 		this.liveStreamUrl = liveStreamUrl;
 	}
 
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((id == null) ? 0 : id.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		LiveStreamEventImpl other = (LiveStreamEventImpl) obj;
+		if (id == null) {
+			if (other.id != null)
+				return false;
+		} else if (!id.equals(other.id))
+			return false;
+		return true;
+	}
+
 }
diff --git a/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamMetadataController.java b/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamMetadataController.java
new file mode 100644
index 00000000000..e35dba8a8b4
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamMetadataController.java
@@ -0,0 +1,115 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes.livestream.ui;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+import org.apache.commons.lang.time.DateUtils;
+import org.olat.commons.calendar.CalendarManager;
+import org.olat.commons.calendar.CalendarUtils;
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.velocity.VelocityContainer;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.util.Formatter;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.Util;
+import org.olat.course.nodes.livestream.LiveStreamEvent;
+
+/**
+ * 
+ * Initial date: 29 May 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class LiveStreamMetadataController extends BasicController {
+	
+	private final VelocityContainer mainVC;
+	
+	private LiveStreamEvent currentEvent;
+
+	protected LiveStreamMetadataController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl, Util.createPackageTranslator(CalendarManager.class, ureq.getLocale()));
+		mainVC = createVelocityContainer("metadata");
+		updateUI(null);
+		putInitialPanel(mainVC);
+	}
+	
+	public void setEvent(LiveStreamEvent event) {
+		updateUI(event);
+	}
+
+	private void updateUI(LiveStreamEvent event) {
+		if (event == null || !event.equals(currentEvent)) {
+			currentEvent = event;
+			if (event != null) {
+				mainVC.contextPut("id", event.getId());
+				mainVC.contextPut("title", event.getSubject());
+				addDateToMainVC(event);
+				StringBuilder description = Formatter.stripTabsAndReturns(Formatter.formatURLsAsLinks(event.getDescription()));
+				mainVC.contextPut("description", description.toString());
+				if (StringHelper.containsNonWhitespace(event.getLocation())) {
+					mainVC.contextPut("location", event.getLocation());
+				}
+			} else {
+				mainVC.contextRemove("id");
+			}
+		}
+	}
+	
+	private String addDateToMainVC(LiveStreamEvent calEvent) {
+		Locale locale = getLocale();
+		Calendar cal = CalendarUtils.createCalendarInstance(locale);
+		Date begin = calEvent.getBegin();
+		Date end = calEvent.getEnd();	
+		cal.setTime(begin);
+		
+		StringBuilder sb = new StringBuilder();
+		sb.append(StringHelper.formatLocaleDateFull(begin.getTime(), locale));
+		mainVC.contextPut("date", sb.toString());
+
+		if (!calEvent.isAllDayEvent()) {
+			sb = new StringBuilder();
+			sb.append(StringHelper.formatLocaleTime(begin.getTime(), locale));
+			sb.append(" - ");
+			if (!DateUtils.isSameDay(begin, end)) {
+				sb.append(StringHelper.formatLocaleDateFull(end.getTime(), locale)).append(", ");
+			} 
+			sb.append(StringHelper.formatLocaleTime(end.getTime(), locale));
+			mainVC.contextPut("time", sb.toString());
+		}
+		return sb.toString();
+	}
+
+	@Override
+	protected void event(UserRequest ureq, Component source, Event event) {
+		//
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+}
diff --git a/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamVideoController.java b/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamVideoController.java
new file mode 100644
index 00000000000..7e70902eec6
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamVideoController.java
@@ -0,0 +1,78 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes.livestream.ui;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.velocity.VelocityContainer;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.util.CodeHelper;
+import org.olat.core.util.StringHelper;
+import org.olat.course.nodes.livestream.LiveStreamEvent;
+
+/**
+ * 
+ * Initial date: 29 May 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class LiveStreamVideoController extends BasicController {
+	
+	private final VelocityContainer mainVC;
+	
+	private String runningUrl;
+
+	protected LiveStreamVideoController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl);
+		mainVC = createVelocityContainer("video");
+		updateUI(null);
+		putInitialPanel(mainVC);
+	}
+	
+	public void setEvent(LiveStreamEvent event) {
+		String url = event != null? event.getLiveStreamUrl(): null;
+		updateUI(url);
+	}
+
+	private void updateUI(String url) {
+		if (url == null || !url.equalsIgnoreCase(runningUrl)) {
+			runningUrl = url;
+			if (StringHelper.containsNonWhitespace(runningUrl)) {
+				mainVC.contextPut("id", CodeHelper.getRAMUniqueID());
+				mainVC.contextPut("src", url);
+			} else {
+				mainVC.contextRemove("id");
+			}
+		}
+	}
+
+	@Override
+	protected void event(UserRequest ureq, Component source, Event event) {
+		//
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+}
diff --git a/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamViewerController.java b/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamViewerController.java
index 9fe0edb36b1..208b8b35fe2 100644
--- a/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamViewerController.java
+++ b/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamViewerController.java
@@ -19,119 +19,44 @@
  */
 package org.olat.course.nodes.livestream.ui;
 
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Date;
-import java.util.Locale;
-
-import org.apache.commons.lang.time.DateUtils;
-import org.olat.commons.calendar.CalendarManager;
-import org.olat.commons.calendar.CalendarUtils;
-import org.olat.commons.calendar.model.Kalendar;
-import org.olat.commons.calendar.model.KalendarEvent;
-import org.olat.commons.calendar.ui.components.KalendarRenderWrapper;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.velocity.VelocityContainer;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
-import org.olat.core.util.CodeHelper;
-import org.olat.core.util.Formatter;
-import org.olat.core.util.StringHelper;
-import org.olat.core.util.Util;
-import org.olat.course.nodes.LiveStreamCourseNode;
-import org.olat.course.nodes.cal.CourseCalendars;
-import org.olat.modules.ModuleConfiguration;
+import org.olat.course.nodes.livestream.LiveStreamEvent;
 
 /**
  * 
- * Initial date: 24 May 2019<br>
+ * Initial date: 29 May 2019<br>
  * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
  *
  */
 public class LiveStreamViewerController extends BasicController {
-
-	private final VelocityContainer mainVC;
-	
-	private final CourseCalendars calendars;
-	private final int bufferBeforeMin;
-	private final int bufferAfterMin;
 	
-	public LiveStreamViewerController(UserRequest ureq, WindowControl wControl, ModuleConfiguration moduleConfiguration,
-			CourseCalendars calendars) {
-		super(ureq, wControl, Util.createPackageTranslator(CalendarManager.class, ureq.getLocale()));
-		this.calendars = calendars;
-		
-		bufferBeforeMin = moduleConfiguration.getIntegerSafe(LiveStreamCourseNode.CONFIG_BUFFER_BEFORE_MIN, 0);
-		bufferAfterMin = moduleConfiguration.getIntegerSafe(LiveStreamCourseNode.CONFIG_BUFFER_AFTER_MIN, 0);
+	private final VelocityContainer mainVC;
+	private final LiveStreamVideoController videoCtrl;
+	private final LiveStreamMetadataController metadataCtrl;
+
+	public LiveStreamViewerController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl);
 		
 		mainVC = createVelocityContainer("viewer");
-		KalendarEvent liveStreamEvent = getActiveLiveStream();
-		if (liveStreamEvent == null) {
-			mainVC.contextPut("noLiveStream", Boolean.TRUE);
-		} else {
-			mainVC.contextPut("id", CodeHelper.getRAMUniqueID());
-			mainVC.contextPut("src", liveStreamEvent.getLiveStreamUrl());
-			mainVC.contextPut("title", liveStreamEvent.getSubject());
-			addDateToMainVC(liveStreamEvent);
-			StringBuilder description = Formatter.stripTabsAndReturns(Formatter.formatURLsAsLinks(liveStreamEvent.getDescription()));
-			mainVC.contextPut("description", description.toString());
-			if (StringHelper.containsNonWhitespace(liveStreamEvent.getLocation())) {
-				mainVC.contextPut("location", liveStreamEvent.getLocation());
-			}
-		}
 		
-		putInitialPanel(mainVC);
-	}
-	
-	private KalendarEvent getActiveLiveStream() {
-		for (KalendarRenderWrapper calendar : calendars.getCalendars()) {
-			Kalendar cal = calendar.reloadKalendar();
-			Collection<KalendarEvent> events = cal.getEvents();
-			for (KalendarEvent event : events) {
-				if (isActiveLiveStream(event)) {
-					return event;
-				}
-			}
-		}
-		return null;
-	}
-
-	private boolean isActiveLiveStream(KalendarEvent event) {
-		if (event.getLiveStreamUrl() == null) return false;
+		videoCtrl = new LiveStreamVideoController(ureq, wControl);
+		listenTo(videoCtrl);
+		mainVC.put("video", videoCtrl.getInitialComponent());
+		metadataCtrl = new LiveStreamMetadataController(ureq, wControl);
+		listenTo(metadataCtrl);
+		mainVC.put("metadata", metadataCtrl.getInitialComponent());
 		
-		Instant now = Instant.now();
-		Instant startWithBuffer = event.getBegin().toInstant().minus(Duration.of(bufferBeforeMin, ChronoUnit.MINUTES));
-		Instant endWithBuffer = event.getEnd().toInstant().plus(Duration.of(bufferAfterMin, ChronoUnit.MINUTES));
-		return now.isAfter(startWithBuffer) && now.isBefore(endWithBuffer);
+		putInitialPanel(mainVC);
 	}
-	
-	private String addDateToMainVC(KalendarEvent calEvent) {
-		Locale locale = getLocale();
-		Calendar cal = CalendarUtils.createCalendarInstance(locale);
-		Date begin = calEvent.getBegin();
-		Date end = calEvent.getEnd();	
-		cal.setTime(begin);
-		
-		StringBuilder sb = new StringBuilder();
-		sb.append(StringHelper.formatLocaleDateFull(begin.getTime(), locale));
-		mainVC.contextPut("date", sb.toString());
 
-		if (!calEvent.isAllDayEvent()) {
-			sb = new StringBuilder();
-			sb.append(StringHelper.formatLocaleTime(begin.getTime(), locale));
-			sb.append(" - ");
-			if (!DateUtils.isSameDay(begin, end)) {
-				sb.append(StringHelper.formatLocaleDateFull(end.getTime(), locale)).append(", ");
-			} 
-			sb.append(StringHelper.formatLocaleTime(end.getTime(), locale));
-			mainVC.contextPut("time", sb.toString());
-		}
-		return sb.toString();
+	public void setEvent(LiveStreamEvent event) {
+		videoCtrl.setEvent(event);
+		metadataCtrl.setEvent(event);
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamViewersController.java b/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamViewersController.java
new file mode 100644
index 00000000000..720d16e9961
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamViewersController.java
@@ -0,0 +1,303 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes.livestream.ui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.velocity.VelocityContainer;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.controller.BasicController;
+import org.olat.course.nodes.LiveStreamCourseNode;
+import org.olat.course.nodes.cal.CourseCalendars;
+import org.olat.course.nodes.livestream.LiveStreamEvent;
+import org.olat.course.nodes.livestream.LiveStreamService;
+import org.olat.modules.ModuleConfiguration;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 24 May 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class LiveStreamViewersController extends BasicController {
+
+	private final VelocityContainer mainVC;
+	
+	private final LiveStreamViewerController displayCtrl0;
+	private final LiveStreamViewerController displayCtrl1;
+	private final LiveStreamViewerController displayCtrl2;
+	private final LiveStreamViewerController displayCtrl3;
+	private final LiveStreamViewerController displayCtrl4;
+	private final LiveStreamViewerController displayCtrl5;
+	private final LiveStreamViewerController displayCtrl6;
+	private final LiveStreamViewerController displayCtrl7;
+	private final LiveStreamViewerController displayCtrl8;
+	private final LiveStreamViewerController displayCtrl9;
+	
+	private final CourseCalendars calendars;
+	private final int bufferBeforeMin;
+	private final int bufferAfterMin;
+	
+	private final List<DisplayWrapper> displayWrappers;
+	private Boolean noLiveStream;
+	
+	@Autowired
+	private LiveStreamService liveStreamService;
+	
+	public LiveStreamViewersController(UserRequest ureq, WindowControl wControl, ModuleConfiguration moduleConfiguration,
+			CourseCalendars calendars) {
+		super(ureq, wControl);
+		this.calendars = calendars;
+		
+		bufferBeforeMin = moduleConfiguration.getIntegerSafe(LiveStreamCourseNode.CONFIG_BUFFER_BEFORE_MIN, 0);
+		bufferAfterMin = moduleConfiguration.getIntegerSafe(LiveStreamCourseNode.CONFIG_BUFFER_AFTER_MIN, 0);
+		
+		mainVC = createVelocityContainer("viewers");
+		
+		displayWrappers = new ArrayList<>();
+		displayCtrl0 = new LiveStreamViewerController(ureq, wControl);
+		listenTo(displayCtrl0);
+		mainVC.put("display0", displayCtrl0.getInitialComponent());
+		displayWrappers.add(new DisplayWrapper(displayCtrl0));
+		
+		displayCtrl1 = new LiveStreamViewerController(ureq, wControl);
+		listenTo(displayCtrl1);
+		mainVC.put("display1", displayCtrl1.getInitialComponent());
+		displayWrappers.add(new DisplayWrapper(displayCtrl1));
+		
+		displayCtrl2 = new LiveStreamViewerController(ureq, wControl);
+		listenTo(displayCtrl2);
+		mainVC.put("display2", displayCtrl2.getInitialComponent());
+		displayWrappers.add(new DisplayWrapper(displayCtrl2));
+		
+		displayCtrl3 = new LiveStreamViewerController(ureq, wControl);
+		listenTo(displayCtrl3);
+		mainVC.put("display3", displayCtrl3.getInitialComponent());
+		displayWrappers.add(new DisplayWrapper(displayCtrl3));
+		
+		displayCtrl4 = new LiveStreamViewerController(ureq, wControl);
+		listenTo(displayCtrl4);
+		mainVC.put("display4", displayCtrl4.getInitialComponent());
+		displayWrappers.add(new DisplayWrapper(displayCtrl4));
+		
+		displayCtrl5 = new LiveStreamViewerController(ureq, wControl);
+		listenTo(displayCtrl5);
+		mainVC.put("display5", displayCtrl5.getInitialComponent());
+		displayWrappers.add(new DisplayWrapper(displayCtrl5));
+		
+		displayCtrl6 = new LiveStreamViewerController(ureq, wControl);
+		listenTo(displayCtrl6);
+		mainVC.put("display6", displayCtrl6.getInitialComponent());
+		displayWrappers.add(new DisplayWrapper(displayCtrl6));
+		
+		displayCtrl7 = new LiveStreamViewerController(ureq, wControl);
+		listenTo(displayCtrl7);
+		mainVC.put("display7", displayCtrl7.getInitialComponent());
+		displayWrappers.add(new DisplayWrapper(displayCtrl7));
+		
+		displayCtrl8 = new LiveStreamViewerController(ureq, wControl);
+		listenTo(displayCtrl8);
+		mainVC.put("display8", displayCtrl8.getInitialComponent());
+		displayWrappers.add(new DisplayWrapper(displayCtrl8));
+		
+		displayCtrl9 = new LiveStreamViewerController(ureq, wControl);
+		listenTo(displayCtrl9);
+		mainVC.put("display9", displayCtrl9.getInitialComponent());
+		displayWrappers.add(new DisplayWrapper(displayCtrl9));
+		
+		refresh();
+		
+		putInitialPanel(mainVC);
+	}
+	
+	void refresh() {
+		List<? extends LiveStreamEvent> events = liveStreamService.getRunningEvents(calendars, bufferBeforeMin, bufferAfterMin);
+		putNoLiveStreamToMainVC(events);
+		
+		// This component should not get dirty, if a new live stream is started or ended to
+		// avoid restart of other running live streams. So we hat some slots. If a new
+		// live stream starts, it is put to the next free slot. If no slot is left, bad
+		// luck.
+		events = removeOverlappingWithSameUrl(events);
+		Collections.sort(events, (e1, e2) -> e1.getBegin().compareTo(e2.getBegin()));
+		displayStartedEvents(events);
+		removeEndedEvents(events);
+	}
+
+	private void putNoLiveStreamToMainVC(List<? extends LiveStreamEvent> events) {
+		Boolean newNoLiveStream = events.isEmpty()? Boolean.TRUE: Boolean.FALSE;
+		if (!newNoLiveStream.equals(noLiveStream)) {
+			noLiveStream = newNoLiveStream;
+			mainVC.contextPut("noLiveStream", noLiveStream);
+		}
+	}
+
+	private List<? extends LiveStreamEvent> removeOverlappingWithSameUrl(List<? extends LiveStreamEvent> events) {
+		List<LiveStreamEvent> remainingEvents = new ArrayList<>();
+		List<LiveStreamEvent> removedEvents = new ArrayList<>();
+		for (LiveStreamEvent event: events) {
+			if (!removedEvents.contains(event)) {
+				List<? extends LiveStreamEvent> sameUrlEvents = getEventsWithSameUrl(events, event);
+				if (sameUrlEvents.size() > 1) {
+					LiveStreamEvent runningEvent = getExcatlyRunning(sameUrlEvents);
+					if (runningEvent != null) {
+						for (LiveStreamEvent sameUrlEvent : sameUrlEvents) {
+							if (runningEvent.equals(sameUrlEvent)) {
+								remainingEvents.add(sameUrlEvent);
+							} else {
+								removedEvents.add(sameUrlEvent);
+							}
+						}
+					}
+				} else {
+					remainingEvents.add(event);
+				}
+			}
+		}
+		return remainingEvents;
+	}
+
+	private LiveStreamEvent getExcatlyRunning(List<? extends LiveStreamEvent> events) {
+		Collections.sort(events, (e1, e2) -> e1.getBegin().compareTo(e2.getBegin()));
+		Date now = new Date();
+		for (LiveStreamEvent event: events) {
+			if (event.getEnd().after(now)) {
+				return event;
+			}
+		}
+		return null;
+	}
+
+	private List<? extends LiveStreamEvent> getEventsWithSameUrl(List<? extends LiveStreamEvent> events,
+			LiveStreamEvent event) {
+		List<LiveStreamEvent> sameUrlEvents = new ArrayList<>();
+		for (LiveStreamEvent liveStreamEvent : events) {
+			if (liveStreamEvent.getLiveStreamUrl().equalsIgnoreCase(event.getLiveStreamUrl())) {
+				sameUrlEvents.add(liveStreamEvent);
+			}
+		}
+		return sameUrlEvents;
+	}
+
+	private void displayStartedEvents(List<? extends LiveStreamEvent> events) {
+		for (LiveStreamEvent event: events) {
+			DisplayWrapper displayWrapper = getDisplayWrapper(event);
+			if (displayWrapper != null) {
+				updateEvent(displayWrapper, event);
+			} else {
+				addToNextDisplay(event);
+			}
+		}
+	}
+	
+	private DisplayWrapper getDisplayWrapper(LiveStreamEvent event) {
+		for (DisplayWrapper displayWrapper : displayWrappers) {
+			if (displayWrapper.getEvent() != null && displayWrapper.getEvent().getLiveStreamUrl().equals(event.getLiveStreamUrl())) {
+				return displayWrapper;
+			}
+		}
+		return null;
+	}
+
+	private void addToNextDisplay(LiveStreamEvent event) {
+		DisplayWrapper nextDisplay = getNextFreeDisplay();
+		if (nextDisplay != null ) {
+			updateEvent(nextDisplay, event);
+		}
+	}
+
+	private DisplayWrapper getNextFreeDisplay() {
+		DisplayWrapper nextDisplay = null;
+		for (int i = displayWrappers.size() - 1; i >= 0; i--) {
+			if (displayWrappers.get(i).getEvent() == null) {
+				nextDisplay = displayWrappers.get(i);
+			} else {
+				return nextDisplay;
+			}
+		}
+		return nextDisplay;
+	}
+
+	private void removeEndedEvents(List<? extends LiveStreamEvent> events) {
+		for (DisplayWrapper displayWrapper : displayWrappers) {
+			LiveStreamEvent wrappedEvent = displayWrapper.getEvent();
+			if (hasEnded(wrappedEvent, events)) {
+				updateEvent(displayWrapper, null);
+			}
+		}
+	}
+
+	private boolean hasEnded(LiveStreamEvent event, List<? extends LiveStreamEvent> runningEvents) {
+		if (event == null) return false;
+		
+		for (LiveStreamEvent runningEvent : runningEvents) {
+			if (runningEvent.getLiveStreamUrl().equals(event.getLiveStreamUrl())) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	private void updateEvent(DisplayWrapper displayWrapper, LiveStreamEvent event) {
+		displayWrapper.setEvent(event);
+		displayWrapper.getController().setEvent(event);
+	}
+
+	@Override
+	protected void event(UserRequest ureq, Component source, Event event) {
+		//
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	private static class DisplayWrapper {
+		
+		private final LiveStreamViewerController controller;
+		private LiveStreamEvent event;
+		
+		private DisplayWrapper(LiveStreamViewerController controller) {
+			this.controller = controller;
+		}
+
+		public LiveStreamViewerController getController() {
+			return controller;
+		}
+
+		public LiveStreamEvent getEvent() {
+			return event;
+		}
+
+		public void setEvent(LiveStreamEvent event) {
+			this.event = event;
+		}
+
+	}
+
+}
diff --git a/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamsController.java b/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamsController.java
index af7fdbdd116..10cb78966db 100644
--- a/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamsController.java
+++ b/src/main/java/org/olat/course/nodes/livestream/ui/LiveStreamsController.java
@@ -19,12 +19,18 @@
  */
 package org.olat.course.nodes.livestream.ui;
 
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.logging.log4j.Logger;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.velocity.VelocityContainer;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.logging.Tracing;
 import org.olat.course.nodes.cal.CourseCalendars;
 import org.olat.modules.ModuleConfiguration;
 
@@ -36,28 +42,37 @@ import org.olat.modules.ModuleConfiguration;
  */
 public class LiveStreamsController extends BasicController {
 
+	private static final Logger log = Tracing.createLoggerFor(LiveStreamsController.class);
+
 	private final VelocityContainer mainVC;
 	
-	private LiveStreamViewerController viewerCtrl;
+	private LiveStreamViewersController viewersCtrl;
 	private LiveStreamListController listCtrl;
 
+	private ScheduledExecutorService scheduler;
+
 	public LiveStreamsController(UserRequest ureq, WindowControl wControl, ModuleConfiguration moduleConfiguration,
 			CourseCalendars calendars) {
 		super(ureq, wControl);
 		mainVC = createVelocityContainer("streams");
 
-		viewerCtrl = new LiveStreamViewerController(ureq, wControl, moduleConfiguration, calendars);
-		listenTo(viewerCtrl);
-		mainVC.put("viewer", viewerCtrl.getInitialComponent());
+		viewersCtrl = new LiveStreamViewersController(ureq, wControl, moduleConfiguration, calendars);
+		listenTo(viewersCtrl);
+		mainVC.put("viewers", viewersCtrl.getInitialComponent());
 
 		listCtrl = new LiveStreamListController(ureq, wControl, moduleConfiguration, calendars);
 		listenTo(listCtrl);
 		mainVC.put("list", listCtrl.getInitialComponent());
+		
+		scheduler = Executors.newScheduledThreadPool(1);
+		scheduler.scheduleAtFixedRate(new RefreshTask(), 10, 10, TimeUnit.SECONDS);
 
 		putInitialPanel(mainVC);
 	}
 
-	public void refreshData() {
+	public synchronized void refreshData() {
+		log.debug("Refresh live stream data of " + getIdentity());
+		viewersCtrl.refresh();
 		listCtrl.refreshData();
 	}
 
@@ -68,7 +83,16 @@ public class LiveStreamsController extends BasicController {
 
 	@Override
 	protected void doDispose() {
-		//
+		scheduler.shutdown();
+	}
+
+	private final class RefreshTask implements Runnable {
+
+		@Override
+		public void run() {
+			refreshData();
+		}
+
 	}
 
 }
diff --git a/src/main/java/org/olat/course/nodes/livestream/ui/_content/metadata.html b/src/main/java/org/olat/course/nodes/livestream/ui/_content/metadata.html
new file mode 100644
index 00000000000..090dfe937df
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/livestream/ui/_content/metadata.html
@@ -0,0 +1,29 @@
+#if($r.isNotNull($id))
+	<div class="o_block_large_bottom clearfix">
+		#if($title)
+			<h3>$title</h3>
+		#end
+
+		<div class="o_cal_date text-muted">
+			<i class="o_icon o_icon-fw o_icon_calendar">&nbsp;</i>
+			$date
+		</div>
+		#if($time && $time != "")
+			<div class="o_cal_time text-muted">
+				<i class="o_icon o_icon-fw o_icon_time">&nbsp;</i>
+				$time
+			</div>
+		#end
+		#if($location && !${location.isEmpty()})
+			<div class="o_cal_location text-muted">
+				<i class="o_icon o_icon-fw o_icon_home" title="$r.translateInAttribute("cal.form.location")">&nbsp;</i>
+				$r.escapeHtml($location)
+			</div>
+		#end
+		#if($description && !${description.isEmpty()})
+			<div class="o_cal_description">
+				$r.xssScan($description)
+			</div>
+		#end
+	</div>
+#end
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/nodes/livestream/ui/_content/streams.html b/src/main/java/org/olat/course/nodes/livestream/ui/_content/streams.html
index a9da74cd7e2..daadf3665d1 100644
--- a/src/main/java/org/olat/course/nodes/livestream/ui/_content/streams.html
+++ b/src/main/java/org/olat/course/nodes/livestream/ui/_content/streams.html
@@ -1,2 +1,2 @@
-$r.render("viewer")
+$r.render("viewers")
 $r.render("list")
diff --git a/src/main/java/org/olat/course/nodes/livestream/ui/_content/video.html b/src/main/java/org/olat/course/nodes/livestream/ui/_content/video.html
new file mode 100644
index 00000000000..913c8b3df55
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/livestream/ui/_content/video.html
@@ -0,0 +1,10 @@
+#if($r.isNotNull($id))
+	<div>
+		## height / width does not matter, because the size is streched responsive.
+		<p><span id="o_livestream_${id}" class="olatFlashMovieViewer" style="display:block;border:solid 1px #000;">
+			<script type="text/javascript" defer>
+				BPlayer.insertPlayer("$src","o_livestream_${id}",400,'100%',0,0,"video",undefined,true,true,false);
+			</script>
+		</span></p>
+	</div>
+#end
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/nodes/livestream/ui/_content/viewer.html b/src/main/java/org/olat/course/nodes/livestream/ui/_content/viewer.html
index 613341a015b..691b46d7395 100644
--- a/src/main/java/org/olat/course/nodes/livestream/ui/_content/viewer.html
+++ b/src/main/java/org/olat/course/nodes/livestream/ui/_content/viewer.html
@@ -1,42 +1,2 @@
-<div class="o_livestream_viewer o_block_large_bottom clearfix">
-	#if($noLiveStream)
-		<div class="o_warning">
-			$r.translate("viewer.no.stream")
-		</div>
-	#else
-		<div class="o_viewer">
-			## height / width does not matter, because the size is streched responsive.
-			<p><span id="o_livestream_${id}" class="olatFlashMovieViewer" style="display:block;border:solid 1px #000;">
-				<script type="text/javascript" defer>
-					BPlayer.insertPlayer("$src","o_livestream_${id}",400,'100%',0,0,"video",undefined,true,true,false);
-				</script>
-			</span></p>
-		</div>
-		
-		#if($title)
-			<h3>$title</h3>
-		#end
-
-		<div class="o_cal_date text-muted">
-			<i class="o_icon o_icon-fw o_icon_calendar">&nbsp;</i>
-			$date
-		</div>
-		#if($time && $time != "")
-			<div class="o_cal_time text-muted">
-				<i class="o_icon o_icon-fw o_icon_time">&nbsp;</i>
-				$time
-			</div>
-		#end
-		#if($location && !${location.isEmpty()})
-			<div class="o_cal_location text-muted">
-				<i class="o_icon o_icon-fw o_icon_home" title="$r.translateInAttribute("cal.form.location")">&nbsp;</i>
-				$r.escapeHtml($location)
-			</div>
-		#end
-		#if($description && !${description.isEmpty()})
-			<div class="o_cal_description">
-				$r.xssScan($description)
-			</div>
-		#end
-	#end
-</div>
\ No newline at end of file
+$r.render("video")
+$r.render("metadata")
diff --git a/src/main/java/org/olat/course/nodes/livestream/ui/_content/viewers.html b/src/main/java/org/olat/course/nodes/livestream/ui/_content/viewers.html
new file mode 100644
index 00000000000..9a83dc40fe0
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/livestream/ui/_content/viewers.html
@@ -0,0 +1,18 @@
+<div class="o_livestream_viewer">
+	#if($noLiveStream)
+		<div class="o_warning">
+			$r.translate("viewer.no.stream")
+		</div>
+	#else
+		$r.render("display0")
+		$r.render("display1")
+		$r.render("display2")
+		$r.render("display3")
+		$r.render("display4")
+		$r.render("display5")
+		$r.render("display6")
+		$r.render("display7")
+		$r.render("display8")
+		$r.render("display9")
+	#end
+</div>
\ No newline at end of file
-- 
GitLab