Working on the player.

This commit is contained in:
Zoe Roux 2019-09-05 23:20:59 +02:00
parent 21002fea4a
commit 44c8451735
17 changed files with 248 additions and 78 deletions

View File

@ -4358,6 +4358,11 @@
"integrity": "sha512-b9usnbDGnD928gJB3LrCmxoibr3VE4U2SMo5PBuBnokWyDADTqDPXg4YpwKF1trpH+UbGp7QLicO3+aWEy0+mw==",
"dev": true
},
"hammerjs": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
"integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE="
},
"handle-thing": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",

View File

@ -22,6 +22,7 @@
"@angular/platform-browser-dynamic": "~8.2.0",
"@angular/router": "~8.2.0",
"bootstrap": "^4.3.1",
"hammerjs": "^2.0.8",
"jquery": "^3.4.1",
"popper.js": "^1.15.0",
"rxjs": "~6.4.0",

View File

@ -14,7 +14,7 @@ const routes: Routes = [
{ path: "browse", component: BrowseComponent, pathMatch: "full", resolve: { shows: LibraryResolverService } },
{ path: "browse/:library-slug", component: BrowseComponent, resolve: { shows: LibraryResolverService } },
{ path: "show/:show-slug", component: ShowDetailsComponent, resolve: { show: ShowResolverService } },
{ path: "watch/:item", component: PlayerComponent, resolve: { item: StreamResolverService }, runGuardsAndResolvers: "always" },
{ path: "watch/:item", component: PlayerComponent, resolve: { item: StreamResolverService } },
{ path: "**", component: NotFoundComponent }
];

View File

@ -5,6 +5,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatSliderModule } from '@angular/material/slider';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@ -36,7 +37,8 @@ import { ShowDetailsComponent } from './show-details/show-details.component';
MatButtonModule,
MatIconModule,
MatSelectModule,
MatMenuModule
MatMenuModule,
MatSliderModule
],
providers: [],
bootstrap: [AppComponent]

View File

@ -56,9 +56,4 @@ export class EpisodesListComponent implements OnInit
return offset;
}
play(episode: Episode)
{
}
}

View File

