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

Misc fixes #107

Merged
merged 14 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,6 @@ import './components/maincontroller';
import './css/glyphicons.css';
import './css/jellyfin.css';

const senders = cast.framework.CastReceiverContext.getInstance().getSenders();
const id =
senders.length !== 0 && senders[0].id
? senders[0].id
: new Date().getTime();

window.deviceInfo = {
deviceId: id,
deviceName: 'Google Cast',
versionNumber: RECEIVERVERSION
};

window.mediaElement = document.getElementById('video-player');

window.playlist = [];
window.currentPlaylistIndex = -1;
window.repeatMode = RepeatMode.RepeatNone;
34 changes: 20 additions & 14 deletions src/components/codecSupportHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,28 @@ export function getMaxBitrateSupport(): number {
/**
* Get the max supported video width the active Cast device supports.
*
* @param deviceId - Cast device id.
* @returns Max supported width.
*/
export function getMaxWidthSupport(deviceId: number): number {
switch (deviceId) {
case deviceIds.ULTRA:
case deviceIds.CCGTV:
return 3840;
case deviceIds.GEN1AND2:
case deviceIds.GEN3:
return 1920;
case deviceIds.NESTHUBANDMAX:
return 1280;
}

return 0;
export function getMaxWidthSupport(): number {
// with HLS, it will produce a manifest error if we
// send any stream larger than the screen size...
return window.innerWidth;
Copy link

@ownbee ownbee Oct 31, 2021

Choose a reason for hiding this comment

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

This causes videos to be transcodeded even though they can be direct played (setups: CCUltra/4K-TV and CCGen3/FHD-TV). window.innerWidth was 1280 when I tried with gen 2/3 chromecast and a FHD TV. 1920 would be the preferred MaxWidth in that case.

Maybe only use window.innerWidth when HLS stream?

Copy link
Contributor

Choose a reason for hiding this comment

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

@ownbee what should the value be if it's not an HLS stream (instead of window.innerWidth) ?

Copy link
Contributor

Choose a reason for hiding this comment

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

How does this look?

Copy link

Choose a reason for hiding this comment

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

@tnyeanderson I think they should be clamped depending on CC-type as it was before. For direct play I think the CC/TV is able to automatically scale if the resolution is too big for the screen (it has worked before). For HLS I have no clue how to solve it.

I fail to see how h264 coding has anything to do with MaxWidthSupport. If I play a 1920x1080 video with h264 coding on my FHD TV (1920x1080) do I get the window.innerWidth constraint? If so, I would say this is not that great since 95% of my movies are in that format and would be transcoded to lower resolution and put a lot of load on my server.

Problem is, I think, is that the jellyfin server is the one that decides if transcoding is needed and how to stream depending on the constraints chromecast-app reports. Which means this logic suits best to be in the jellyfin server or if we could find something better than window.innerWidth or if we could tell it "1920x1080 is okay but if you are going to transcode for other reasons, use window.innerWidth".

I'll try find time to test if this works and how window.innerWidth behaves this weekend but I can't make a promise.

Thanks for your effort trying to get this in! Really appreciated!

Copy link

@ownbee ownbee Mar 25, 2022

Choose a reason for hiding this comment

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

Found some relevant info regarding this: https://stackoverflow.com/a/47851190

If we cannot find a better alternative to window.innerWidth that does not involve css, I think all window.innerWidthstuff should be dropped completely and maybe added in a separate PR that focus on HLS.


// mkv playback can use the device limitations.
// The devices are capable of decoding and downscaling,
// they just refuse to do it with HLS. This increases
// the rate of direct playback.
//switch (deviceId) {
// case deviceIds.ULTRA:
// case deviceIds.CCGTV:
// return 3840;
// case deviceIds.GEN1AND2:
// case deviceIds.GEN3:
// return 1920;
// case deviceIds.NESTHUBANDMAX:
// return 1280;
//}
//return 0;
}

/**
Expand Down
25 changes: 9 additions & 16 deletions src/components/commandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ import {

import { reportPlaybackProgress } from './jellyfinActions';

import { playbackManager } from './playbackManager';
import { PlaybackManager } from './playbackManager';

import { DocumentManager } from './documentManager';

export abstract class CommandHandler {
private static playerManager: framework.PlayerManager;
private static playbackManager: playbackManager;
private static supportedCommands: SupportedCommands = {
DisplayContent: CommandHandler.displayContentHandler,
Identify: CommandHandler.IdentifyHandler,
Expand All @@ -52,12 +51,8 @@ export abstract class CommandHandler {
VolumeUp: CommandHandler.VolumeUpHandler
};

static configure(
playerManager: framework.PlayerManager,
playbackManager: playbackManager
): void {
static configure(playerManager: framework.PlayerManager): void {
this.playerManager = playerManager;
this.playbackManager = playbackManager;
}

static playNextHandler(data: DataMessage): void {
Expand Down Expand Up @@ -89,23 +84,20 @@ export abstract class CommandHandler {
}

static displayContentHandler(data: DataMessage): void {
if (!this.playbackManager.isPlaying()) {
if (!PlaybackManager.isPlaying()) {
DocumentManager.showItemId((<DisplayRequest>data.options).ItemId);
}
}

static nextTrackHandler(): void {
if (
window.playlist &&
window.currentPlaylistIndex < window.playlist.length - 1
) {
this.playbackManager.playNextItem({}, true);
if (PlaybackManager.hasNextItem()) {
PlaybackManager.playNextItem({}, true);
}
}

static previousTrackHandler(): void {
if (window.playlist && window.currentPlaylistIndex > 0) {
this.playbackManager.playPreviousItem({});
if (PlaybackManager.hasPrevItem()) {
PlaybackManager.playPreviousItem({});
}
}

Expand Down Expand Up @@ -138,7 +130,8 @@ export abstract class CommandHandler {
}

static IdentifyHandler(): void {
if (!this.playbackManager.isPlaying()) {
if (!PlaybackManager.isPlaying()) {
DocumentManager.setAppStatus('waiting');
Copy link
Member

Choose a reason for hiding this comment

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

Can't we make these app status an enum? DocumentManager seems a good place to declare it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree

Copy link
Contributor

Choose a reason for hiding this comment

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

Fixed in hawken93#228

Copy link

Choose a reason for hiding this comment

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

When I tried latest master on jellyfin/jellyfin and jellyfin/jellyfin-web, The identify command was sent from the client while buffering (after load). isPlaying() returned false here and the receiver went back to "waiting" state (i.e. "welcome screen") while the video played behind it. Got the same behavior every time I tried. When I removed these actions, it works as expected.

Should this logic really be in the identify command handler? For me, this is looks like side effect. I think more robust guards than isPlaying check must be in place if this is kept.

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think of this solution?

Copy link

Choose a reason for hiding this comment

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

Looks promising and I am happy with it if it works.

DocumentManager.startBackdropInterval();
} else {
// When a client connects send back the initial device state (volume etc) via a playbackstop message
Expand Down
2 changes: 1 addition & 1 deletion src/components/deviceprofileBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ function getCodecProfiles(): Array<CodecProfile> {

CodecProfiles.push(aacConditions);

const maxWidth: number = getMaxWidthSupport(currentDeviceId);
const maxWidth: number = getMaxWidthSupport();
const h26xLevel: number = getH26xLevelSupport(currentDeviceId);
const h26xProfile: string = getH26xProfileSupport(currentDeviceId);

Expand Down
58 changes: 34 additions & 24 deletions src/components/jellyfinActions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
getSenderReportingData,
resetPlaybackScope,
extend,
broadcastToMessageBus
} from '../helpers';

Expand Down Expand Up @@ -199,12 +198,22 @@ export function pingTranscoder(
*/
export function load(
$scope: GlobalScope,
customData: PlaybackProgressInfo,
customData: any,
Copy link
Member

Choose a reason for hiding this comment

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

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this type was never actually PlaybackProgressInfo. The scripting throws other properties at it as well. That has to be resolved somehow first, but this looks to be a major refactor on its own

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah I remember. This had to be done because I removed the silly extend() function.
Since we now explicitly address the properties in an object, it cannot pretend to be something it isn't anymore :)

Copy link
Contributor

Choose a reason for hiding this comment

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

So is this staying as-is?

serverItem: BaseItemDto
): void {
resetPlaybackScope($scope);

extend($scope, customData);
// These are set up in maincontroller.createMediaInformation
$scope.playSessionId = customData.playSessionId;
$scope.audioStreamIndex = customData.audioStreamIndex;
$scope.subtitleStreamIndex = customData.subtitleStreamIndex;
$scope.startPositionTicks = customData.startPositionTicks;
$scope.canSeek = customData.canSeek;
$scope.itemId = customData.itemId;
$scope.liveStreamId = customData.liveStreamId;
$scope.mediaSourceId = customData.mediaSourceId;
$scope.playMethod = customData.playMethod;
$scope.runtimeTicks = customData.runtimeTicks;

$scope.item = serverItem;

Expand All @@ -230,7 +239,7 @@ export function play($scope: GlobalScope): void {
DocumentManager.getAppStatus() == 'audio'
) {
setTimeout(() => {
window.mediaManager.play();
window.playerManager.play();

if ($scope.mediaType == 'Audio') {
DocumentManager.setAppStatus('audio');
Expand All @@ -241,15 +250,6 @@ export function play($scope: GlobalScope): void {
}
}

/**
* Don't actually stop, just show the idle view after 20ms
*/
export function stop(): void {
setTimeout(() => {
DocumentManager.setAppStatus('waiting');
}, 20);
}

/**
* @param item
* @param maxBitrate
Expand Down Expand Up @@ -369,11 +369,14 @@ export async function getDownloadSpeed(byteSize: number): Promise<number> {

const now = new Date().getTime();

await JellyfinApi.authAjax(path, {
const response = await JellyfinApi.authAjax(path, {
timeout: 5000,
type: 'GET'
});

// Force javascript to download the whole response before calculating bitrate
await response.blob();

const responseTimeSeconds = (new Date().getTime() - now) / 1000;
const bytesPerSecond = byteSize / responseTimeSeconds;
const bitrate = Math.round(bytesPerSecond * 8);
Expand All @@ -383,22 +386,29 @@ export async function getDownloadSpeed(byteSize: number): Promise<number> {

/**
* Function to detect the bitrate.
* It first tries 1MB and if bitrate is above 1Mbit/s it tries again with 2.4MB.
* It starts at 500kB and doubles it every time it takes under 2s, for max 10MB.
* This should get an accurate bitrate relatively fast on any connection
*
* @param numBytes - Number of bytes to start with, default 500k
* @returns bitrate in bits/s
*/
export async function detectBitrate(): Promise<number> {
// First try a small amount so that we don't hang up their mobile connection
export async function detectBitrate(numBytes = 500000): Promise<number> {
// Jellyfin has a 10MB limit on the test size
const byteLimit = 10000000;

let bitrate = await getDownloadSpeed(1000000);

if (bitrate < 1000000) {
return Math.round(bitrate * 0.8);
if (numBytes > byteLimit) {
numBytes = byteLimit;
}

bitrate = await getDownloadSpeed(2400000);
const bitrate = await getDownloadSpeed(numBytes);

return Math.round(bitrate * 0.8);
if (bitrate * (2 / 8.0) < numBytes || numBytes >= byteLimit) {
// took > 2s, or numBytes hit the limit
return Math.round(bitrate * 0.8);
} else {
// If that produced a fairly high speed, try again with a larger size to get a more accurate result
return await detectBitrate(numBytes * 2);
}
}

/**
Expand All @@ -409,7 +419,7 @@ export async function detectBitrate(): Promise<number> {
*/
export function stopActiveEncodings($scope: GlobalScope): Promise<void> {
const options = {
deviceId: window.deviceInfo.deviceId,
deviceId: JellyfinApi.deviceId,
PlaySessionId: undefined
};

Expand Down
39 changes: 33 additions & 6 deletions src/components/jellyfinApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,55 @@ export abstract class JellyfinApi {
// Address of server
public static serverAddress: string | null = null;

// device name
public static deviceName = 'Google Cast';

// unique id
public static deviceId = '';

// version
public static versionNumber = RECEIVERVERSION;

public static setServerInfo(
userId: string,
accessToken: string,
serverAddress: string
serverAddress: string,
receiverName: string
): void {
console.debug(
`JellyfinApi.setServerInfo: user:${userId}, token:${accessToken}, server:${serverAddress}`
`JellyfinApi.setServerInfo: user:${userId}, token:${accessToken}, ` +
`server:${serverAddress}, name:${receiverName}`
);
this.userId = userId;
this.accessToken = accessToken;
this.serverAddress = serverAddress;

// remove special characters from the receiver name
receiverName = receiverName.replace(/[^\w\s]/gi, '');
Copy link

@ownbee ownbee Oct 28, 2021

Choose a reason for hiding this comment

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

Move inside null-check below, I got null-exception here when I tried it.

Edit: Android app seem to send null receiverName while Chrome browser sets this to non-null.

Copy link
Contributor

Choose a reason for hiding this comment

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


if (receiverName) {
this.deviceName = receiverName;
// deviceId just needs to be unique-ish
this.deviceId = btoa(receiverName);
Copy link
Member

Choose a reason for hiding this comment

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

This is nitpicky but why not generate an UUID or something else? a base64 representation of the device's name is not exactly an id.

We can also use sessionId from here: https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.system.ApplicationData

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're probably right.

This code has been refactored from maincontroller or stable, don't remember which exactly.

It fixes an error in execution order where nightly will register with the server before the device identity has been set, so after these properties are set it would identify itself differently and hence leave ghost entries in the server device list.

This is the part that I fixed, so my idea here is to just keep it as-is for now, and then we can try to change it and assert that it did not break later on.

} else {
const senders =
cast.framework.CastReceiverContext.getInstance().getSenders();

this.deviceName = 'Google Cast';
this.deviceId =
senders.length !== 0 && senders[0].id
? senders[0].id
: new Date().getTime().toString();
}
}

// create the necessary headers for authentication
private static getSecurityHeaders(): Dictionary<string> {
// TODO throw error if this fails

let auth =
`Emby Client="Chromecast", ` +
`Device="${window.deviceInfo.deviceName}", ` +
`DeviceId="${window.deviceInfo.deviceId}", ` +
`Version="${window.deviceInfo.versionNumber}"`;
`Emby Client="Chromecast", Device="${this.deviceName}", ` +
`DeviceId="${this.deviceId}", Version="${this.versionNumber}"`;

if (this.userId) {
auth += `, UserId="${this.userId}"`;
Expand Down
Loading