Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Veue 486 audience view show VOD badges when broadcast finishes #311

Closed
wants to merge 11 commits into from
4 changes: 4 additions & 0 deletions app/helpers/videos_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,8 @@ def seconds_to_time(seconds)

[seconds / 3600, seconds / 60 % 60, seconds % 60].map { |t| t.to_s.rjust(2, "0") }.join(":")
end

def video_views
current_video.video_views.count
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny note, we use a counter_cache on video views, instead of count we should be using size since itll read from the counter_cache first, and then if it cant find it will query from the database to get the count. #count will always query the database for a count.

https://blog.appsignal.com/2018/06/19/activerecords-counter-cache.html

end
end
7 changes: 7 additions & 0 deletions app/javascript/controllers/audience/header_bar_controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Controller } from "stimulus";
import { NavigationUpdate } from "controllers/broadcast/browser_controller";
import { VideoEventProcessor } from "helpers/event/event_processor";
import { showHideWhenLive } from "helpers/video_helpers";
import { StreamTypeChangedEvent } from "../audience_view_controller";

export default class HeaderBarController extends Controller {
static targets = ["addressInput"];
Expand All @@ -10,6 +12,11 @@ export default class HeaderBarController extends Controller {
VideoEventProcessor.subscribeTo("BrowserNavigation", (event) => {
this.processNavigationEvent(event.detail.data);
});

showHideWhenLive();
document.addEventListener(StreamTypeChangedEvent, () => {
showHideWhenLive();
});
}

private processNavigationEvent(navigationUpdate: NavigationUpdate) {
Expand Down
15 changes: 15 additions & 0 deletions app/javascript/controllers/audience_view_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { startMuxData } from "controllers/audience/mux_integration";
import { isProduction } from "util/environment";
import { postForm } from "util/fetch";
import { BroadcastVideoLayout } from "types/video_layout";
import { VideoState } from "types/video_state";

export const StreamTypeChangedEvent = "StreamTypeChanged";

type StreamType = "upcoming" | "live" | "vod";
export default class extends BaseController {
Expand Down Expand Up @@ -97,6 +100,10 @@ export default class extends BaseController {
}, 60 * 1000);

this.subscribeToAuthChange();

VideoEventProcessor.subscribeTo("StateChange", (event: CustomEvent) => {
this.processVideoStateChange(event.detail);
});
}

authChanged(): void {
Expand Down Expand Up @@ -161,6 +168,14 @@ export default class extends BaseController {
this.element.className = "content-area";
}

private processVideoStateChange(videoState: VideoState): void {
this.streamType = videoState.state === "finished" ? "vod" : "live";
this.data.set("stream-type", this.streamType);
if (videoState.state === "finished") {
document.dispatchEvent(new CustomEvent(StreamTypeChangedEvent));
}
}

set state(state: string) {
this.data.set("state", state);

Expand Down
2 changes: 2 additions & 0 deletions app/javascript/controllers/authentication_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import IntlTelInput from "intl-tel-input";
import "intl-tel-input/build/css/intlTelInput.min.css";
import { secureFormFetch } from "util/fetch";
import { showHideByLogin } from "helpers/authentication_helpers";
import { showHideWhenLive } from "helpers/video_helpers";

export default class extends BaseController {
static targets = [
Expand Down Expand Up @@ -97,6 +98,7 @@ export default class extends BaseController {
}
this.emitAuthChange();
showHideByLogin();
showHideWhenLive();
this.modalTarget.style.display = "none";
} else if (response.status == 200) {
// Need to show the modal again
Expand Down
10 changes: 10 additions & 0 deletions app/javascript/controllers/chat/send_message_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ import { Controller } from "stimulus";
import { getChannelId } from "helpers/channel_helpers";
import { currentUserId } from "helpers/authentication_helpers";
import { displayChatMessage } from "helpers/chat_helpers";
import { showHideWhenLive } from "helpers/video_helpers";
import { StreamTypeChangedEvent } from "../audience_view_controller";

export default class extends Controller {
static targets = ["messageInput"];

element: HTMLElement;
private messageInputTarget!: HTMLElement;
private lastMessageFromUserId: string;
private userId: string;

connect(): void {
const bodyDataset = document.body.dataset;

this.messageInputTarget.addEventListener("focus", () => {
bodyDataset["keyboard"] = "visible";
setTimeout(function () {
Expand All @@ -26,13 +30,19 @@ export default class extends Controller {
);

this.userId = currentUserId();

showHideWhenLive();
document.addEventListener(StreamTypeChangedEvent, () => {
showHideWhenLive();
});
}

fallBackContentEditable(): void {
if (!this.messageInputTarget.isContentEditable) {
this.messageInputTarget.contentEditable = "true";
}
}

chatBoxKeyDown(event: KeyboardEvent): void {
if (!event.shiftKey && event.key === "Enter") {
event.preventDefault();
Expand Down
7 changes: 4 additions & 3 deletions app/javascript/helpers/authentication_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ function hideLoginElements(): void {
});
}

function visibilityOfDataElement(
export function visibilityOfDataElement(
dataElementName: HTMLElement,
hidden: boolean
hidden: boolean,
visibility: string = "block"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 if you do visibility = "block" I believe the compiler can auto infer that it should be a string!

): void {
dataElementName.setAttribute(
"style",
hidden ? "display: none;" : "display: block;"
hidden ? "display: none;" : `display: ${visibility};`
);
}
1 change: 1 addition & 0 deletions app/javascript/helpers/event/live_event_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default class LiveEventManager implements EventManagerInterface {
await this.reload();
}
VideoEventProcessor.addEvent(data);

if (data.viewers) {
document.dispatchEvent(
new CustomEvent(ViewerCountUpdateEvent, { detail: data.viewers })
Expand Down
40 changes: 40 additions & 0 deletions app/javascript/helpers/video_helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
visibilityOfDataElement,
currentUserId,
} from "./authentication_helpers";

export function getVideoId(): string {
return document
.querySelector("*[data-video-id]")
Expand Down Expand Up @@ -30,3 +35,38 @@ export function getVideoVisibility(): Visibility {
function getVideoVisibilityElement(): HTMLElement {
return document.querySelector("*[data-video-visibility]");
}

export function showHideWhenLive() {
showStreamElements();
hideStreamElements();
}

export function currentStreamType(): string | undefined {
const element = document.querySelector("*[data-audience-view-stream-type]");
return element?.getAttribute("data-audience-view-stream-type");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always forget we use TS and can use optional chaining!!

}

function showStreamElements() {
let vodStream = currentStreamType() !== "vod";
document
.querySelectorAll("*[data-show-when-live]")
.forEach((element: HTMLElement) => {
const showOnAudience = element.dataset.hasOwnProperty("showOnAudience");
vodStream = showOnAudience ? vodStream : vodStream && !!currentUserId();

visibilityOfDataElement(element, !vodStream, "flex");
});
}

function hideStreamElements() {
let vodStream = currentStreamType() === "vod";

document
.querySelectorAll("*[data-show-when-vod]")
.forEach((element: HTMLElement) => {
const showOnAudience = element.dataset.hasOwnProperty("showOnAudience");
vodStream = showOnAudience ? vodStream : vodStream && !!currentUserId();

visibilityOfDataElement(element, !vodStream, "flex");
});
}
6 changes: 6 additions & 0 deletions app/javascript/images/vod.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 2 additions & 4 deletions app/javascript/style/chat.scss
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,13 @@
}

.stream-end {
border-top: 1px solid color.$neutral-accents;
padding: 10px;

&--content {
// layouts
display: flex;
justify-content: center;
align-items: center;
padding: 7px 0;
padding: 7px;
width: 100%;

// styles
border-radius: 10px;
Expand Down
5 changes: 5 additions & 0 deletions app/javascript/types/video_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type VideoState = {
state: string;
type: string;
timecodeMs: number;
};
2 changes: 1 addition & 1 deletion app/models/concerns/video_states.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ module VideoStates

event :finish do
after do
send_ifttt! "#{user.display_name} stopped streaming" if visibility.eql?("public")
after_broadcast_finished
end

transitions from: %i[live paused pending ended starting], to: :finished
Expand Down
10 changes: 8 additions & 2 deletions app/models/video.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def change_playback_id(new_playback_id)
self.hls_url = "https://stream.mux.com/#{new_playback_id}.m3u8"
end

def transition_audience_to_live
def transition_state_for_audience
SseBroadcaster.broadcast(channel.id, {state: state, type: "StateChange", timecodeMs: 0})
end

Expand Down Expand Up @@ -150,14 +150,20 @@ def last_user_join_time
private

def after_go_live
transition_audience_to_live
transition_state_for_audience

return unless visibility.eql?("public")

send_ifttt!("#{user.display_name} went live!")
send_broadcast_start_text!
end

def after_broadcast_finished
transition_state_for_audience

send_ifttt!("#{user.display_name} stopped streaming") if visibility.eql?("public")
end

def send_ifttt!(message)
url = Router.channel_video_url(channel, self)
IfThisThenThatJob.perform_later(message: message, url: url)
Expand Down
78 changes: 48 additions & 30 deletions app/views/channels/videos/partials/_header.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,51 @@
"target": "audience--header-bar.addressInput"
}
}
- if current_video.live?
.widget
.icon
= svg_tag "view"
#active-viewers{
"data": {
"controller": "live-viewer-count",
"target": "live-viewer-count.counter"
}
}= current_video.active_viewers_count
.widget.live
.widget-live__desktop
LIVE
.widget-live__mobile
= svg_tag "live"
- if current_video.finished?
- video_views = current_video.video_views.count
.widget{"data-views": video_views}
.icon
= svg_tag "view"
#active-viewers
= video_views
.replay-badge
.replay-badge__text
REPLAY
= svg_tag "no-stream", class: "replay-badge__image"
.badge-message
.badge-message__body
This stream happened on
= current_video.created_at.strftime("%B %d, %Y")
.widget{
data: {
"show-when-live": true,
"show-on-audience": true,
}
}
Comment on lines +10 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ hmmm...these repeated data-* attributes dont feel great.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ParamagicDev yeah, but required for show/hide. I think otherwise we need to fetch Markup from backend. 🤔

.icon
= svg_tag "view"
#active-viewers{
"data": {
"controller": "live-viewer-count",
"target": "live-viewer-count.counter"
}
}= current_video.active_viewers_count
.widget.live{
data: {
"show-when-live": true,
"show-on-audience": true
}
}
.widget-live__desktop
LIVE
.widget-live__mobile
= svg_tag "live"
.widget{
data: {
views: video_views,
"show-when-vod": true,
"show-on-audience": "true"
}
}
.icon
= svg_tag "view"
#active-viewers
= video_views
.replay-badge{
data: {
"show-when-vod": true,
"show-on-audience": true
}
}
.replay-badge__text
REPLAY
= svg_tag "no-stream", class: "replay-badge__image"
.badge-message
.badge-message__body
This stream happened on
= current_video.created_at.strftime("%B %d, %Y")
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
= svg_tag 'user-icon-dark'
%span
= current_channel.followers.size

- if user_signed_in?
- if current_user.follows?(current_channel)
%button.unfollow-btn{data: { action: "click->streamer-profile#unfollow" }} Following
Expand Down
Loading