@ -1,56 +1,70 @@
<div id="root">
<div class="player">
<video id="player" autoplay muted (click)="tooglePlayback()">
<source src="/api/video/{{this.video}}" type="video/mp4" />
<video id="player" poster="backdrop/{{this.item.showSlug}}" autoplay muted (click)="tooglePlayback()">
<source src="/api/video/{{this.item.link}}" type="video/mp4" />
</video>
</div>
<div class="back">
<button mat-icon-button data-toggle="tooltip" data-placement="bottom" title="Back" (click)="back()">
<mat-icon>arrow_back</mat-icon>
</button>
<h5>{{this.item.showTitle}}</h5>
</div>
<div class="controller container-fluid">
<div class="img">
<img src="thumb/{{this.item.showSlug}}" />
<div id="hover">
<div class="back">
<button mat-icon-button data-toggle="tooltip" data-placement="bottom" title="Back" (click)="back()">
<mat-icon>arrow_back</mat-icon>
</button>
<h5>{{this.item.showTitle}}</h5>
</div>
<div class="content">
<h3>S{{this.item.seasonNumber}}:E{{this.item.episodeNumber}} - {{this.item.title}}</h3>
<mat-progress-bar color="accent"></mat-progress-bar>
<div class="buttons">
<div class="left">
<button *ngIf="this.item.episodeNumber != 1" mat-icon-button data-toggle="tooltip" data-placement="top" title="Previous" routerLink="/watch/{{this.item.showSlug}}-s{{this.item.seasonNumber}}e{{this.item.episodeNumber - 1}}">
<mat-icon>skip_previous</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Play" id="play" (click)="tooglePlayback()">
<mat-icon>{{this.playIcon}}</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Next" routerLink="/watch/{{this.item.showSlug}}-s{{this.item.seasonNumber}}e{{this.item.episodeNumber + 1}}">
<mat-icon>skip_next</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Volume">
<mat-icon>volume_up</mat-icon>
</button>
<p>00:00 / --:--</p>
</div>
<div class="right">
<button *ngIf="this.item.audios != null" mat-icon-button data-toggle="tooltip" data-placement="top" title="Select audio track">
<mat-icon>music_note</mat-icon>
</button>
<button *ngIf="this.item.subtitles != null" mat-icon-button data-toggle="tooltip" data-placement="top" title="Select subtitle track">
<mat-icon>closed_caption</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Cast">
<mat-icon>cast</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Settings">
<mat-icon>settings</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Fullscreen" id="fullscreen" (click)="fullscreen()">
<mat-icon>{{fullscreenIcon}}</mat-icon>
</button>
<div class="controller container-fluid">
<div class="img">
<img src="thumb/{{this.item.showSlug}}" />
</div>
<div class="content">
<h3>S{{this.item.seasonNumber}}:E{{this.item.episodeNumber}} - {{this.item.title}}</h3>
<mat-progress-bar color="accent"></mat-progress-bar>
<div class="buttons">
<div class="left">
<button *ngIf="this.item.previousEpisode" mat-icon-button data-toggle="tooltip" data-placement="top" title="Previous" routerLink="/watch/{{this.item.previousEpisode}}">
<mat-icon>skip_previous</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Play" id="play" (click)="tooglePlayback()">
<mat-icon>{{this.playIcon}}</mat-icon>
</button>
<button mat-icon-button id="nextBtn" *ngIf="this.item.nextEpisode" routerLink="/watch/{{this.item.nextEpisode.link}}">
<mat-icon>skip_next</mat-icon>
<div id="next">
<div id="main">
<img src="{{this.item.nextEpisode.thumb}}" />
</div>
<div id="overview">
<h6>S{{this.item.nextEpisode.seasonNumber}}:E{{this.item.nextEpisode.episodeNumber}} - {{this.item.nextEpisode.title}}</h6>
<p>{{this.item.nextEpisode.overview}}</p>
</div>
</div>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Volume">
<mat-icon>volume_up</mat-icon>
<mat-slider></mat-slider>
</button>
<p>{{this.minutes | number: '2.0-0'}}:{{this.seconds | number: '2.0-0'}} / --:--</p>
</div>
<div class="right">
<button *ngIf="this.item.audios != null" mat-icon-button data-toggle="tooltip" data-placement="top" title="Select audio track">
<mat-icon>music_note</mat-icon>
</button>
<button *ngIf="this.item.subtitles != null" mat-icon-button data-toggle="tooltip" data-placement="top" title="Select subtitle track">
<mat-icon>closed_caption</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Cast">
<mat-icon>cast</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Settings">
<mat-icon>settings</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Fullscreen" id="fullscreen" (click)="fullscreen()">
<mat-icon>{{fullscreenIcon}}</mat-icon>
</button>
</div>
</div>
</div>
</div>

View File

@ -114,3 +114,62 @@
}
}
}
#nextBtn
{
position: relative;
&:hover
{
#next
{
display: flex;
}
}
#next
{
position: absolute;
left: 0;
bottom: 100%;
display: none;
background-color: #212121;
white-space: normal;
line-height: normal;
cursor: default;
height: 150px;
#main
{
width: auto;
height: 100%;
flex-shrink: 0;
flex-grow: 0;
> img
{
width: auto;
height: 100%;
}
}
#overview
{
padding: 1%;
width: 50%;
min-width: 300px;
flex-shrink: 0;
display: flex;
flex-direction: column;
> p
{
text-align: justify;
font-weight: 300;
overflow: hidden;
margin: 0;
}
}
}
}

View File

