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==", "integrity": "sha512-b9usnbDGnD928gJB3LrCmxoibr3VE4U2SMo5PBuBnokWyDADTqDPXg4YpwKF1trpH+UbGp7QLicO3+aWEy0+mw==",
"dev": true "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": { "handle-thing": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", "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/platform-browser-dynamic": "~8.2.0",
"@angular/router": "~8.2.0", "@angular/router": "~8.2.0",
"bootstrap": "^4.3.1", "bootstrap": "^4.3.1",
"hammerjs": "^2.0.8",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"popper.js": "^1.15.0", "popper.js": "^1.15.0",
"rxjs": "~6.4.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", component: BrowseComponent, pathMatch: "full", resolve: { shows: LibraryResolverService } },
{ path: "browse/:library-slug", component: BrowseComponent, resolve: { shows: LibraryResolverService } }, { path: "browse/:library-slug", component: BrowseComponent, resolve: { shows: LibraryResolverService } },
{ path: "show/:show-slug", component: ShowDetailsComponent, resolve: { show: ShowResolverService } }, { 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 } { path: "**", component: NotFoundComponent }
]; ];

View File

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

View File

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

View File

@ -1,56 +1,70 @@
<div id="root"> <div id="root">
<div class="player"> <div class="player">
<video id="player" autoplay muted (click)="tooglePlayback()"> <video id="player" poster="backdrop/{{this.item.showSlug}}" autoplay muted (click)="tooglePlayback()">
<source src="/api/video/{{this.video}}" type="video/mp4" /> <source src="/api/video/{{this.item.link}}" type="video/mp4" />
</video> </video>
</div> </div>
<div class="back"> <div id="hover">
<button mat-icon-button data-toggle="tooltip" data-placement="bottom" title="Back" (click)="back()"> <div class="back">
<mat-icon>arrow_back</mat-icon> <button mat-icon-button data-toggle="tooltip" data-placement="bottom" title="Back" (click)="back()">
</button> <mat-icon>arrow_back</mat-icon>
<h5>{{this.item.showTitle}}</h5> </button>
</div> <h5>{{this.item.showTitle}}</h5>
<div class="controller container-fluid">
<div class="img">
<img src="thumb/{{this.item.showSlug}}" />
</div> </div>
<div class="content">
<h3>S{{this.item.seasonNumber}}:E{{this.item.episodeNumber}} - {{this.item.title}}</h3> <div class="controller container-fluid">
<mat-progress-bar color="accent"></mat-progress-bar> <div class="img">
<div class="buttons"> <img src="thumb/{{this.item.showSlug}}" />
<div class="left"> </div>
<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}}"> <div class="content">
<mat-icon>skip_previous</mat-icon> <h3>S{{this.item.seasonNumber}}:E{{this.item.episodeNumber}} - {{this.item.title}}</h3>
</button> <mat-progress-bar color="accent"></mat-progress-bar>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Play" id="play" (click)="tooglePlayback()"> <div class="buttons">
<mat-icon>{{this.playIcon}}</mat-icon> <div class="left">
</button> <button *ngIf="this.item.previousEpisode" mat-icon-button data-toggle="tooltip" data-placement="top" title="Previous" routerLink="/watch/{{this.item.previousEpisode}}">
<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_previous</mat-icon>
<mat-icon>skip_next</mat-icon> </button>
</button> <button mat-icon-button data-toggle="tooltip" data-placement="top" title="Play" id="play" (click)="tooglePlayback()">
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Volume"> <mat-icon>{{this.playIcon}}</mat-icon>
<mat-icon>volume_up</mat-icon> </button>
</button> <button mat-icon-button id="nextBtn" *ngIf="this.item.nextEpisode" routerLink="/watch/{{this.item.nextEpisode.link}}">
<p>00:00 / --:--</p> <mat-icon>skip_next</mat-icon>
</div>
<div class="right"> <div id="next">
<button *ngIf="this.item.audios != null" mat-icon-button data-toggle="tooltip" data-placement="top" title="Select audio track"> <div id="main">
<mat-icon>music_note</mat-icon> <img src="{{this.item.nextEpisode.thumb}}" />
</button> </div>
<button *ngIf="this.item.subtitles != null" mat-icon-button data-toggle="tooltip" data-placement="top" title="Select subtitle track"> <div id="overview">
<mat-icon>closed_caption</mat-icon> <h6>S{{this.item.nextEpisode.seasonNumber}}:E{{this.item.nextEpisode.episodeNumber}} - {{this.item.nextEpisode.title}}</h6>
</button> <p>{{this.item.nextEpisode.overview}}</p>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Cast"> </div>
<mat-icon>cast</mat-icon> </div>
</button> </button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Settings"> <button mat-icon-button data-toggle="tooltip" data-placement="top" title="Volume">
<mat-icon>settings</mat-icon> <mat-icon>volume_up</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Fullscreen" id="fullscreen" (click)="fullscreen()"> <mat-slider></mat-slider>
<mat-icon>{{fullscreenIcon}}</mat-icon> </button>
</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> </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 export class PlayerComponent implements OnInit
{ {
item: WatchItem; item: WatchItem;
video: string;
hours: number;
minutes: number;
seconds: number;
playIcon: string = "pause"; //Icon used by the play btn. playIcon: string = "pause"; //Icon used by the play btn.
fullscreenIcon: string = "fullscreen"; //Icon used by the fullscreen btn. fullscreenIcon: string = "fullscreen"; //Icon used by the fullscreen btn.
private player: HTMLVideoElement; private player: HTMLVideoElement;
constructor(private route: ActivatedRoute, private location: Location) constructor(private route: ActivatedRoute, private sanitizer: DomSanitizer, private location: Location) { }
{
this.video = this.route.snapshot.paramMap.get("item");
}
ngOnInit() ngOnInit()
{ {
document.getElementById("nav").classList.add("d-none"); 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"); console.log("Init");
} }
@ -36,9 +45,25 @@ export class PlayerComponent implements OnInit
this.player = document.getElementById("player") as HTMLVideoElement; this.player = document.getElementById("player") as HTMLVideoElement;
this.player.controls = false; 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) if (document.fullscreenElement != null)
{ {
@ -51,6 +76,8 @@ export class PlayerComponent implements OnInit
$("#fullscreen").attr("data-original-title", "Fullscreen").tooltip("show"); $("#fullscreen").attr("data-original-title", "Fullscreen").tooltip("show");
} }
}); });
$('[data-toggle="tooltip"]').tooltip();
} }
ngOnDestroy() ngOnDestroy()
@ -65,22 +92,16 @@ export class PlayerComponent implements OnInit
tooglePlayback() tooglePlayback()
{ {
let playBtn: HTMLElement = document.getElementById("play");
if (this.player.paused) if (this.player.paused)
{
this.player.play(); this.player.play();
this.playIcon = "pause"
$(playBtn).attr("data-original-title", "Pause").tooltip("show");
}
else else
{
this.player.pause(); this.player.pause();
}
this.playIcon = "play_arrow" initPlayBtn()
$(playBtn).attr("data-original-title", "Play").tooltip("show"); {
} this.playIcon = "pause";
$("#play").attr("data-original-title", "Pause").tooltip("show");
} }
fullscreen() fullscreen()
@ -90,4 +111,10 @@ export class PlayerComponent implements OnInit
else else
document.exitFullscreen(); 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 { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
import "hammerjs"
if (environment.production) { if (environment.production) {
enableProdMode(); 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; episodeNumber: number;
title: string; title: string;
thumb: string;
link: string;
overview: string; overview: string;
releaseDate; releaseDate;
runtimeInMinutes: number; runtimeInMinutes: number;

View File

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

View File

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

View File

@ -1,6 +1,7 @@
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Watch; using Kyoo.Models.Watch;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SQLite; using System.Data.SQLite;
using System.Diagnostics; using System.Diagnostics;
@ -233,7 +234,7 @@ namespace Kyoo.InternalAPI
public List<Season> GetSeasons(long showID) 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)) 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) 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)) 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;"; 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(); SQLiteDataReader reader = cmd.ExecuteReader();
if (reader.Read()) 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 else
return null; return null;
} }

View File

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

View File

@ -15,8 +15,11 @@ namespace Kyoo.Models
public long seasonNumber; public long seasonNumber;
public long episodeNumber; public long episodeNumber;
public string Title; public string Title;
public string Link;
public DateTime? ReleaseDate; public DateTime? ReleaseDate;
[JsonIgnore] public string Path; [JsonIgnore] public string Path;
public string previousEpisode;
public Episode nextEpisode;
[JsonIgnore] public VideoStream video; [JsonIgnore] public VideoStream video;
public IEnumerable<Stream> audios; public IEnumerable<Stream> audios;
@ -34,6 +37,8 @@ namespace Kyoo.Models
Title = title; Title = title;
ReleaseDate = releaseDate; ReleaseDate = releaseDate;
Path = path; 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) 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; subtitles = streams.subtitles;
return this; 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;
}
} }
} }