@ -12,22 +12,31 @@ import { Location } from "@angular/common";
export class PlayerComponent implements OnInit
{
item: WatchItem;
video: string;
hours: number;
minutes: number;
seconds: number;
playIcon: string = "pause"; //Icon used by the play btn.
fullscreenIcon: string = "fullscreen"; //Icon used by the fullscreen btn.
private player: HTMLVideoElement;
constructor(private route: ActivatedRoute, private location: Location)
{
this.video = this.route.snapshot.paramMap.get("item");
}
constructor(private route: ActivatedRoute, private sanitizer: DomSanitizer, private location: Location) { }
ngOnInit()
{
document.getElementById("nav").classList.add("d-none");
this.item = this.route.snapshot.data.item;
this.route.data.subscribe((data) =>
{
this.item = data.item;
if (this.player)
{
this.player.load();
this.initPlayBtn();
}
});
console.log("Init");
}
@ -36,9 +45,25 @@ export class PlayerComponent implements OnInit
this.player = document.getElementById("player") as HTMLVideoElement;
this.player.controls = false;
$('[data-toggle="tooltip"]').tooltip();
this.player.onplay = () =>
{
this.initPlayBtn();
}
document.addEventListener("fullscreenchange", (event) =>
this.player.onpause = () =>
{
this.playIcon = "play_arrow"
$("#play").attr("data-original-title", "Play").tooltip("show");
}
this.player.ontimeupdate = () =>
{
this.seconds = Math.round(this.player.currentTime % 60);
this.minutes = Math.round(this.player.currentTime / 60);
};
document.addEventListener("fullscreenchange", () =>
{
if (document.fullscreenElement != null)
{
@ -51,6 +76,8 @@ export class PlayerComponent implements OnInit
$("#fullscreen").attr("data-original-title", "Fullscreen").tooltip("show");
}
});
$('[data-toggle="tooltip"]').tooltip();
}
ngOnDestroy()
@ -65,22 +92,16 @@ export class PlayerComponent implements OnInit
tooglePlayback()
{
let playBtn: HTMLElement = document.getElementById("play");
if (this.player.paused)
{
this.player.play();
this.playIcon = "pause"
$(playBtn).attr("data-original-title", "Pause").tooltip("show");
}
else
{
this.player.pause();
}
this.playIcon = "play_arrow"
$(playBtn).attr("data-original-title", "Play").tooltip("show");
}
initPlayBtn()
{
this.playIcon = "pause";
$("#play").attr("data-original-title", "Pause").tooltip("show");
}
fullscreen()
@ -90,4 +111,10 @@ export class PlayerComponent implements OnInit
else
document.exitFullscreen();
}
getThumb(url: string)
{
return this.sanitizer.bypassSecurityTrustStyle("url(" + url + ")");
}
}

View File

@ -3,6 +3,7 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import "hammerjs"
if (environment.production) {
enableProdMode();

View File

@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=episode.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"episode.js","sourceRoot":"","sources":["episode.ts"],"names":[],"mappings":""}

View File

@ -2,6 +2,8 @@ export interface Episode
{
episodeNumber: number;
title: string;
thumb: string;
link: string;
overview: string;
releaseDate;
runtimeInMinutes: number;

View File

@ -1,11 +1,19 @@
import { Episode } from "./episode";
export interface WatchItem
{
showTitle: string;
showSlug: string;
seasonNumber: number;
episodeNumber: number;
video: string;
title: string;
link: string;
releaseDate;
previousEpisode: string;
nextEpisode: Episode;
audio: Stream[];
subtitles: Stream[];
}

View File

@ -14,6 +14,7 @@ namespace Kyoo.InternalAPI
List<People> GetPeople(long showID);
List<Genre> GetGenreForShow(long showID);
List<Season> GetSeasons(long showID);
int GetSeasonCount(string showSlug, long seasonNumber);
(VideoStream video, List<Stream> audios, List<Stream> subtitles) GetStreams(long episodeID);
//Public read
@ -22,7 +23,7 @@ namespace Kyoo.InternalAPI
Season GetSeason(string showSlug, long seasonNumber);
List<Episode> GetEpisodes(string showSlug, long seasonNumber);
Episode GetEpisode(string showSlug, long seasonNumber, long episodeNumber);
WatchItem GetWatchItem(string showSlug, long seasonNumber, long episodeNumber);
WatchItem GetWatchItem(string showSlug, long seasonNumber, long episodeNumber, bool complete = true);
People GetPeopleBySlug(string slug);
Genre GetGenreBySlug(string slug);
Studio GetStudioBySlug(string slug);

View File

@ -1,6 +1,7 @@
using Kyoo.Models;
using Kyoo.Models.Watch;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Diagnostics;
@ -233,7 +234,7 @@ namespace Kyoo.InternalAPI
public List<Season> GetSeasons(long showID)
{
string query = "SELECT * FROM seasons WHERE showID = $showID;";
string query = "SELECT * FROM seasons WHERE showID = $showID ORDER BY seasonNumber;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
@ -266,9 +267,23 @@ namespace Kyoo.InternalAPI
}
}
public int GetSeasonCount(string showSlug, long seasonNumber)
{
string query = "SELECT count(episodes.id) FROM episodes JOIN shows ON shows.id = episodes.showID WHERE shows.slug = $showSlug AND episodes.seasonNumber = $seasonNumber;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$showSlug", showSlug);
cmd.Parameters.AddWithValue("$seasonNumber", seasonNumber);
int count = Convert.ToInt32(cmd.ExecuteScalar());
return count;
}
}
public List<Episode> GetEpisodes(string showSlug, long seasonNumber)
{
string query = "SELECT * FROM episodes JOIN shows ON shows.id = episodes.showID WHERE shows.slug = $showSlug AND episodes.seasonNumber = $seasonNumber;";
string query = "SELECT * FROM episodes JOIN shows ON shows.id = episodes.showID WHERE shows.slug = $showSlug AND episodes.seasonNumber = $seasonNumber ORDER BY episodeNumber;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
@ -303,7 +318,7 @@ namespace Kyoo.InternalAPI
}
}
public WatchItem GetWatchItem(string showSlug, long seasonNumber, long episodeNumber)
public WatchItem GetWatchItem(string showSlug, long seasonNumber, long episodeNumber, bool complete = true)
{
string query = "SELECT episodes.id, shows.title as showTitle, shows.slug as showSlug, seasonNumber, episodeNumber, episodes.title, releaseDate, episodes.path FROM episodes JOIN shows ON shows.id = episodes.showID WHERE shows.slug = $showSlug AND episodes.seasonNumber = $seasonNumber AND episodes.episodeNumber = $episodeNumber;";
@ -315,7 +330,12 @@ namespace Kyoo.InternalAPI
SQLiteDataReader reader = cmd.ExecuteReader();
if (reader.Read())
return WatchItem.FromReader(reader).SetStreams(this);
{
if (complete)
return WatchItem.FromReader(reader).SetStreams(this).SetPrevious(this).SetNext(this);
else
return WatchItem.FromReader(reader);
}
else
return null;
}

View File

@ -21,6 +21,7 @@ namespace Kyoo.Models
[JsonIgnore] public string ImgPrimary;
public string ExternalIDs;
public string Link; //Used only on the player
public string Thumb; //Used in the API response only
@ -77,6 +78,7 @@ namespace Kyoo.Models
public Episode SetThumb(string showSlug)
{
Thumb = "thumb/" + showSlug + "/s" + seasonNumber + "/e" + episodeNumber;
Link = showSlug + "-s" + seasonNumber + "e" + episodeNumber;
return this;
}
}

View File

@ -15,8 +15,11 @@ namespace Kyoo.Models
public long seasonNumber;
public long episodeNumber;
public string Title;
public string Link;
public DateTime? ReleaseDate;
[JsonIgnore] public string Path;
public string previousEpisode;
public Episode nextEpisode;
[JsonIgnore] public VideoStream video;
public IEnumerable<Stream> audios;
@ -34,6 +37,8 @@ namespace Kyoo.Models
Title = title;
ReleaseDate = releaseDate;
Path = path;
Link = ShowSlug + "-s" + seasonNumber + "e" + episodeNumber;
}
public WatchItem(long episodeID, string showTitle, string showSlug, long seasonNumber, long episodeNumber, string title, DateTime? releaseDate, string path, Stream[] audios, Stream[] subtitles) : this(episodeID, showTitle, showSlug, seasonNumber, episodeNumber, title, releaseDate, path)
@ -62,5 +67,29 @@ namespace Kyoo.Models
subtitles = streams.subtitles;
return this;
}
public WatchItem SetPrevious(ILibraryManager libraryManager)
{
long lastEp = episodeNumber - 1;
if(lastEp > 0)
previousEpisode = ShowSlug + "-s" + seasonNumber + "e" + lastEp;
else if(seasonNumber > 1)
{
int seasonCount = libraryManager.GetSeasonCount(ShowSlug, seasonNumber - 1);
previousEpisode = ShowSlug + "-s" + (seasonNumber - 1) + "e" + seasonCount;
}
return this;
}
public WatchItem SetNext(ILibraryManager libraryManager)
{
long seasonCount = libraryManager.GetSeasonCount(ShowSlug, seasonNumber);
if (episodeNumber >= seasonCount)
nextEpisode = libraryManager.GetEpisode(ShowSlug, seasonNumber + 1, 1);
else
nextEpisode = libraryManager.GetEpisode(ShowSlug, seasonNumber, episodeNumber + 1);
return this;
}
}
}