Merge pull request #166 from jellyfin/develop

Develop
This commit is contained in:
Andrew Rabert 2018-12-14 21:28:12 -05:00 committed by GitHub
commit 46c75d75bd
93 changed files with 12693 additions and 349 deletions

View File

@ -1,14 +1,11 @@
kind: pipeline
name: build:debian
clone:
depth: 1
steps:
- name: submodules
image: docker:git
commands:
- git submodule update --init
- git submodule update --init --recursive
- name: build:debian
image: plugins/docker
group: build
@ -22,14 +19,11 @@ steps:
kind: pipeline
name: build:docker
clone:
depth: 1
steps:
- name: submodules
image: docker:git
commands:
- git submodule update --init
- git submodule update --init --recursive
- name: build:docker
image: plugins/docker
group: build

12
.gitignore vendored
View File

@ -235,7 +235,19 @@ pip-log.txt
MediaBrowser.WebDashboard/dashboard-ui/.idea/
/.vs
##########
# Rider
##########
.idea/
#########################
# Debian build artifacts
#########################
debian/.debhelper/
debian/files
debian/jellyfin.substvars
debian/jellyfin/
# Don't ignore the debian/bin folder
!debian/bin/

View File

@ -1,40 +0,0 @@
Contributing to Jellyfin
============
Thank you for your interest in contributing to Jellyfin. We are a community project made up of volunteers doing our best to keep a free software version of Emby alive and healthy.
This document describes how to contribute to Jellyfin via GitHub, and is applicable to anyone who might wish to open up an issue or do a pull request. Please give it a read-through first.
## Issues
Issues should be one of three things:
1. A software bug with Jellyfin.
2. A feature request or suggestion.
3. A task to be done with Jellyfin.
All other discussions should be directed towards the [Jellyfin Riot channel](https://matrix.to/#/#jellyfin:matrix.org), the [Jellyfish development Riot channel](https://matrix.to/#/#jellyfin-dev:matrix.org), or [our subreddit](https://reddit.com/r/jellyfin).
When writing an issue, please ensure you capture as much relevant detail as possible - having to request additional information just leads to wasted time. Please include any log output, configuration options, and system details.
## Pull Requests
We adhere to a fairly standard fork-and-PR model of development:
1. Fork a copy of the Jellyfin repository to your own user.
2. Make your changes on a local feature branch of your copy of the repository.
3. Submit a pull request from your feature branch back to the upstream develop branch. If your PR includes *only* documentation or text file changes you should submit it against master.
All PRs require review before merging. Once that is done, if the PR is not tagged with the WIP label, it should be immediately merged by the reviewer assuming that they have write access. If you do not have write access, please feel free to leave reviews as well and suggest changes as they will be taken into account as well.
## Developer Access
We are open with granting developer (merge/write) access to those who show interest in contributing and reviewing PRs. Such access can be granted by a current developer. Unruly or abusive developers can be removed at any time by the core team, so please take your responsibility seriously. We're all in this together trying to make the best free software media platform we can.
## How can I contribute?
If you're a .NET C# or Javascript developer, please feel free to jump right in - we can use all the help we can get!
If you're not a developer, please consider helping out by contributing documentation, testing the latest releases and reporting bugs, or by telling your friends about the project! Any little bit helps.

View File

@ -2,20 +2,20 @@ FROM debian:9
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
RUN apt-get update \
&& apt-get install -y apt-transport-https debhelper gnupg wget \
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts \
&& wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.asc.gpg \
&& mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/ \
&& wget -q https://packages.microsoft.com/config/debian/9/prod.list \
&& mv prod.list /etc/apt/sources.list.d/microsoft-prod.list \
&& chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg \
&& chown root:root /etc/apt/sources.list.d/microsoft-prod.list \
&& apt-get update \
&& apt-get install -y dotnet-sdk-2.2
&& apt-get update
WORKDIR /repo
COPY . .
RUN dpkg-buildpackage -us -uc \
RUN yes|mk-build-deps -i \
&& dpkg-buildpackage -us -uc \
&& mkdir /dist \
&& mv /jellyfin*deb /dist

View File

@ -224,8 +224,10 @@ namespace Emby.Dlna.Api
var first = pathInfo[0];
// backwards compatibility
// TODO: Work out what this is doing.
if (string.Equals(first, "mediabrowser", StringComparison.OrdinalIgnoreCase) ||
string.Equals(first, "emby", StringComparison.OrdinalIgnoreCase))
string.Equals(first, "emby", StringComparison.OrdinalIgnoreCase) ||
string.Equals(first, "jellyfin", StringComparison.OrdinalIgnoreCase ))
{
index++;
}

View File

@ -272,9 +272,9 @@ namespace Emby.Dlna.Main
{
CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info.
Location = uri, // Must point to the URL that serves your devices UPnP description document.
FriendlyName = "Emby Server",
Manufacturer = "Emby",
ModelName = "Emby Server",
FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server",
Uuid = udn
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
};

View File

@ -14,7 +14,7 @@ namespace Emby.Dlna.PlayTo
public class SsdpHttpClient
{
private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
private const string FriendlyName = "Emby";
private const string FriendlyName = "Jellyfin";
private readonly IHttpClient _httpClient;
private readonly IServerConfigurationManager _config;

View File

@ -14,9 +14,9 @@ namespace Emby.Dlna.Profiles
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*";
Manufacturer = "Emby";
Manufacturer = "Jellyfin";
ModelDescription = "UPnP/AV 1.0 Compliant Media Server";
ModelName = "Emby Server";
ModelName = "Jellyfin Server";
ModelNumber = "01";
ModelUrl = "https://github.com/jellyfin/jellyfin";
ManufacturerUrl = "https://github.com/jellyfin/jellyfin";

View File

@ -1,9 +1,9 @@
<?xml version="1.0"?>
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Generic Device</Name>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -6,9 +6,9 @@
<Manufacturer>Denon</Manufacturer>
<Headers />
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -7,9 +7,9 @@
<HttpHeaderInfo name="User-Agent" value="DIRECTV" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -8,9 +8,9 @@
<HttpHeaderInfo name="User-Agent" value="Zip_" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -7,9 +7,9 @@
<HttpHeaderInfo name="User-Agent" value="LG" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -5,9 +5,9 @@
<ModelName>DMA2100us</ModelName>
<Headers />
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -7,9 +7,9 @@
<HttpHeaderInfo name="User-Agent" value="Marantz" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -7,9 +7,9 @@
<HttpHeaderInfo name="User-Agent" value="MediaMonkey" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -8,9 +8,9 @@
<HttpHeaderInfo name="User-Agent" value="Panasonic MIL DLNA" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -1,9 +1,9 @@
<?xml version="1.0"?>
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Popcorn Hour</Name>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -7,9 +7,9 @@
<HttpHeaderInfo name="User-Agent" value="SEC_" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -7,9 +7,9 @@
<HttpHeaderInfo name="User-Agent" value="Sharp" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -8,9 +8,9 @@
<HttpHeaderInfo name="X-AV-Client-Info" value="PLAYSTATION 3" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -8,9 +8,9 @@
<HttpHeaderInfo name="X-AV-Client-Info" value="PLAYSTATION 4" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -8,9 +8,9 @@
<HttpHeaderInfo name="User-Agent" value="ALPHA Networks" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -8,9 +8,9 @@
<HttpHeaderInfo name="User-Agent" value="NSPlayer/12" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -7,9 +7,9 @@
<HttpHeaderInfo name="User-Agent" value="foobar" match="Substring" />
</Headers>
</Identification>
<Manufacturer>Emby</Manufacturer>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Emby Server</ModelName>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>

View File

@ -217,7 +217,7 @@ namespace Emby.Dlna.Server
{
if (string.IsNullOrEmpty(_profile.FriendlyName))
{
return "Emby - " + _serverName;
return "Jellyfin - " + _serverName;
}
var characterList = new List<char>();

View File

@ -174,28 +174,24 @@ namespace Emby.Drawing.Skia
var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
using (var image = SKImage.FromBitmap(bitmap))
using (var subset = image.Subset(newRect))
{
using (var subset = image.Subset(newRect))
{
return SKBitmap.FromImage(subset);
}
return SKBitmap.FromImage(subset);
}
}
public ImageSize GetImageSize(string path)
{
using (var s = new SKFileStream(path))
using (var codec = SKCodec.Create(s))
{
using (var codec = SKCodec.Create(s))
{
var info = codec.Info;
var info = codec.Info;
return new ImageSize
{
Width = info.Width,
Height = info.Height
};
}
return new ImageSize
{
Width = info.Width,
Height = info.Height
};
}
}
@ -234,36 +230,36 @@ namespace Emby.Drawing.Skia
return tempPath;
}
private static SKCodecOrigin GetSKCodecOrigin(ImageOrientation? orientation)
private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
{
if (!orientation.HasValue)
{
return SKCodecOrigin.TopLeft;
return SKEncodedOrigin.TopLeft;
}
switch (orientation.Value)
{
case ImageOrientation.TopRight:
return SKCodecOrigin.TopRight;
return SKEncodedOrigin.TopRight;
case ImageOrientation.RightTop:
return SKCodecOrigin.RightTop;
return SKEncodedOrigin.RightTop;
case ImageOrientation.RightBottom:
return SKCodecOrigin.RightBottom;
return SKEncodedOrigin.RightBottom;
case ImageOrientation.LeftTop:
return SKCodecOrigin.LeftTop;
return SKEncodedOrigin.LeftTop;
case ImageOrientation.LeftBottom:
return SKCodecOrigin.LeftBottom;
return SKEncodedOrigin.LeftBottom;
case ImageOrientation.BottomRight:
return SKCodecOrigin.BottomRight;
return SKEncodedOrigin.BottomRight;
case ImageOrientation.BottomLeft:
return SKCodecOrigin.BottomLeft;
return SKEncodedOrigin.BottomLeft;
default:
return SKCodecOrigin.TopLeft;
return SKEncodedOrigin.TopLeft;
}
}
private static string[] TransparentImageTypes = new string[] { ".png", ".gif", ".webp" };
internal static SKBitmap Decode(string path, bool forceCleanBitmap, IFileSystem fileSystem, ImageOrientation? orientation, out SKCodecOrigin origin)
internal static SKBitmap Decode(string path, bool forceCleanBitmap, IFileSystem fileSystem, ImageOrientation? orientation, out SKEncodedOrigin origin)
{
if (!fileSystem.FileExists(path))
{
@ -275,32 +271,30 @@ namespace Emby.Drawing.Skia
if (requiresTransparencyHack || forceCleanBitmap)
{
using (var stream = new SKFileStream(NormalizePath(path, fileSystem)))
using (var codec = SKCodec.Create(stream))
{
using (var codec = SKCodec.Create(stream))
if (codec == null)
{
if (codec == null)
{
origin = GetSKCodecOrigin(orientation);
return null;
}
// create the bitmap
var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
if (bitmap != null)
{
// decode
codec.GetPixels(bitmap.Info, bitmap.GetPixels());
origin = codec.Origin;
}
else
{
origin = GetSKCodecOrigin(orientation);
}
return bitmap;
origin = GetSKEncodedOrigin(orientation);
return null;
}
// create the bitmap
var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
if (bitmap != null)
{
// decode
codec.GetPixels(bitmap.Info, bitmap.GetPixels());
origin = codec.EncodedOrigin;
}
else
{
origin = GetSKEncodedOrigin(orientation);
}
return bitmap;
}
}
@ -320,11 +314,11 @@ namespace Emby.Drawing.Skia
}
}
origin = SKCodecOrigin.TopLeft;
origin = SKEncodedOrigin.TopLeft;
return resultBitmap;
}
private SKBitmap GetBitmap(string path, bool cropWhitespace, bool forceAnalyzeBitmap, ImageOrientation? orientation, out SKCodecOrigin origin)
private SKBitmap GetBitmap(string path, bool cropWhitespace, bool forceAnalyzeBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
{
if (cropWhitespace)
{
@ -339,7 +333,7 @@ namespace Emby.Drawing.Skia
private SKBitmap GetBitmap(string path, bool cropWhitespace, bool autoOrient, ImageOrientation? orientation)
{
SKCodecOrigin origin;
SKEncodedOrigin origin;
if (autoOrient)
{
@ -347,7 +341,7 @@ namespace Emby.Drawing.Skia
if (bitmap != null)
{
if (origin != SKCodecOrigin.TopLeft)
if (origin != SKEncodedOrigin.TopLeft)
{
using (bitmap)
{
@ -362,7 +356,7 @@ namespace Emby.Drawing.Skia
return GetBitmap(path, cropWhitespace, false, orientation, out origin);
}
private SKBitmap OrientImage(SKBitmap bitmap, SKCodecOrigin origin)
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
{
//var transformations = {
// 2: { rotate: 0, flip: true},
@ -377,7 +371,7 @@ namespace Emby.Drawing.Skia
switch (origin)
{
case SKCodecOrigin.TopRight:
case SKEncodedOrigin.TopRight:
{
var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
using (var surface = new SKCanvas(rotated))
@ -390,7 +384,7 @@ namespace Emby.Drawing.Skia
return rotated;
}
case SKCodecOrigin.BottomRight:
case SKEncodedOrigin.BottomRight:
{
var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
using (var surface = new SKCanvas(rotated))
@ -408,7 +402,7 @@ namespace Emby.Drawing.Skia
return rotated;
}
case SKCodecOrigin.BottomLeft:
case SKEncodedOrigin.BottomLeft:
{
var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
using (var surface = new SKCanvas(rotated))
@ -429,7 +423,7 @@ namespace Emby.Drawing.Skia
return rotated;
}
case SKCodecOrigin.LeftTop:
case SKEncodedOrigin.LeftTop:
{
// TODO: Remove dual canvases, had trouble with flipping
using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
@ -456,7 +450,7 @@ namespace Emby.Drawing.Skia
}
}
case SKCodecOrigin.RightTop:
case SKEncodedOrigin.RightTop:
{
var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
using (var surface = new SKCanvas(rotated))
@ -469,7 +463,7 @@ namespace Emby.Drawing.Skia
return rotated;
}
case SKCodecOrigin.RightBottom:
case SKEncodedOrigin.RightBottom:
{
// TODO: Remove dual canvases, had trouble with flipping
using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
@ -493,7 +487,7 @@ namespace Emby.Drawing.Skia
}
}
case SKCodecOrigin.LeftBottom:
case SKEncodedOrigin.LeftBottom:
{
var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
using (var surface = new SKCanvas(rotated))
@ -569,56 +563,54 @@ namespace Emby.Drawing.Skia
}
}
// create bitmap to use for canvas drawing
// create bitmap to use for canvas drawing used to draw into bitmap
using (var saveBitmap = new SKBitmap(width, height))//, bitmap.ColorType, bitmap.AlphaType))
using (var canvas = new SKCanvas(saveBitmap))
{
// create canvas used to draw into bitmap
using (var canvas = new SKCanvas(saveBitmap))
// set background color if present
if (hasBackgroundColor)
{
// set background color if present
if (hasBackgroundColor)
canvas.Clear(SKColor.Parse(options.BackgroundColor));
}
// Add blur if option is present
if (blur > 0)
{
// create image from resized bitmap to apply blur
using (var paint = new SKPaint())
using (var filter = SKImageFilter.CreateBlur(blur, blur))
{
canvas.Clear(SKColor.Parse(options.BackgroundColor));
paint.ImageFilter = filter;
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
}
}
else
{
// draw resized bitmap onto canvas
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
}
// If foreground layer present then draw
if (hasForegroundColor)
{
Double opacity;
if (!Double.TryParse(options.ForegroundLayer, out opacity))
{
opacity = .4;
}
// Add blur if option is present
if (blur > 0)
{
using (var paint = new SKPaint())
{
// create image from resized bitmap to apply blur
using (var filter = SKImageFilter.CreateBlur(blur, blur))
{
paint.ImageFilter = filter;
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
}
}
}
else
{
// draw resized bitmap onto canvas
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
}
canvas.DrawColor(new SKColor(0, 0, 0, (Byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
}
// If foreground layer present then draw
if (hasForegroundColor)
{
Double opacity;
if (!Double.TryParse(options.ForegroundLayer, out opacity)) opacity = .4;
if (hasIndicator)
{
DrawIndicator(canvas, width, height, options);
}
canvas.DrawColor(new SKColor(0, 0, 0, (Byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
}
if (hasIndicator)
{
DrawIndicator(canvas, width, height, options);
}
_fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath));
using (var outputStream = new SKFileWStream(outputPath))
{
saveBitmap.Encode(outputStream, skiaOutputFormat, quality);
}
_fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath));
using (var outputStream = new SKFileWStream(outputPath))
{
saveBitmap.Encode(outputStream, skiaOutputFormat, quality);
}
}
}
@ -687,4 +679,4 @@ namespace Emby.Drawing.Skia
get { return true; }
}
}
}
}

View File

@ -99,56 +99,48 @@ namespace Emby.Drawing.Skia
using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
{
currentBitmap.Resize(resizeBitmap, SKBitmapResizeMethod.Lanczos3);
// determine how much to crop
// crop image
int ix = (int)Math.Abs((iWidth - iSlice) / 2);
using (var image = SKImage.FromBitmap(resizeBitmap))
using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)))
{
// crop image
using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)))
// draw image onto canvas
canvas.DrawImage(subset ?? image, (horizontalImagePadding * (i + 1)) + (iSlice * i), verticalSpacing);
if (subset == null)
{
// draw image onto canvas
canvas.DrawImage(subset ?? image, (horizontalImagePadding * (i + 1)) + (iSlice * i), verticalSpacing);
continue;
}
// create reflection of image below the drawn image
using (var croppedBitmap = SKBitmap.FromImage(subset))
using (var reflectionBitmap = new SKBitmap(croppedBitmap.Width, croppedBitmap.Height / 2, croppedBitmap.ColorType, croppedBitmap.AlphaType))
{
// resize to half height
croppedBitmap.Resize(reflectionBitmap, SKBitmapResizeMethod.Lanczos3);
if (subset == null)
using (var flippedBitmap = new SKBitmap(reflectionBitmap.Width, reflectionBitmap.Height, reflectionBitmap.ColorType, reflectionBitmap.AlphaType))
using (var flippedCanvas = new SKCanvas(flippedBitmap))
{
continue;
}
// flip image vertically
var matrix = SKMatrix.MakeScale(1, -1);
matrix.SetScaleTranslate(1, -1, 0, flippedBitmap.Height);
flippedCanvas.SetMatrix(matrix);
flippedCanvas.DrawBitmap(reflectionBitmap, 0, 0);
flippedCanvas.ResetMatrix();
using (var croppedBitmap = SKBitmap.FromImage(subset))
{
// create reflection of image below the drawn image
using (var reflectionBitmap = new SKBitmap(croppedBitmap.Width, croppedBitmap.Height / 2, croppedBitmap.ColorType, croppedBitmap.AlphaType))
// create gradient to make image appear as a reflection
var remainingHeight = height - (iHeight + (2 * verticalSpacing));
flippedCanvas.ClipRect(SKRect.Create(reflectionBitmap.Width, remainingHeight));
using (var gradient = new SKPaint())
{
// resize to half height
croppedBitmap.Resize(reflectionBitmap, SKBitmapResizeMethod.Lanczos3);
using (var flippedBitmap = new SKBitmap(reflectionBitmap.Width, reflectionBitmap.Height, reflectionBitmap.ColorType, reflectionBitmap.AlphaType))
{
using (var flippedCanvas = new SKCanvas(flippedBitmap))
{
// flip image vertically
var matrix = SKMatrix.MakeScale(1, -1);
matrix.SetScaleTranslate(1, -1, 0, flippedBitmap.Height);
flippedCanvas.SetMatrix(matrix);
flippedCanvas.DrawBitmap(reflectionBitmap, 0, 0);
flippedCanvas.ResetMatrix();
// create gradient to make image appear as a reflection
var remainingHeight = height - (iHeight + (2 * verticalSpacing));
flippedCanvas.ClipRect(SKRect.Create(reflectionBitmap.Width, remainingHeight));
using (var gradient = new SKPaint())
{
gradient.IsAntialias = true;
gradient.BlendMode = SKBlendMode.SrcOver;
gradient.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0), new SKPoint(0, remainingHeight), new[] { new SKColor(0, 0, 0, 128), new SKColor(0, 0, 0, 208), new SKColor(0, 0, 0, 240), new SKColor(0, 0, 0, 255) }, null, SKShaderTileMode.Clamp);
flippedCanvas.DrawPaint(gradient);
}
// finally draw reflection onto canvas
canvas.DrawBitmap(flippedBitmap, (horizontalImagePadding * (i + 1)) + (iSlice * i), iHeight + (2 * verticalSpacing));
}
}
gradient.IsAntialias = true;
gradient.BlendMode = SKBlendMode.SrcOver;
gradient.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0), new SKPoint(0, remainingHeight), new[] { new SKColor(0, 0, 0, 128), new SKColor(0, 0, 0, 208), new SKColor(0, 0, 0, 240), new SKColor(0, 0, 0, 255) }, null, SKShaderTileMode.Clamp);
flippedCanvas.DrawPaint(gradient);
}
// finally draw reflection onto canvas
canvas.DrawBitmap(flippedBitmap, (horizontalImagePadding * (i + 1)) + (iSlice * i), iHeight + (2 * verticalSpacing));
}
}
}
@ -172,7 +164,7 @@ namespace Emby.Drawing.Skia
currentIndex = 0;
}
SKCodecOrigin origin;
SKEncodedOrigin origin;
bitmap = SkiaEncoder.Decode(paths[currentIndex], false, _fileSystem, null, out origin);
imagesTested[currentIndex] = 0;
@ -202,7 +194,6 @@ namespace Emby.Drawing.Skia
{
for (var y = 0; y < 2; y++)
{
SKCodecOrigin origin;
int newIndex;
using (var currentBitmap = GetNextValidImage(paths, imageIndex, out newIndex))
@ -232,4 +223,4 @@ namespace Emby.Drawing.Skia
return bitmap;
}
}
}
}

View File

@ -1,8 +1,8 @@
#MediaBrowser.IsoMounting.Linux
# MediaBrowser.IsoMounting.Linux
This implements two core interfaces, IIsoManager, and IIsoMount.
###IIsoManager
### IIsoManager
The manager class can be used to create a mount, and also determine if the mounter is capable of mounting a given file.
###IIsoMount
### IIsoMount
IIsoMount then represents a mount instance, which will be unmounted on disposal.
***
This Linux version use sudo, mount and umount.

View File

@ -21,7 +21,6 @@ using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.MediaEncoder;
using Emby.Server.Implementations.Net;
using Emby.Notifications;
using Emby.Server.Implementations.Playlists;
@ -34,7 +33,6 @@ using Emby.Server.Implementations.Threading;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Emby.Server.Implementations.Xml;
using Emby.Server.MediaEncoding.Subtitles;
using MediaBrowser.Api;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
@ -1040,7 +1038,7 @@ namespace Emby.Server.Implementations
RegisterMediaEncoder(assemblyInfo);
EncodingManager = new EncodingManager(FileSystemManager, Logger, MediaEncoder, ChapterManager, LibraryManager);
EncodingManager = new Emby.Server.Implementations.MediaEncoder.EncodingManager(FileSystemManager, Logger, MediaEncoder, ChapterManager, LibraryManager);
RegisterSingleInstance(EncodingManager);
var activityLogRepo = GetActivityLogRepository();
@ -1054,7 +1052,7 @@ namespace Emby.Server.Implementations
AuthService = new AuthService(UserManager, authContext, ServerConfigurationManager, ConnectManager, SessionManager, NetworkManager);
RegisterSingleInstance<IAuthService>(AuthService);
SubtitleEncoder = new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory, TextEncoding);
SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory, TextEncoding);
RegisterSingleInstance(SubtitleEncoder);
RegisterSingleInstance(CreateResourceFileManager());
@ -1270,7 +1268,7 @@ namespace Emby.Server.Implementations
probePath = info.ProbePath;
var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase);
var mediaEncoder = new MediaEncoding.Encoder.MediaEncoder(LogManager.GetLogger("MediaEncoder"),
var mediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(LogManager.GetLogger("MediaEncoder"),
JsonSerializer,
encoderPath,
probePath,
@ -1287,10 +1285,8 @@ namespace Emby.Server.Implementations
HttpClient,
ZipClient,
ProcessFactory,
EnvironmentInfo,
BlurayExaminer,
assemblyInfo,
this);
5000, false,
EnvironmentInfo);
MediaEncoder = mediaEncoder;
RegisterSingleInstance(MediaEncoder);
@ -1777,7 +1773,7 @@ namespace Emby.Server.Implementations
list.Add(GetAssembly(typeof(InstallationManager)));
// MediaEncoding
list.Add(GetAssembly(typeof(MediaEncoding.Encoder.MediaEncoder)));
list.Add(GetAssembly(typeof(MediaBrowser.MediaEncoding.Encoder.MediaEncoder)));
// Dlna
list.Add(GetAssembly(typeof(DlnaEntryPoint)));

View File

@ -18,6 +18,7 @@
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
<ProjectReference Include="..\Emby.XmlTv\Emby.XmlTv\Emby.XmlTv.csproj" />
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
</ItemGroup>
<ItemGroup>
@ -148,10 +149,5 @@
<EmbeddedResource Include="Localization\Ratings\es.txt" />
<EmbeddedResource Include="Localization\Ratings\ro.txt" />
</ItemGroup>
<ItemGroup>
<Reference Include="Emby.Server.MediaEncoding">
<HintPath>..\ThirdParty\emby\Emby.Server.MediaEncoding.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -10,6 +10,15 @@ using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Threading;
using System.Collections.Generic;
using System.Threading;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Api.Playback;
using System.IO;
using MediaBrowser.Model.Session;
using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Common.Configuration;
namespace MediaBrowser.Api
{
@ -41,6 +50,11 @@ namespace MediaBrowser.Api
public readonly ITimerFactory TimerFactory;
public readonly IProcessFactory ProcessFactory;
/// <summary>
/// The active transcoding jobs
/// </summary>
private readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks =
new Dictionary<string, SemaphoreSlim>();
@ -64,6 +78,8 @@ namespace MediaBrowser.Api
ResultFactory = resultFactory;
Instance = this;
_sessionManager.PlaybackProgress += _sessionManager_PlaybackProgress;
_sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
}
public static string[] Split(string value, char separator, bool removeEmpty)
@ -81,13 +97,739 @@ namespace MediaBrowser.Api
return value.Split(separator);
}
public void Run()
public SemaphoreSlim GetTranscodingLock(string outputPath)
{
lock (_transcodingLocks)
{
SemaphoreSlim result;
if (!_transcodingLocks.TryGetValue(outputPath, out result))
{
result = new SemaphoreSlim(1, 1);
_transcodingLocks[outputPath] = result;
}
return result;
}
}
private void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
{
PingTranscodingJob(e.PlaySessionId, e.IsPaused);
}
}
void _sessionManager_PlaybackProgress(object sender, PlaybackProgressEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
{
PingTranscodingJob(e.PlaySessionId, e.IsPaused);
}
}
/// <summary>
/// Runs this instance.
/// </summary>
public void Run()
{
try
{
DeleteEncodedMediaCache();
}
catch (FileNotFoundException)
{
// Don't clutter the log
}
catch (IOException)
{
// Don't clutter the log
}
catch (Exception ex)
{
Logger.ErrorException("Error deleting encoded media cache", ex);
}
}
public EncodingOptions GetEncodingOptions()
{
return ConfigurationManagerExtensions.GetConfiguration<EncodingOptions>(_config, "encoding");
}
/// <summary>
/// Deletes the encoded media cache.
/// </summary>
private void DeleteEncodedMediaCache()
{
var path = _config.ApplicationPaths.TranscodingTempPath;
foreach (var file in _fileSystem.GetFilePaths(path, true)
.ToList())
{
_fileSystem.DeleteFile(file);
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{
var list = _activeTranscodingJobs.ToList();
var jobCount = list.Count;
Parallel.ForEach(list, j => KillTranscodingJob(j, false, path => true));
// Try to allow for some time to kill the ffmpeg processes and delete the partial stream files
if (jobCount > 0)
{
var task = Task.Delay(1000);
Task.WaitAll(task);
}
}
/// <summary>
/// Called when [transcode beginning].
/// </summary>
/// <param name="path">The path.</param>
/// <param name="playSessionId">The play session identifier.</param>
/// <param name="liveStreamId">The live stream identifier.</param>
/// <param name="transcodingJobId">The transcoding job identifier.</param>
/// <param name="type">The type.</param>
/// <param name="process">The process.</param>
/// <param name="deviceId">The device id.</param>
/// <param name="state">The state.</param>
/// <param name="cancellationTokenSource">The cancellation token source.</param>
/// <returns>TranscodingJob.</returns>
public TranscodingJob OnTranscodeBeginning(string path,
string playSessionId,
string liveStreamId,
string transcodingJobId,
TranscodingJobType type,
IProcess process,
string deviceId,
StreamState state,
CancellationTokenSource cancellationTokenSource)
{
lock (_activeTranscodingJobs)
{
var job = new TranscodingJob(Logger, TimerFactory)
{
Type = type,
Path = path,
Process = process,
ActiveRequestCount = 1,
DeviceId = deviceId,
CancellationTokenSource = cancellationTokenSource,
Id = transcodingJobId,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId,
MediaSource = state.MediaSource
};
_activeTranscodingJobs.Add(job);
ReportTranscodingProgress(job, state, null, null, null, null, null);
return job;
}
}
public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
{
var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null;
if (job != null)
{
job.Framerate = framerate;
job.CompletionPercentage = percentComplete;
job.TranscodingPositionTicks = ticks;
job.BytesTranscoded = bytesTranscoded;
job.BitRate = bitRate;
}
var deviceId = state.Request.DeviceId;
if (!string.IsNullOrWhiteSpace(deviceId))
{
var audioCodec = state.ActualOutputAudioCodec;
var videoCodec = state.ActualOutputVideoCodec;
_sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
{
Bitrate = bitRate ?? state.TotalOutputBitrate,
AudioCodec = audioCodec,
VideoCodec = videoCodec,
Container = state.OutputContainer,
Framerate = framerate,
CompletionPercentage = percentComplete,
Width = state.OutputWidth,
Height = state.OutputHeight,
AudioChannels = state.OutputAudioChannels,
IsAudioDirect = string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase),
IsVideoDirect = string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase),
TranscodeReasons = state.TranscodeReasons
});
}
}
/// <summary>
/// <summary>
/// The progressive
/// </summary>
/// Called when [transcode failed to start].
/// </summary>
/// <param name="path">The path.</param>
/// <param name="type">The type.</param>
/// <param name="state">The state.</param>
public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
{
lock (_activeTranscodingJobs)
{
var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
if (job != null)
{
_activeTranscodingJobs.Remove(job);
}
}
lock (_transcodingLocks)
{
_transcodingLocks.Remove(path);
}
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
{
_sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
}
}
/// <summary>
/// Determines whether [has active transcoding job] [the specified path].
/// </summary>
/// <param name="path">The path.</param>
/// <param name="type">The type.</param>
/// <returns><c>true</c> if [has active transcoding job] [the specified path]; otherwise, <c>false</c>.</returns>
public bool HasActiveTranscodingJob(string path, TranscodingJobType type)
{
return GetTranscodingJob(path, type) != null;
}
public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
{
lock (_activeTranscodingJobs)
{
return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
}
}
public TranscodingJob GetTranscodingJob(string playSessionId)
{
lock (_activeTranscodingJobs)
{
return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary>
/// Called when [transcode begin request].
/// </summary>
/// <param name="path">The path.</param>
/// <param name="type">The type.</param>
public TranscodingJob OnTranscodeBeginRequest(string path, TranscodingJobType type)
{
lock (_activeTranscodingJobs)
{
var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
if (job == null)
{
return null;
}
OnTranscodeBeginRequest(job);
return job;
}
}
public void OnTranscodeBeginRequest(TranscodingJob job)
{
job.ActiveRequestCount++;
if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
{
job.StopKillTimer();
}
}
public void OnTranscodeEndRequest(TranscodingJob job)
{
job.ActiveRequestCount--;
//Logger.Debug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
if (job.ActiveRequestCount <= 0)
{
PingTimer(job, false);
}
}
internal void PingTranscodingJob(string playSessionId, bool? isUserPaused)
{
if (string.IsNullOrEmpty(playSessionId))
{
throw new ArgumentNullException("playSessionId");
}
//Logger.Debug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
List<TranscodingJob> jobs;
lock (_activeTranscodingJobs)
{
// This is really only needed for HLS.
// Progressive streams can stop on their own reliably
jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
}
foreach (var job in jobs)
{
if (isUserPaused.HasValue)
{
//Logger.Debug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
job.IsUserPaused = isUserPaused.Value;
}
PingTimer(job, true);
}
}
private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
{
if (job.HasExited)
{
job.StopKillTimer();
return;
}
var timerDuration = 10000;
if (job.Type != TranscodingJobType.Progressive)
{
timerDuration = 60000;
}
job.PingTimeout = timerDuration;
job.LastPingDate = DateTime.UtcNow;
// Don't start the timer for playback checkins with progressive streaming
if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
{
job.StartKillTimer(OnTranscodeKillTimerStopped);
}
else
{
job.ChangeKillTimerIfStarted();
}
}
/// <summary>
/// Called when [transcode kill timer stopped].
/// </summary>
/// <param name="state">The state.</param>
private void OnTranscodeKillTimerStopped(object state)
{
var job = (TranscodingJob)state;
if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
{
var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
if (timeSinceLastPing < job.PingTimeout)
{
job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
return;
}
}
Logger.Info("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
KillTranscodingJob(job, true, path => true);
}
/// <summary>
/// Kills the single transcoding job.
/// </summary>
/// <param name="deviceId">The device id.</param>
/// <param name="playSessionId">The play session identifier.</param>
/// <param name="deleteFiles">The delete files.</param>
/// <returns>Task.</returns>
internal void KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
{
KillTranscodingJobs(j =>
{
if (!string.IsNullOrWhiteSpace(playSessionId))
{
return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
}
return string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase);
}, deleteFiles);
}
/// <summary>
/// Kills the transcoding jobs.
/// </summary>
/// <param name="killJob">The kill job.</param>
/// <param name="deleteFiles">The delete files.</param>
/// <returns>Task.</returns>
private void KillTranscodingJobs(Func<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles)
{
var jobs = new List<TranscodingJob>();
lock (_activeTranscodingJobs)
{
// This is really only needed for HLS.
// Progressive streams can stop on their own reliably
jobs.AddRange(_activeTranscodingJobs.Where(killJob));
}
if (jobs.Count == 0)
{
return;
}
foreach (var job in jobs)
{
KillTranscodingJob(job, false, deleteFiles);
}
}
/// <summary>
/// Kills the transcoding job.
/// </summary>
/// <param name="job">The job.</param>
/// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
/// <param name="delete">The delete.</param>
private async void KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
{
job.DisposeKillTimer();
Logger.Debug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
lock (_activeTranscodingJobs)
{
_activeTranscodingJobs.Remove(job);
if (!job.CancellationTokenSource.IsCancellationRequested)
{
job.CancellationTokenSource.Cancel();
}
}
lock (_transcodingLocks)
{
_transcodingLocks.Remove(job.Path);
}
lock (job.ProcessLock)
{
if (job.TranscodingThrottler != null)
{
job.TranscodingThrottler.Stop();
}
var process = job.Process;
var hasExited = job.HasExited;
if (!hasExited)
{
try
{
Logger.Info("Stopping ffmpeg process with q command for {0}", job.Path);
//process.Kill();
process.StandardInput.WriteLine("q");
// Need to wait because killing is asynchronous
if (!process.WaitForExit(5000))
{
Logger.Info("Killing ffmpeg process for {0}", job.Path);
process.Kill();
}
}
catch (Exception ex)
{
Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
}
}
}
if (delete(job.Path))
{
DeletePartialStreamFiles(job.Path, job.Type, 0, 1500);
}
if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
{
try
{
await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.ErrorException("Error closing live stream for {0}", ex, job.Path);
}
}
}
private async void DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
{
if (retryCount >= 10)
{
return;
}
Logger.Info("Deleting partial stream file(s) {0}", path);
await Task.Delay(delayMs).ConfigureAwait(false);
try
{
if (jobType == TranscodingJobType.Progressive)
{
DeleteProgressivePartialStreamFiles(path);
}
else
{
DeleteHlsPartialStreamFiles(path);
}
}
catch (FileNotFoundException)
{
}
catch (IOException)
{
//Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path);
DeletePartialStreamFiles(path, jobType, retryCount + 1, 500);
}
catch
{
//Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path);
}
}
/// <summary>
/// Deletes the progressive partial stream files.
/// </summary>
/// <param name="outputFilePath">The output file path.</param>
private void DeleteProgressivePartialStreamFiles(string outputFilePath)
{
_fileSystem.DeleteFile(outputFilePath);
}
/// <summary>
/// Deletes the HLS partial stream files.
/// </summary>
/// <param name="outputFilePath">The output file path.</param>
private void DeleteHlsPartialStreamFiles(string outputFilePath)
{
var directory = _fileSystem.GetDirectoryName(outputFilePath);
var name = Path.GetFileNameWithoutExtension(outputFilePath);
var filesToDelete = _fileSystem.GetFilePaths(directory)
.Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1)
.ToList();
Exception e = null;
foreach (var file in filesToDelete)
{
try
{
//Logger.Debug("Deleting HLS file {0}", file);
_fileSystem.DeleteFile(file);
}
catch (FileNotFoundException)
{
}
catch (IOException ex)
{
e = ex;
//Logger.ErrorException("Error deleting HLS file {0}", ex, file);
}
}
if (e != null)
{
throw e;
}
}
}
/// <summary>
/// Class TranscodingJob
/// </summary>
public class TranscodingJob
{
/// <summary>
/// Gets or sets the play session identifier.
/// </summary>
/// <value>The play session identifier.</value>
public string PlaySessionId { get; set; }
/// <summary>
/// Gets or sets the live stream identifier.
/// </summary>
/// <value>The live stream identifier.</value>
public string LiveStreamId { get; set; }
public bool IsLiveOutput { get; set; }
/// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
public MediaSourceInfo MediaSource { get; set; }
public string Path { get; set; }
/// <summary>
/// Gets or sets the type.
/// </summary>
/// <value>The type.</value>
public TranscodingJobType Type { get; set; }
/// <summary>
/// Gets or sets the process.
/// </summary>
/// <value>The process.</value>
public IProcess Process { get; set; }
public ILogger Logger { get; private set; }
/// <summary>
/// Gets or sets the active request count.
/// </summary>
/// <value>The active request count.</value>
public int ActiveRequestCount { get; set; }
/// <summary>
/// Gets or sets the kill timer.
/// </summary>
/// <value>The kill timer.</value>
private ITimer KillTimer { get; set; }
private readonly ITimerFactory _timerFactory;
public string DeviceId { get; set; }
public CancellationTokenSource CancellationTokenSource { get; set; }
public object ProcessLock = new object();
public bool HasExited { get; set; }
public bool IsUserPaused { get; set; }
public string Id { get; set; }
public float? Framerate { get; set; }
public double? CompletionPercentage { get; set; }
public long? BytesDownloaded { get; set; }
public long? BytesTranscoded { get; set; }
public int? BitRate { get; set; }
public long? TranscodingPositionTicks { get; set; }
public long? DownloadPositionTicks { get; set; }
public TranscodingThrottler TranscodingThrottler { get; set; }
private readonly object _timerLock = new object();
public DateTime LastPingDate { get; set; }
public int PingTimeout { get; set; }
public TranscodingJob(ILogger logger, ITimerFactory timerFactory)
{
Logger = logger;
_timerFactory = timerFactory;
}
public void StopKillTimer()
{
lock (_timerLock)
{
if (KillTimer != null)
{
KillTimer.Change(Timeout.Infinite, Timeout.Infinite);
}
}
}
public void DisposeKillTimer()
{
lock (_timerLock)
{
if (KillTimer != null)
{
KillTimer.Dispose();
KillTimer = null;
}
}
}
public void StartKillTimer(Action<object> callback)
{
StartKillTimer(callback, PingTimeout);
}
public void StartKillTimer(Action<object> callback, int intervalMs)
{
if (HasExited)
{
return;
}
lock (_timerLock)
{
if (KillTimer == null)
{
//Logger.Debug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
KillTimer = _timerFactory.Create(callback, this, intervalMs, Timeout.Infinite);
}
else
{
//Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
KillTimer.Change(intervalMs, Timeout.Infinite);
}
}
}
public void ChangeKillTimerIfStarted()
{
if (HasExited)
{
return;
}
lock (_timerLock)
{
if (KillTimer != null)
{
var intervalMs = PingTimeout;
//Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
KillTimer.Change(intervalMs, Timeout.Infinite);
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,333 @@
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Api.Playback.Hls
{
/// <summary>
/// Class BaseHlsService
/// </summary>
public abstract class BaseHlsService : BaseStreamingService
{
/// <summary>
/// Gets the audio arguments.
/// </summary>
protected abstract string GetAudioArguments(StreamState state, EncodingOptions encodingOptions);
/// <summary>
/// Gets the video arguments.
/// </summary>
protected abstract string GetVideoArguments(StreamState state, EncodingOptions encodingOptions);
/// <summary>
/// Gets the segment file extension.
/// </summary>
protected string GetSegmentFileExtension(StreamRequest request)
{
var segmentContainer = request.SegmentContainer;
if (!string.IsNullOrWhiteSpace(segmentContainer))
{
return "." + segmentContainer;
}
return ".ts";
}
/// <summary>
/// Gets the type of the transcoding job.
/// </summary>
/// <value>The type of the transcoding job.</value>
protected override TranscodingJobType TranscodingJobType
{
get { return TranscodingJobType.Hls; }
}
/// <summary>
/// Processes the request.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="isLive">if set to <c>true</c> [is live].</param>
/// <returns>System.Object.</returns>
protected async Task<object> ProcessRequest(StreamRequest request, bool isLive)
{
return await ProcessRequestAsync(request, isLive).ConfigureAwait(false);
}
/// <summary>
/// Processes the request async.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="isLive">if set to <c>true</c> [is live].</param>
/// <returns>Task{System.Object}.</returns>
/// <exception cref="ArgumentException">A video bitrate is required
/// or
/// An audio bitrate is required</exception>
private async Task<object> ProcessRequestAsync(StreamRequest request, bool isLive)
{
var cancellationTokenSource = new CancellationTokenSource();
var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
TranscodingJob job = null;
var playlist = state.OutputFilePath;
if (!FileSystem.FileExists(playlist))
{
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlist);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{
if (!FileSystem.FileExists(playlist))
{
// If the playlist doesn't already exist, startup ffmpeg
try
{
job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
job.IsLiveOutput = isLive;
}
catch
{
state.Dispose();
throw;
}
var minSegments = state.MinSegments;
if (minSegments > 0)
{
await WaitForMinimumSegmentCount(playlist, minSegments, cancellationTokenSource.Token).ConfigureAwait(false);
}
}
}
finally
{
transcodingLock.Release();
}
}
if (isLive)
{
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
if (job != null)
{
ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
}
return ResultFactory.GetResult(GetLivePlaylistText(playlist, state.SegmentLength), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
}
var audioBitrate = state.OutputAudioBitrate ?? 0;
var videoBitrate = state.OutputVideoBitrate ?? 0;
var baselineStreamBitrate = 64000;
var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, baselineStreamBitrate);
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
if (job != null)
{
ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
}
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
}
private string GetLivePlaylistText(string path, int segmentLength)
{
using (var stream = FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite))
{
using (var reader = new StreamReader(stream))
{
var text = reader.ReadToEnd();
text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT");
var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(UsCulture);
text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(UsCulture), newDuration, StringComparison.OrdinalIgnoreCase);
//text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(UsCulture), newDuration, StringComparison.OrdinalIgnoreCase);
return text;
}
}
}
private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, int baselineStreamBitrate)
{
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U");
// Pad a little to satisfy the apple hls validator
var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
// Main stream
builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(UsCulture));
var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
builder.AppendLine(playlistUrl);
return builder.ToString();
}
protected virtual async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
{
Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist);
while (!cancellationToken.IsCancellationRequested)
{
try
{
// Need to use FileShareMode.ReadWrite because we're reading the file at the same time it's being written
using (var fileStream = GetPlaylistFileStream(playlist))
{
using (var reader = new StreamReader(fileStream))
{
var count = 0;
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
{
count++;
if (count >= segmentCount)
{
Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
return;
}
}
}
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
}
}
catch (IOException)
{
// May get an error if the file is locked
}
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
}
}
protected Stream GetPlaylistFileStream(string path)
{
var tmpPath = path + ".tmp";
tmpPath = path;
try
{
return FileSystem.GetFileStream(tmpPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, FileOpenOptions.SequentialScan);
}
catch (IOException)
{
return FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, FileOpenOptions.SequentialScan);
}
}
protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
{
var itsOffsetMs = 0;
var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(UsCulture));
var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
// If isEncoding is true we're actually starting ffmpeg
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
var baseUrlParam = string.Empty;
if (state.Request is GetLiveHlsStream)
{
baseUrlParam = string.Format(" -hls_base_url \"{0}/\"",
"hls/" + Path.GetFileNameWithoutExtension(outputPath));
}
var useGenericSegmenter = true;
if (useGenericSegmenter)
{
var outputTsArg = Path.Combine(FileSystem.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
var timeDeltaParam = String.Empty;
var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{
segmentFormat = "mpegts";
}
baseUrlParam = string.Format("\"{0}/\"", "hls/" + Path.GetFileNameWithoutExtension(outputPath));
return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_entry_prefix {12} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
inputModifier,
EncodingHelper.GetInputArgument(state, encodingOptions),
threads,
EncodingHelper.GetMapArgs(state),
GetVideoArguments(state, encodingOptions),
GetAudioArguments(state, encodingOptions),
state.SegmentLength.ToString(UsCulture),
startNumberParam,
outputPath,
outputTsArg,
timeDeltaParam,
segmentFormat,
baseUrlParam
).Trim();
}
// add when stream copying?
// -avoid_negative_ts make_zero -fflags +genpts
var args = string.Format("{0} {1} {2} -map_metadata -1 -map_chapters -1 -threads {3} {4} {5} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero {6} -hls_time {7} -individual_header_trailer 0 -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
itsOffset,
inputModifier,
EncodingHelper.GetInputArgument(state, encodingOptions),
threads,
EncodingHelper.GetMapArgs(state),
GetVideoArguments(state, encodingOptions),
GetAudioArguments(state, encodingOptions),
state.SegmentLength.ToString(UsCulture),
startNumberParam,
state.HlsListSize.ToString(UsCulture),
baseUrlParam,
outputPath
).Trim();
return args;
}
protected override string GetDefaultH264Preset()
{
return "veryfast";
}
protected virtual int GetStartNumber(StreamState state)
{
return 0;
}
public BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext)
{
}
}
}

View File

@ -0,0 +1,971 @@
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Services;
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
namespace MediaBrowser.Api.Playback.Hls
{
/// <summary>
/// Options is needed for chromecast. Threw Head in there since it's related
/// </summary>
[Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
[Route("/Videos/{Id}/master.m3u8", "HEAD", Summary = "Gets a video stream using HTTP live streaming.")]
public class GetMasterHlsVideoPlaylist : VideoStreamRequest, IMasterHlsRequest
{
public bool EnableAdaptiveBitrateStreaming { get; set; }
public GetMasterHlsVideoPlaylist()
{
EnableAdaptiveBitrateStreaming = true;
}
}
[Route("/Audio/{Id}/master.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
[Route("/Audio/{Id}/master.m3u8", "HEAD", Summary = "Gets an audio stream using HTTP live streaming.")]
public class GetMasterHlsAudioPlaylist : StreamRequest, IMasterHlsRequest
{
public bool EnableAdaptiveBitrateStreaming { get; set; }
public GetMasterHlsAudioPlaylist()
{
EnableAdaptiveBitrateStreaming = true;
}
}
public interface IMasterHlsRequest
{
bool EnableAdaptiveBitrateStreaming { get; set; }
}
[Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
public class GetVariantHlsVideoPlaylist : VideoStreamRequest
{
}
[Route("/Audio/{Id}/main.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
public class GetVariantHlsAudioPlaylist : StreamRequest
{
}
[Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
public class GetHlsVideoSegment : VideoStreamRequest
{
public string PlaylistId { get; set; }
/// <summary>
/// Gets or sets the segment id.
/// </summary>
/// <value>The segment id.</value>
public string SegmentId { get; set; }
}
[Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
public class GetHlsAudioSegment : StreamRequest
{
public string PlaylistId { get; set; }
/// <summary>
/// Gets or sets the segment id.
/// </summary>
/// <value>The segment id.</value>
public string SegmentId { get; set; }
}
[Authenticated]
public class DynamicHlsService : BaseHlsService
{
public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext)
{
NetworkManager = networkManager;
}
protected INetworkManager NetworkManager { get; private set; }
public Task<object> Get(GetMasterHlsVideoPlaylist request)
{
return GetMasterPlaylistInternal(request, "GET");
}
public Task<object> Head(GetMasterHlsVideoPlaylist request)
{
return GetMasterPlaylistInternal(request, "HEAD");
}
public Task<object> Get(GetMasterHlsAudioPlaylist request)
{
return GetMasterPlaylistInternal(request, "GET");
}
public Task<object> Head(GetMasterHlsAudioPlaylist request)
{
return GetMasterPlaylistInternal(request, "HEAD");
}
public Task<object> Get(GetVariantHlsVideoPlaylist request)
{
return GetVariantPlaylistInternal(request, true, "main");
}
public Task<object> Get(GetVariantHlsAudioPlaylist request)
{
return GetVariantPlaylistInternal(request, false, "main");
}
public Task<object> Get(GetHlsVideoSegment request)
{
return GetDynamicSegment(request, request.SegmentId);
}
public Task<object> Get(GetHlsAudioSegment request)
{
return GetDynamicSegment(request, request.SegmentId);
}
private async Task<object> GetDynamicSegment(StreamRequest request, string segmentId)
{
if ((request.StartTimeTicks ?? 0) > 0)
{
throw new ArgumentException("StartTimeTicks is not allowed.");
}
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, UsCulture);
var state = await GetState(request, cancellationToken).ConfigureAwait(false);
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
var segmentPath = GetSegmentPath(state, playlistPath, requestedIndex);
var segmentExtension = GetSegmentFileExtension(state.Request);
TranscodingJob job = null;
if (FileSystem.FileExists(segmentPath))
{
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
}
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlistPath);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
var released = false;
var startTranscoding = false;
try
{
if (FileSystem.FileExists(segmentPath))
{
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
transcodingLock.Release();
released = true;
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
}
else
{
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
if (currentTranscodingIndex == null)
{
Logger.Debug("Starting transcoding because currentTranscodingIndex=null");
startTranscoding = true;
}
else if (requestedIndex < currentTranscodingIndex.Value)
{
Logger.Debug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", requestedIndex, currentTranscodingIndex);
startTranscoding = true;
}
else if (requestedIndex - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
{
Logger.Debug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", requestedIndex - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, requestedIndex);
startTranscoding = true;
}
if (startTranscoding)
{
// If the playlist doesn't already exist, startup ffmpeg
try
{
ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false);
if (currentTranscodingIndex.HasValue)
{
DeleteLastFile(playlistPath, segmentExtension, 0);
}
request.StartTimeTicks = GetStartPositionTicks(state, requestedIndex);
job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
}
catch
{
state.Dispose();
throw;
}
//await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
}
else
{
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job.TranscodingThrottler != null)
{
job.TranscodingThrottler.UnpauseTranscoding();
}
}
}
}
finally
{
if (!released)
{
transcodingLock.Release();
}
}
//Logger.Info("waiting for {0}", segmentPath);
//while (!File.Exists(segmentPath))
//{
// await Task.Delay(50, cancellationToken).ConfigureAwait(false);
//}
Logger.Info("returning {0}", segmentPath);
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
}
private const int BufferSize = 81920;
private long GetStartPositionTicks(StreamState state, int requestedIndex)
{
double startSeconds = 0;
var lengths = GetSegmentLengths(state);
if (requestedIndex >= lengths.Length)
{
var msg = string.Format("Invalid segment index requested: {0} - Segment count: {1}", requestedIndex, lengths.Length);
throw new ArgumentException(msg);
}
for (var i = 0; i < requestedIndex; i++)
{
startSeconds += lengths[i];
}
var position = TimeSpan.FromSeconds(startSeconds).Ticks;
return position;
}
private long GetEndPositionTicks(StreamState state, int requestedIndex)
{
double startSeconds = 0;
var lengths = GetSegmentLengths(state);
if (requestedIndex >= lengths.Length)
{
var msg = string.Format("Invalid segment index requested: {0} - Segment count: {1}", requestedIndex, lengths.Length);
throw new ArgumentException(msg);
}
for (var i = 0; i <= requestedIndex; i++)
{
startSeconds += lengths[i];
}
var position = TimeSpan.FromSeconds(startSeconds).Ticks;
return position;
}
private double[] GetSegmentLengths(StreamState state)
{
var result = new List<double>();
var ticks = state.RunTimeTicks ?? 0;
var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks;
while (ticks > 0)
{
var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks;
result.Add(TimeSpan.FromTicks(length).TotalSeconds);
ticks -= length;
}
return result.ToArray();
}
public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
{
var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType);
if (job == null || job.HasExited)
{
return null;
}
var file = GetLastTranscodingFile(playlist, segmentExtension, FileSystem);
if (file == null)
{
return null;
}
var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
return int.Parse(indexString, NumberStyles.Integer, UsCulture);
}
private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
{
var file = GetLastTranscodingFile(playlistPath, segmentExtension, FileSystem);
if (file != null)
{
DeleteFile(file.FullName, retryCount);
}
}
private void DeleteFile(string path, int retryCount)
{
if (retryCount >= 5)
{
return;
}
Logger.Debug("Deleting partial HLS file {0}", path);
try
{
FileSystem.DeleteFile(path);
}
catch (IOException ex)
{
Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path);
var task = Task.Delay(100);
Task.WaitAll(task);
DeleteFile(path, retryCount + 1);
}
catch (Exception ex)
{
Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path);
}
}
private static FileSystemMetadata GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
{
var folder = fileSystem.GetDirectoryName(playlist);
var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
try
{
return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false)
.Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(fileSystem.GetLastWriteTimeUtc)
.FirstOrDefault();
}
catch (IOException)
{
return null;
}
}
protected override int GetStartNumber(StreamState state)
{
return GetStartNumber(state.VideoRequest);
}
private int GetStartNumber(VideoStreamRequest request)
{
var segmentId = "0";
var segmentRequest = request as GetHlsVideoSegment;
if (segmentRequest != null)
{
segmentId = segmentRequest.SegmentId;
}
return int.Parse(segmentId, NumberStyles.Integer, UsCulture);
}
private string GetSegmentPath(StreamState state, string playlist, int index)
{
var folder = FileSystem.GetDirectoryName(playlist);
var filename = Path.GetFileNameWithoutExtension(playlist);
return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state.Request));
}
private async Task<object> GetSegmentResult(StreamState state,
string playlistPath,
string segmentPath,
string segmentExtension,
int segmentIndex,
TranscodingJob transcodingJob,
CancellationToken cancellationToken)
{
var segmentFileExists = FileSystem.FileExists(segmentPath);
// If all transcoding has completed, just return immediately
if (transcodingJob != null && transcodingJob.HasExited && segmentFileExists)
{
return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
}
if (segmentFileExists)
{
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
// If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready
if (segmentIndex < currentTranscodingIndex)
{
return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
}
}
var segmentFilename = Path.GetFileName(segmentPath);
while (!cancellationToken.IsCancellationRequested)
{
try
{
var text = FileSystem.ReadAllText(playlistPath, Encoding.UTF8);
// If it appears in the playlist, it's done
if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
{
if (!segmentFileExists)
{
segmentFileExists = FileSystem.FileExists(segmentPath);
}
if (segmentFileExists)
{
return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
}
//break;
}
}
catch (IOException)
{
// May get an error if the file is locked
}
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
cancellationToken.ThrowIfCancellationRequested();
return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
}
private Task<object> GetSegmentResult(StreamState state, string segmentPath, int index, TranscodingJob transcodingJob)
{
var segmentEndingPositionTicks = GetEndPositionTicks(state, index);
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
Path = segmentPath,
FileShare = FileShareMode.ReadWrite,
OnComplete = () =>
{
if (transcodingJob != null)
{
transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
}
}
});
}
private async Task<object> GetMasterPlaylistInternal(StreamRequest request, string method)
{
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
if (string.IsNullOrEmpty(request.MediaSourceId))
{
throw new ArgumentException("MediaSourceId is required");
}
var playlistText = string.Empty;
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
{
var audioBitrate = state.OutputAudioBitrate ?? 0;
var videoBitrate = state.OutputVideoBitrate ?? 0;
playlistText = GetMasterPlaylistFileText(state, videoBitrate + audioBitrate);
}
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
}
private string GetMasterPlaylistFileText(StreamState state, int totalBitrate)
{
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U");
var isLiveStream = state.IsSegmentedLiveStream;
var queryStringIndex = Request.RawUrl.IndexOf('?');
var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
// from universal audio service
if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
{
queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
}
// from universal audio service
if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
{
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
}
// Main stream
var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
playlistUrl += queryString;
var request = state.Request;
var subtitleStreams = state.MediaSource
.MediaStreams
.Where(i => i.IsTextSubtitleStream)
.ToList();
var subtitleGroup = subtitleStreams.Count > 0 &&
request is GetMasterHlsVideoPlaylist &&
(state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest.EnableSubtitlesInManifest) ?
"subs" :
null;
// If we're burning in subtitles then don't add additional subs to the manifest
if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
{
subtitleGroup = null;
}
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
AddSubtitles(state, subtitleStreams, builder);
}
AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
if (EnableAdaptiveBitrateStreaming(state, isLiveStream))
{
var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
// By default, vary by just 200k
var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation;
var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
variation *= 2;
newBitrate = totalBitrate - variation;
variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
}
return builder.ToString();
}
private string ReplaceBitrate(string url, int oldValue, int newValue)
{
return url.Replace(
"videobitrate=" + oldValue.ToString(UsCulture),
"videobitrate=" + newValue.ToString(UsCulture),
StringComparison.OrdinalIgnoreCase);
}
private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
{
var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
foreach (var stream in subtitles)
{
const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
var name = stream.DisplayTitle;
var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
var isForced = stream.IsForced;
var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
state.Request.MediaSourceId,
stream.Index.ToString(UsCulture),
30.ToString(UsCulture),
AuthorizationContext.GetAuthorizationInfo(Request).Token);
var line = string.Format(format,
name,
isDefault ? "YES" : "NO",
isForced ? "YES" : "NO",
url,
stream.Language ?? "Unknown");
builder.AppendLine(line);
}
}
private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream)
{
// Within the local network this will likely do more harm than good.
if (Request.IsLocal || NetworkManager.IsInLocalNetwork(Request.RemoteIp))
{
return false;
}
var request = state.Request as IMasterHlsRequest;
if (request != null && !request.EnableAdaptiveBitrateStreaming)
{
return false;
}
if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
{
// Opening live streams is so slow it's not even worth it
return false;
}
if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!state.IsOutputVideo)
{
return false;
}
// Having problems in android
return false;
//return state.VideoRequest.VideoBitRate.HasValue;
}
private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup)
{
var header = "#EXT-X-STREAM-INF:BANDWIDTH=" + bitrate.ToString(UsCulture) + ",AVERAGE-BANDWIDTH=" + bitrate.ToString(UsCulture);
// tvos wants resolution, codecs, framerate
//if (state.TargetFramerate.HasValue)
//{
// header += string.Format(",FRAME-RATE=\"{0}\"", state.TargetFramerate.Value.ToString(CultureInfo.InvariantCulture));
//}
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
header += string.Format(",SUBTITLES=\"{0}\"", subtitleGroup);
}
builder.AppendLine(header);
builder.AppendLine(url);
}
private int GetBitrateVariation(int bitrate)
{
// By default, vary by just 50k
var variation = 50000;
if (bitrate >= 10000000)
{
variation = 2000000;
}
else if (bitrate >= 5000000)
{
variation = 1500000;
}
else if (bitrate >= 3000000)
{
variation = 1000000;
}
else if (bitrate >= 2000000)
{
variation = 500000;
}
else if (bitrate >= 1000000)
{
variation = 300000;
}
else if (bitrate >= 600000)
{
variation = 200000;
}
else if (bitrate >= 400000)
{
variation = 100000;
}
return variation;
}
private async Task<object> GetVariantPlaylistInternal(StreamRequest request, bool isOutputVideo, string name)
{
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
var segmentLengths = GetSegmentLengths(state);
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
builder.AppendLine("#EXT-X-VERSION:3");
builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(UsCulture));
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
var queryStringIndex = Request.RawUrl.IndexOf('?');
var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
//if ((Request.UserAgent ?? string.Empty).IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1)
//{
// queryString = string.Empty;
//}
var index = 0;
foreach (var length in segmentLengths)
{
builder.AppendLine("#EXTINF:" + length.ToString("0.0000", UsCulture) + ", nodesc");
builder.AppendLine(string.Format("hls1/{0}/{1}{2}{3}",
name,
index.ToString(UsCulture),
GetSegmentFileExtension(request),
queryString));
index++;
}
builder.AppendLine("#EXT-X-ENDLIST");
var playlistText = builder.ToString();
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
}
protected override string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
{
var audioCodec = EncodingHelper.GetAudioEncoder(state);
if (!state.IsOutputVideo)
{
if (string.Equals(audioCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
return "-acodec copy";
}
var audioTranscodeParams = new List<string>();
audioTranscodeParams.Add("-acodec " + audioCodec);
if (state.OutputAudioBitrate.HasValue)
{
audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(UsCulture));
}
if (state.OutputAudioChannels.HasValue)
{
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(UsCulture));
}
if (state.OutputAudioSampleRate.HasValue)
{
audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture));
}
audioTranscodeParams.Add("-vn");
return string.Join(" ", audioTranscodeParams.ToArray());
}
if (string.Equals(audioCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.EnableBreakOnNonKeyFrames(videoCodec))
{
return "-codec:a:0 copy -copypriorss:a:0 0";
}
return "-codec:a:0 copy";
}
var args = "-codec:a:0 " + audioCodec;
var channels = state.OutputAudioChannels;
if (channels.HasValue)
{
args += " -ac " + channels.Value;
}
var bitrate = state.OutputAudioBitrate;
if (bitrate.HasValue)
{
args += " -ab " + bitrate.Value.ToString(UsCulture);
}
if (state.OutputAudioSampleRate.HasValue)
{
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture);
}
args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true);
return args;
}
protected override string GetVideoArguments(StreamState state, EncodingOptions encodingOptions)
{
if (!state.IsOutputVideo)
{
return string.Empty;
}
var codec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
var args = "-codec:v:0 " + codec;
// if (state.EnableMpegtsM2TsMode)
// {
// args += " -mpegts_m2ts_mode 1";
// }
// See if we can save come cpu cycles by avoiding encoding
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
{
if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
args += " -bsf:v h264_mp4toannexb";
}
//args += " -flags -global_header";
}
else
{
var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
state.SegmentLength.ToString(UsCulture));
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
//args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
// Add resolution params, if specified
if (!hasGraphicalSubs)
{
args += EncodingHelper.GetOutputSizeParam(state, encodingOptions, codec, true);
}
// This is for internal graphical subs
if (hasGraphicalSubs)
{
args += EncodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
}
//args += " -flags -global_header";
}
if (args.IndexOf("-copyts", StringComparison.OrdinalIgnoreCase) == -1)
{
args += " -copyts";
}
if (!string.IsNullOrEmpty(state.OutputVideoSync))
{
args += " -vsync " + state.OutputVideoSync;
}
args += EncodingHelper.GetOutputFFlags(state);
return args;
}
protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
{
var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
// If isEncoding is true we're actually starting ffmpeg
var startNumber = GetStartNumber(state);
var startNumberParam = isEncoding ? startNumber.ToString(UsCulture) : "0";
var mapArgs = state.IsOutputVideo ? EncodingHelper.GetMapArgs(state) : string.Empty;
var outputTsArg = Path.Combine(FileSystem.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
var timeDeltaParam = String.Empty;
if (isEncoding && startNumber > 0)
{
var startTime = state.SegmentLength * startNumber;
timeDeltaParam = string.Format("-segment_time_delta -{0}", startTime);
}
var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{
segmentFormat = "mpegts";
}
var breakOnNonKeyFrames = state.EnableBreakOnNonKeyFrames(videoCodec);
var breakOnNonKeyFramesArg = breakOnNonKeyFrames ? " -break_non_keyframes 1" : "";
return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0{12} -segment_format {11} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
inputModifier,
EncodingHelper.GetInputArgument(state, encodingOptions),
threads,
mapArgs,
GetVideoArguments(state, encodingOptions),
GetAudioArguments(state, encodingOptions),
state.SegmentLength.ToString(UsCulture),
startNumberParam,
outputPath,
outputTsArg,
timeDeltaParam,
segmentFormat,
breakOnNonKeyFramesArg
).Trim();
}
}
}

View File

@ -0,0 +1,163 @@
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Services;
namespace MediaBrowser.Api.Playback.Hls
{
/// <summary>
/// Class GetHlsAudioSegment
/// </summary>
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
//[Authenticated]
[Route("/Audio/{Id}/hls/{SegmentId}/stream.mp3", "GET")]
[Route("/Audio/{Id}/hls/{SegmentId}/stream.aac", "GET")]
public class GetHlsAudioSegmentLegacy
{
// TODO: Deprecate with new iOS app
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
public string Id { get; set; }
/// <summary>
/// Gets or sets the segment id.
/// </summary>
/// <value>The segment id.</value>
public string SegmentId { get; set; }
}
/// <summary>
/// Class GetHlsVideoSegment
/// </summary>
[Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")]
[Authenticated]
public class GetHlsPlaylistLegacy
{
// TODO: Deprecate with new iOS app
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
public string Id { get; set; }
public string PlaylistId { get; set; }
}
[Route("/Videos/ActiveEncodings", "DELETE")]
[Authenticated]
public class StopEncodingProcess
{
[ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
public string DeviceId { get; set; }
[ApiMember(Name = "PlaySessionId", Description = "The play session id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
public string PlaySessionId { get; set; }
}
/// <summary>
/// Class GetHlsVideoSegment
/// </summary>
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
//[Authenticated]
[Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
public class GetHlsVideoSegmentLegacy : VideoStreamRequest
{
public string PlaylistId { get; set; }
/// <summary>
/// Gets or sets the segment id.
/// </summary>
/// <value>The segment id.</value>
public string SegmentId { get; set; }
}
public class HlsSegmentService : BaseApiService
{
private readonly IServerApplicationPaths _appPaths;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
public HlsSegmentService(IServerApplicationPaths appPaths, IServerConfigurationManager config, IFileSystem fileSystem)
{
_appPaths = appPaths;
_config = config;
_fileSystem = fileSystem;
}
public Task<object> Get(GetHlsPlaylistLegacy request)
{
var file = request.PlaylistId + Path.GetExtension(Request.PathInfo);
file = Path.Combine(_appPaths.TranscodingTempPath, file);
return GetFileResult(file, file);
}
public void Delete(StopEncodingProcess request)
{
ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, path => true);
}
/// <summary>
/// Gets the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public Task<object> Get(GetHlsVideoSegmentLegacy request)
{
var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
var transcodeFolderPath = _config.ApplicationPaths.TranscodingTempPath;
file = Path.Combine(transcodeFolderPath, file);
var normalizedPlaylistId = request.PlaylistId;
var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
.FirstOrDefault(i => string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
return GetFileResult(file, playlistPath);
}
/// <summary>
/// Gets the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public Task<object> Get(GetHlsAudioSegmentLegacy request)
{
// TODO: Deprecate with new iOS app
var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
file = Path.Combine(_appPaths.TranscodingTempPath, file);
return ResultFactory.GetStaticFileResult(Request, file, FileShareMode.ReadWrite);
}
private Task<object> GetFileResult(string path, string playlistPath)
{
var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
Path = path,
FileShare = FileShareMode.ReadWrite,
OnComplete = () =>
{
if (transcodingJob != null)
{
ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
}
}
});
}
}
}

View File

@ -0,0 +1,137 @@
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using System;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Services;
namespace MediaBrowser.Api.Playback.Hls
{
[Route("/Videos/{Id}/live.m3u8", "GET")]
public class GetLiveHlsStream : VideoStreamRequest
{
}
/// <summary>
/// Class VideoHlsService
/// </summary>
[Authenticated]
public class VideoHlsService : BaseHlsService
{
public object Get(GetLiveHlsStream request)
{
return ProcessRequest(request, true);
}
/// <summary>
/// Gets the audio arguments.
/// </summary>
protected override string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
{
var codec = EncodingHelper.GetAudioEncoder(state);
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
{
return "-codec:a:0 copy";
}
var args = "-codec:a:0 " + codec;
var channels = state.OutputAudioChannels;
if (channels.HasValue)
{
args += " -ac " + channels.Value;
}
var bitrate = state.OutputAudioBitrate;
if (bitrate.HasValue)
{
args += " -ab " + bitrate.Value.ToString(UsCulture);
}
if (state.OutputAudioSampleRate.HasValue)
{
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture);
}
args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true);
return args;
}
/// <summary>
/// Gets the video arguments.
/// </summary>
protected override string GetVideoArguments(StreamState state, EncodingOptions encodingOptions)
{
if (!state.IsOutputVideo)
{
return string.Empty;
}
var codec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
var args = "-codec:v:0 " + codec;
// if (state.EnableMpegtsM2TsMode)
// {
// args += " -mpegts_m2ts_mode 1";
// }
// See if we can save come cpu cycles by avoiding encoding
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
{
// if h264_mp4toannexb is ever added, do not use it for live tv
if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) &&
!string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
args += " -bsf:v h264_mp4toannexb";
}
}
else
{
var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
state.SegmentLength.ToString(UsCulture));
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
// Add resolution params, if specified
if (!hasGraphicalSubs)
{
args += EncodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
}
// This is for internal graphical subs
if (hasGraphicalSubs)
{
args += EncodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
}
}
args += " -flags -global_header";
if (!string.IsNullOrEmpty(state.OutputVideoSync))
{
args += " -vsync " + state.OutputVideoSync;
}
args += EncodingHelper.GetOutputFFlags(state);
return args;
}
public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext)
{
}
}
}

View File

@ -0,0 +1,618 @@
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
namespace MediaBrowser.Api.Playback
{
[Route("/Items/{Id}/PlaybackInfo", "GET", Summary = "Gets live playback media info for an item")]
public class GetPlaybackInfo : IReturn<PlaybackInfoResponse>
{
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public Guid Id { get; set; }
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
public Guid UserId { get; set; }
}
[Route("/Items/{Id}/PlaybackInfo", "POST", Summary = "Gets live playback media info for an item")]
public class GetPostedPlaybackInfo : PlaybackInfoRequest, IReturn<PlaybackInfoResponse>
{
}
[Route("/LiveStreams/Open", "POST", Summary = "Opens a media source")]
public class OpenMediaSource : LiveStreamRequest, IReturn<LiveStreamResponse>
{
}
[Route("/LiveStreams/Close", "POST", Summary = "Closes a media source")]
public class CloseMediaSource : IReturnVoid
{
[ApiMember(Name = "LiveStreamId", Description = "LiveStreamId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string LiveStreamId { get; set; }
}
[Route("/Playback/BitrateTest", "GET")]
public class GetBitrateTestBytes
{
[ApiMember(Name = "Size", Description = "Size", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")]
public long Size { get; set; }
public GetBitrateTestBytes()
{
// 100k
Size = 102400;
}
}
[Authenticated]
public class MediaInfoService : BaseApiService
{
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IDeviceManager _deviceManager;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IUserManager _userManager;
private readonly IJsonSerializer _json;
private readonly IAuthorizationContext _authContext;
public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, IServerConfigurationManager config, INetworkManager networkManager, IMediaEncoder mediaEncoder, IUserManager userManager, IJsonSerializer json, IAuthorizationContext authContext)
{
_mediaSourceManager = mediaSourceManager;
_deviceManager = deviceManager;
_libraryManager = libraryManager;
_config = config;
_networkManager = networkManager;
_mediaEncoder = mediaEncoder;
_userManager = userManager;
_json = json;
_authContext = authContext;
}
public object Get(GetBitrateTestBytes request)
{
var bytes = new byte[request.Size];
for (var i = 0; i < bytes.Length; i++)
{
bytes[i] = 0;
}
return ResultFactory.GetResult(null, bytes, "application/octet-stream");
}
public async Task<object> Get(GetPlaybackInfo request)
{
var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
return ToOptimizedResult(result);
}
public async Task<object> Post(OpenMediaSource request)
{
var result = await OpenMediaSource(request).ConfigureAwait(false);
return ToOptimizedResult(result);
}
private async Task<LiveStreamResponse> OpenMediaSource(OpenMediaSource request)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
var profile = request.DeviceProfile;
if (profile == null)
{
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
if (caps != null)
{
profile = caps.DeviceProfile;
}
}
if (profile != null)
{
var item = _libraryManager.GetItemById(request.ItemId);
SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate,
request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex,
request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, request.EnableDirectPlay, true, request.EnableDirectStream, true, true, true);
}
else
{
if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
{
result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
}
}
if (result.MediaSource != null)
{
NormalizeMediaSourceContainer(result.MediaSource, profile, DlnaProfileType.Video);
}
return result;
}
public void Post(CloseMediaSource request)
{
var task = _mediaSourceManager.CloseLiveStream(request.LiveStreamId);
Task.WaitAll(task);
}
public async Task<PlaybackInfoResponse> GetPlaybackInfo(GetPostedPlaybackInfo request)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
var profile = request.DeviceProfile;
//Logger.Info("GetPostedPlaybackInfo profile: {0}", _json.SerializeToString(profile));
if (profile == null)
{
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
if (caps != null)
{
profile = caps.DeviceProfile;
}
}
var info = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
if (profile != null)
{
var mediaSourceId = request.MediaSourceId;
SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, true, request.EnableDirectStream, request.EnableTranscoding, request.AllowVideoStreamCopy, request.AllowAudioStreamCopy);
}
if (request.AutoOpenLiveStream)
{
var mediaSource = string.IsNullOrWhiteSpace(request.MediaSourceId) ? info.MediaSources.FirstOrDefault() : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId, StringComparison.Ordinal));
if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
{
var openStreamResult = await OpenMediaSource(new OpenMediaSource
{
AudioStreamIndex = request.AudioStreamIndex,
DeviceProfile = request.DeviceProfile,
EnableDirectPlay = request.EnableDirectPlay,
EnableDirectStream = request.EnableDirectStream,
ItemId = request.Id,
MaxAudioChannels = request.MaxAudioChannels,
MaxStreamingBitrate = request.MaxStreamingBitrate,
PlaySessionId = info.PlaySessionId,
StartTimeTicks = request.StartTimeTicks,
SubtitleStreamIndex = request.SubtitleStreamIndex,
UserId = request.UserId,
OpenToken = mediaSource.OpenToken,
//EnableMediaProbe = request.EnableMediaProbe
}).ConfigureAwait(false);
info.MediaSources = new MediaSourceInfo[] { openStreamResult.MediaSource };
}
}
if (info.MediaSources != null)
{
foreach (var mediaSource in info.MediaSources)
{
NormalizeMediaSourceContainer(mediaSource, profile, DlnaProfileType.Video);
}
}
return info;
}
private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
{
mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
}
public async Task<object> Post(GetPostedPlaybackInfo request)
{
var result = await GetPlaybackInfo(request).ConfigureAwait(false);
return ToOptimizedResult(result);
}
private T Clone<T>(T obj)
{
// Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
// Should we move this directly into MediaSourceManager?
var json = _json.SerializeToString(obj);
return _json.DeserializeFromString<T>(json);
}
private async Task<PlaybackInfoResponse> GetPlaybackInfo(Guid id, Guid userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null)
{
var user = _userManager.GetUserById(userId);
var item = _libraryManager.GetItemById(id);
var result = new PlaybackInfoResponse();
if (string.IsNullOrWhiteSpace(liveStreamId))
{
IEnumerable<MediaSourceInfo> mediaSources;
try
{
// TODO handle supportedLiveMediaTypes ?
mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, user, true, false, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
mediaSources = new List<MediaSourceInfo>();
// TODO PlaybackException ??
//result.ErrorCode = ex.ErrorCode;
}
result.MediaSources = mediaSources.ToArray();
if (!string.IsNullOrWhiteSpace(mediaSourceId))
{
result.MediaSources = result.MediaSources
.Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
.ToArray();
}
}
else
{
var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
result.MediaSources = new MediaSourceInfo[] { mediaSource };
}
if (result.MediaSources.Length == 0)
{
if (!result.ErrorCode.HasValue)
{
result.ErrorCode = PlaybackErrorCode.NoCompatibleStream;
}
}
else
{
result.MediaSources = Clone(result.MediaSources);
result.PlaySessionId = Guid.NewGuid().ToString("N");
}
return result;
}
private void SetDeviceSpecificData(Guid itemId,
PlaybackInfoResponse result,
DeviceProfile profile,
AuthorizationInfo auth,
long? maxBitrate,
long startTimeTicks,
string mediaSourceId,
int? audioStreamIndex,
int? subtitleStreamIndex,
int? maxAudioChannels,
Guid userId,
bool enableDirectPlay,
bool forceDirectPlayRemoteMediaSource,
bool enableDirectStream,
bool enableTranscoding,
bool allowVideoStreamCopy,
bool allowAudioStreamCopy)
{
var item = _libraryManager.GetItemById(itemId);
foreach (var mediaSource in result.MediaSources)
{
SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, forceDirectPlayRemoteMediaSource, enableDirectStream, enableTranscoding, allowVideoStreamCopy, allowAudioStreamCopy);
}
SortMediaSources(result, maxBitrate);
}
private void SetDeviceSpecificData(BaseItem item,
MediaSourceInfo mediaSource,
DeviceProfile profile,
AuthorizationInfo auth,
long? maxBitrate,
long startTimeTicks,
string mediaSourceId,
int? audioStreamIndex,
int? subtitleStreamIndex,
int? maxAudioChannels,
string playSessionId,
Guid userId,
bool enableDirectPlay,
bool forceDirectPlayRemoteMediaSource,
bool enableDirectStream,
bool enableTranscoding,
bool allowVideoStreamCopy,
bool allowAudioStreamCopy)
{
var streamBuilder = new StreamBuilder(_mediaEncoder, Logger);
var options = new VideoOptions
{
MediaSources = new MediaSourceInfo[] { mediaSource },
Context = EncodingContext.Streaming,
DeviceId = auth.DeviceId,
ItemId = item.Id,
Profile = profile,
MaxAudioChannels = maxAudioChannels
};
if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
{
options.MediaSourceId = mediaSourceId;
options.AudioStreamIndex = audioStreamIndex;
options.SubtitleStreamIndex = subtitleStreamIndex;
}
var user = _userManager.GetUserById(userId);
if (!enableDirectPlay)
{
mediaSource.SupportsDirectPlay = false;
}
if (!enableDirectStream)
{
mediaSource.SupportsDirectStream = false;
}
if (!enableTranscoding)
{
mediaSource.SupportsTranscoding = false;
}
if (item is Audio)
{
Logger.Info("User policy for {0}. EnableAudioPlaybackTranscoding: {1}", user.Name, user.Policy.EnableAudioPlaybackTranscoding);
}
else
{
Logger.Info("User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
user.Name,
user.Policy.EnablePlaybackRemuxing,
user.Policy.EnableVideoPlaybackTranscoding,
user.Policy.EnableAudioPlaybackTranscoding);
}
if (mediaSource.SupportsDirectPlay)
{
if (mediaSource.IsRemote && forceDirectPlayRemoteMediaSource)
{
}
else
{
var supportsDirectStream = mediaSource.SupportsDirectStream;
// Dummy this up to fool StreamBuilder
mediaSource.SupportsDirectStream = true;
options.MaxBitrate = maxBitrate;
if (item is Audio)
{
if (!user.Policy.EnableAudioPlaybackTranscoding)
{
options.ForceDirectPlay = true;
}
}
else if (item is Video)
{
if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
{
options.ForceDirectPlay = true;
}
}
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
streamBuilder.BuildAudioItem(options) :
streamBuilder.BuildVideoItem(options);
if (streamInfo == null || !streamInfo.IsDirectStream)
{
mediaSource.SupportsDirectPlay = false;
}
// Set this back to what it was
mediaSource.SupportsDirectStream = supportsDirectStream;
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
}
if (mediaSource.SupportsDirectStream)
{
options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
if (item is Audio)
{
if (!user.Policy.EnableAudioPlaybackTranscoding)
{
options.ForceDirectStream = true;
}
}
else if (item is Video)
{
if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
{
options.ForceDirectStream = true;
}
}
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
streamBuilder.BuildAudioItem(options) :
streamBuilder.BuildVideoItem(options);
if (streamInfo == null || !streamInfo.IsDirectStream)
{
mediaSource.SupportsDirectStream = false;
}
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
if (mediaSource.SupportsTranscoding)
{
options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
streamBuilder.BuildAudioItem(options) :
streamBuilder.BuildVideoItem(options);
if (streamInfo != null)
{
streamInfo.PlaySessionId = playSessionId;
if (streamInfo.PlayMethod == PlayMethod.Transcode)
{
streamInfo.StartPositionTicks = startTimeTicks;
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
if (!allowVideoStreamCopy)
{
mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
}
if (!allowAudioStreamCopy)
{
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
}
mediaSource.TranscodingContainer = streamInfo.Container;
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
}
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
}
private long? GetMaxBitrate(long? clientMaxBitrate, User user)
{
var maxBitrate = clientMaxBitrate;
var remoteClientMaxBitrate = user == null ? 0 : user.Policy.RemoteClientBitrateLimit;
if (remoteClientMaxBitrate <= 0)
{
remoteClientMaxBitrate = _config.Configuration.RemoteClientBitrateLimit;
}
if (remoteClientMaxBitrate > 0)
{
var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.RemoteIp);
Logger.Info("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.RemoteIp, isInLocalNetwork);
if (!isInLocalNetwork)
{
maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
}
}
return maxBitrate;
}
private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
{
var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
mediaSource.TranscodeReasons = info.TranscodeReasons;
foreach (var profile in profiles)
{
foreach (var stream in mediaSource.MediaStreams)
{
if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
{
stream.DeliveryMethod = profile.DeliveryMethod;
if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
{
stream.DeliveryUrl = profile.Url.TrimStart('-');
stream.IsExternalUrl = profile.IsExternalUrl;
}
}
}
}
}
private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
{
var originalList = result.MediaSources.ToList();
result.MediaSources = result.MediaSources.OrderBy(i =>
{
// Nothing beats direct playing a file
if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
{
return 0;
}
return 1;
}).ThenBy(i =>
{
// Let's assume direct streaming a file is just as desirable as direct playing a remote url
if (i.SupportsDirectPlay || i.SupportsDirectStream)
{
return 0;
}
return 1;
}).ThenBy(i =>
{
switch (i.Protocol)
{
case MediaProtocol.File:
return 0;
default:
return 1;
}
}).ThenBy(i =>
{
if (maxBitrate.HasValue)
{
if (i.Bitrate.HasValue)
{
if (i.Bitrate.Value <= maxBitrate.Value)
{
return 0;
}
return 2;
}
}
return 1;
}).ThenBy(originalList.IndexOf)
.ToArray();
}
}
}

View File

@ -0,0 +1,67 @@
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using System.Collections.Generic;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
namespace MediaBrowser.Api.Playback.Progressive
{
/// <summary>
/// Class GetAudioStream
/// </summary>
[Route("/Audio/{Id}/stream.{Container}", "GET", Summary = "Gets an audio stream")]
[Route("/Audio/{Id}/stream", "GET", Summary = "Gets an audio stream")]
[Route("/Audio/{Id}/stream.{Container}", "HEAD", Summary = "Gets an audio stream")]
[Route("/Audio/{Id}/stream", "HEAD", Summary = "Gets an audio stream")]
public class GetAudioStream : StreamRequest
{
}
/// <summary>
/// Class AudioService
/// </summary>
// TODO: In order to autheneticate this in the future, Dlna playback will require updating
//[Authenticated]
public class AudioService : BaseProgressiveStreamingService
{
public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, IImageProcessor imageProcessor, IEnvironmentInfo environmentInfo) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext, imageProcessor, environmentInfo)
{
}
/// <summary>
/// Gets the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public Task<object> Get(GetAudioStream request)
{
return ProcessRequest(request, false);
}
/// <summary>
/// Gets the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public Task<object> Head(GetAudioStream request)
{
return ProcessRequest(request, true);
}
protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
{
return EncodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
}
}
}

View File

@ -0,0 +1,429 @@
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
namespace MediaBrowser.Api.Playback.Progressive
{
/// <summary>
/// Class BaseProgressiveStreamingService
/// </summary>
public abstract class BaseProgressiveStreamingService : BaseStreamingService
{
protected readonly IImageProcessor ImageProcessor;
protected readonly IEnvironmentInfo EnvironmentInfo;
public BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, IImageProcessor imageProcessor, IEnvironmentInfo environmentInfo) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext)
{
ImageProcessor = imageProcessor;
EnvironmentInfo = environmentInfo;
}
/// <summary>
/// Gets the output file extension.
/// </summary>
/// <param name="state">The state.</param>
/// <returns>System.String.</returns>
protected override string GetOutputFileExtension(StreamState state)
{
var ext = base.GetOutputFileExtension(state);
if (!string.IsNullOrEmpty(ext))
{
return ext;
}
var isVideoRequest = state.VideoRequest != null;
// Try to infer based on the desired video codec
if (isVideoRequest)
{
var videoCodec = state.VideoRequest.VideoCodec;
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
return ".ts";
}
if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
{
return ".ogv";
}
if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
{
return ".webm";
}
if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
{
return ".asf";
}
}
// Try to infer based on the desired audio codec
if (!isVideoRequest)
{
var audioCodec = state.Request.AudioCodec;
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".aac";
}
if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".mp3";
}
if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".ogg";
}
if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".wma";
}
}
return null;
}
/// <summary>
/// Gets the type of the transcoding job.
/// </summary>
/// <value>The type of the transcoding job.</value>
protected override TranscodingJobType TranscodingJobType
{
get { return TranscodingJobType.Progressive; }
}
/// <summary>
/// Processes the request.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
/// <returns>Task.</returns>
protected async Task<object> ProcessRequest(StreamRequest request, bool isHeadRequest)
{
var cancellationTokenSource = new CancellationTokenSource();
var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
var responseHeaders = new Dictionary<string, string>();
if (request.Static && state.DirectStreamProvider != null)
{
AddDlnaHeaders(state, responseHeaders, true);
using (state)
{
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// TODO: Don't hardcode this
outputHeaders["Content-Type"] = MediaBrowser.Model.Net.MimeTypes.GetMimeType("file.ts");
return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, EnvironmentInfo, CancellationToken.None)
{
AllowEndOfFile = false
};
}
}
// Static remote stream
if (request.Static && state.InputProtocol == MediaProtocol.Http)
{
AddDlnaHeaders(state, responseHeaders, true);
using (state)
{
return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
}
}
if (request.Static && state.InputProtocol != MediaProtocol.File)
{
throw new ArgumentException(string.Format("Input protocol {0} cannot be streamed statically.", state.InputProtocol));
}
var outputPath = state.OutputFilePath;
var outputPathExists = FileSystem.FileExists(outputPath);
var transcodingJob = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
var isTranscodeCached = outputPathExists && transcodingJob != null;
AddDlnaHeaders(state, responseHeaders, request.Static || isTranscodeCached);
// Static stream
if (request.Static)
{
var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
using (state)
{
if (state.MediaSource.IsInfiniteStream)
{
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
outputHeaders["Content-Type"] = contentType;
return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, Logger, EnvironmentInfo, CancellationToken.None)
{
AllowEndOfFile = false
};
}
TimeSpan? cacheDuration = null;
if (!string.IsNullOrEmpty(request.Tag))
{
cacheDuration = TimeSpan.FromDays(365);
}
return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
ResponseHeaders = responseHeaders,
ContentType = contentType,
IsHeadRequest = isHeadRequest,
Path = state.MediaPath,
CacheDuration = cacheDuration
}).ConfigureAwait(false);
}
}
//// Not static but transcode cache file exists
//if (isTranscodeCached && state.VideoRequest == null)
//{
// var contentType = state.GetMimeType(outputPath);
// try
// {
// if (transcodingJob != null)
// {
// ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
// }
// return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
// {
// ResponseHeaders = responseHeaders,
// ContentType = contentType,
// IsHeadRequest = isHeadRequest,
// Path = outputPath,
// FileShare = FileShareMode.ReadWrite,
// OnComplete = () =>
// {
// if (transcodingJob != null)
// {
// ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
// }
// }
// }).ConfigureAwait(false);
// }
// finally
// {
// state.Dispose();
// }
//}
// Need to start ffmpeg
try
{
return await GetStreamResult(request, state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
}
catch
{
state.Dispose();
throw;
}
}
/// <summary>
/// Gets the static remote stream result.
/// </summary>
/// <param name="state">The state.</param>
/// <param name="responseHeaders">The response headers.</param>
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
/// <param name="cancellationTokenSource">The cancellation token source.</param>
/// <returns>Task{System.Object}.</returns>
private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
{
string useragent = null;
state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
var trySupportSeek = false;
var options = new HttpRequestOptions
{
Url = state.MediaPath,
UserAgent = useragent,
BufferContent = false,
CancellationToken = cancellationTokenSource.Token
};
if (trySupportSeek)
{
if (!string.IsNullOrWhiteSpace(Request.QueryString["Range"]))
{
options.RequestHeaders["Range"] = Request.QueryString["Range"];
}
}
var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
if (trySupportSeek)
{
foreach (var name in new[] { "Content-Range", "Accept-Ranges" })
{
var val = response.Headers[name];
if (!string.IsNullOrWhiteSpace(val))
{
responseHeaders[name] = val;
}
}
}
else
{
responseHeaders["Accept-Ranges"] = "none";
}
// Seeing cases of -1 here
if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
{
responseHeaders["Content-Length"] = response.ContentLength.Value.ToString(UsCulture);
}
if (isHeadRequest)
{
using (response)
{
return ResultFactory.GetResult(null, new byte[] { }, response.ContentType, responseHeaders);
}
}
var result = new StaticRemoteStreamWriter(response);
result.Headers["Content-Type"] = response.ContentType;
// Add the response headers to the result object
foreach (var header in responseHeaders)
{
result.Headers[header.Key] = header.Value;
}
return result;
}
/// <summary>
/// Gets the stream result.
/// </summary>
/// <param name="state">The state.</param>
/// <param name="responseHeaders">The response headers.</param>
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
/// <param name="cancellationTokenSource">The cancellation token source.</param>
/// <returns>Task{System.Object}.</returns>
private async Task<object> GetStreamResult(StreamRequest request, StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
{
// Use the command line args with a dummy playlist path
var outputPath = state.OutputFilePath;
responseHeaders["Accept-Ranges"] = "none";
var contentType = state.GetMimeType(outputPath);
// TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
// What we really want to do is hunt that down and remove that
var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
if (contentLength.HasValue)
{
responseHeaders["Content-Length"] = contentLength.Value.ToString(UsCulture);
}
// Headers only
if (isHeadRequest)
{
var streamResult = ResultFactory.GetResult(null, new byte[] { }, contentType, responseHeaders);
var hasHeaders = streamResult as IHasHeaders;
if (hasHeaders != null)
{
if (contentLength.HasValue)
{
hasHeaders.Headers["Content-Length"] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
}
else
{
if (hasHeaders.Headers.ContainsKey("Content-Length"))
{
hasHeaders.Headers.Remove("Content-Length");
}
}
}
return streamResult;
}
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{
TranscodingJob job;
if (!FileSystem.FileExists(outputPath))
{
job = await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false);
}
else
{
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
state.Dispose();
}
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
outputHeaders["Content-Type"] = contentType;
// Add the response headers to the result object
foreach (var item in responseHeaders)
{
outputHeaders[item.Key] = item.Value;
}
return new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, EnvironmentInfo, CancellationToken.None);
}
finally
{
transcodingLock.Release();
}
}
/// <summary>
/// Gets the length of the estimated content.
/// </summary>
/// <param name="state">The state.</param>
/// <returns>System.Nullable{System.Int64}.</returns>
private long? GetEstimatedContentLength(StreamState state)
{
var totalBitrate = state.TotalOutputBitrate ?? 0;
if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
{
return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
}
return null;
}
}
}

View File

@ -0,0 +1,193 @@
using MediaBrowser.Model.Logging;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.IO;
using MediaBrowser.Controller.Net;
using System.Collections.Generic;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
namespace MediaBrowser.Api.Playback.Progressive
{
public class ProgressiveFileCopier : IAsyncStreamWriter, IHasHeaders
{
private readonly IFileSystem _fileSystem;
private readonly TranscodingJob _job;
private readonly ILogger _logger;
private readonly string _path;
private readonly CancellationToken _cancellationToken;
private readonly Dictionary<string, string> _outputHeaders;
const int StreamCopyToBufferSize = 81920;
private long _bytesWritten = 0;
public long StartPosition { get; set; }
public bool AllowEndOfFile = true;
private readonly IDirectStreamProvider _directStreamProvider;
private readonly IEnvironmentInfo _environment;
public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, IEnvironmentInfo environment, CancellationToken cancellationToken)
{
_fileSystem = fileSystem;
_path = path;
_outputHeaders = outputHeaders;
_job = job;
_logger = logger;
_cancellationToken = cancellationToken;
_environment = environment;
}
public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, IEnvironmentInfo environment, CancellationToken cancellationToken)
{
_directStreamProvider = directStreamProvider;
_outputHeaders = outputHeaders;
_job = job;
_logger = logger;
_cancellationToken = cancellationToken;
_environment = environment;
}
public IDictionary<string, string> Headers
{
get
{
return _outputHeaders;
}
}
private Stream GetInputStream(bool allowAsyncFileRead)
{
var fileOpenOptions = FileOpenOptions.SequentialScan;
if (allowAsyncFileRead)
{
fileOpenOptions |= FileOpenOptions.Asynchronous;
}
return _fileSystem.GetFileStream(_path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, fileOpenOptions);
}
public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
{
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
try
{
if (_directStreamProvider != null)
{
await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
return;
}
var eofCount = 0;
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
var allowAsyncFileRead = _environment.OperatingSystem != Model.System.OperatingSystem.Windows;
using (var inputStream = GetInputStream(allowAsyncFileRead))
{
if (StartPosition > 0)
{
inputStream.Position = StartPosition;
}
while (eofCount < 20 || !AllowEndOfFile)
{
int bytesRead;
if (allowAsyncFileRead)
{
bytesRead = await CopyToInternalAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
}
else
{
bytesRead = await CopyToInternalAsyncWithSyncRead(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
}
//var position = fs.Position;
//_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
if (bytesRead == 0)
{
if (_job == null || _job.HasExited)
{
eofCount++;
}
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
else
{
eofCount = 0;
}
}
}
}
finally
{
if (_job != null)
{
ApiEntryPoint.Instance.OnTranscodeEndRequest(_job);
}
}
}
private async Task<int> CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
{
var array = new byte[StreamCopyToBufferSize];
int bytesRead;
int totalBytesRead = 0;
while ((bytesRead = source.Read(array, 0, array.Length)) != 0)
{
var bytesToWrite = bytesRead;
if (bytesToWrite > 0)
{
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
_bytesWritten += bytesRead;
totalBytesRead += bytesRead;
if (_job != null)
{
_job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
}
}
}
return totalBytesRead;
}
private async Task<int> CopyToInternalAsync(Stream source, Stream destination, CancellationToken cancellationToken)
{
var array = new byte[StreamCopyToBufferSize];
int bytesRead;
int totalBytesRead = 0;
while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
var bytesToWrite = bytesRead;
if (bytesToWrite > 0)
{
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
_bytesWritten += bytesRead;
totalBytesRead += bytesRead;
if (_job != null)
{
_job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
}
}
}
return totalBytesRead;
}
}
}

View File

@ -0,0 +1,100 @@
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
namespace MediaBrowser.Api.Playback.Progressive
{
/// <summary>
/// Class GetVideoStream
/// </summary>
[Route("/Videos/{Id}/stream.mpegts", "GET")]
[Route("/Videos/{Id}/stream.ts", "GET")]
[Route("/Videos/{Id}/stream.webm", "GET")]
[Route("/Videos/{Id}/stream.asf", "GET")]
[Route("/Videos/{Id}/stream.wmv", "GET")]
[Route("/Videos/{Id}/stream.ogv", "GET")]
[Route("/Videos/{Id}/stream.mp4", "GET")]
[Route("/Videos/{Id}/stream.m4v", "GET")]
[Route("/Videos/{Id}/stream.mkv", "GET")]
[Route("/Videos/{Id}/stream.mpeg", "GET")]
[Route("/Videos/{Id}/stream.mpg", "GET")]
[Route("/Videos/{Id}/stream.avi", "GET")]
[Route("/Videos/{Id}/stream.m2ts", "GET")]
[Route("/Videos/{Id}/stream.3gp", "GET")]
[Route("/Videos/{Id}/stream.wmv", "GET")]
[Route("/Videos/{Id}/stream.wtv", "GET")]
[Route("/Videos/{Id}/stream.mov", "GET")]
[Route("/Videos/{Id}/stream.iso", "GET")]
[Route("/Videos/{Id}/stream.flv", "GET")]
[Route("/Videos/{Id}/stream", "GET")]
[Route("/Videos/{Id}/stream.ts", "HEAD")]
[Route("/Videos/{Id}/stream.webm", "HEAD")]
[Route("/Videos/{Id}/stream.asf", "HEAD")]
[Route("/Videos/{Id}/stream.wmv", "HEAD")]
[Route("/Videos/{Id}/stream.ogv", "HEAD")]
[Route("/Videos/{Id}/stream.mp4", "HEAD")]
[Route("/Videos/{Id}/stream.m4v", "HEAD")]
[Route("/Videos/{Id}/stream.mkv", "HEAD")]
[Route("/Videos/{Id}/stream.mpeg", "HEAD")]
[Route("/Videos/{Id}/stream.mpg", "HEAD")]
[Route("/Videos/{Id}/stream.avi", "HEAD")]
[Route("/Videos/{Id}/stream.3gp", "HEAD")]
[Route("/Videos/{Id}/stream.wmv", "HEAD")]
[Route("/Videos/{Id}/stream.wtv", "HEAD")]
[Route("/Videos/{Id}/stream.m2ts", "HEAD")]
[Route("/Videos/{Id}/stream.mov", "HEAD")]
[Route("/Videos/{Id}/stream.iso", "HEAD")]
[Route("/Videos/{Id}/stream.flv", "HEAD")]
[Route("/Videos/{Id}/stream", "HEAD")]
public class GetVideoStream : VideoStreamRequest
{
}
/// <summary>
/// Class VideoService
/// </summary>
// TODO: In order to autheneticate this in the future, Dlna playback will require updating
//[Authenticated]
public class VideoService : BaseProgressiveStreamingService
{
public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, IImageProcessor imageProcessor, IEnvironmentInfo environmentInfo) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext, imageProcessor, environmentInfo)
{
}
/// <summary>
/// Gets the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public Task<object> Get(GetVideoStream request)
{
return ProcessRequest(request, false);
}
/// <summary>
/// Heads the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public Task<object> Head(GetVideoStream request)
{
return ProcessRequest(request, true);
}
protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
{
return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultH264Preset());
}
}
}

View File

@ -0,0 +1,47 @@
using MediaBrowser.Common.Net;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
namespace MediaBrowser.Api.Playback
{
/// <summary>
/// Class StaticRemoteStreamWriter
/// </summary>
public class StaticRemoteStreamWriter : IAsyncStreamWriter, IHasHeaders
{
/// <summary>
/// The _input stream
/// </summary>
private readonly HttpResponseInfo _response;
/// <summary>
/// The _options
/// </summary>
private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
public StaticRemoteStreamWriter(HttpResponseInfo response)
{
_response = response;
}
/// <summary>
/// Gets the options.
/// </summary>
/// <value>The options.</value>
public IDictionary<string, string> Headers
{
get { return _options; }
}
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
{
using (_response)
{
await _response.Content.CopyToAsync(responseStream, 81920, cancellationToken).ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,64 @@
using System;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Services;
namespace MediaBrowser.Api.Playback
{
/// <summary>
/// Class StreamRequest
/// </summary>
public class StreamRequest : BaseEncodingJobOptions
{
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public Guid Id { get; set; }
[ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string MediaSourceId { get; set; }
[ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string DeviceId { get; set; }
[ApiMember(Name = "Container", Description = "Container", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Container { get; set; }
/// <summary>
/// Gets or sets the audio codec.
/// </summary>
/// <value>The audio codec.</value>
[ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string AudioCodec { get; set; }
[ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string DeviceProfileId { get; set; }
public string Params { get; set; }
public string PlaySessionId { get; set; }
public string Tag { get; set; }
public string SegmentContainer { get; set; }
public int? SegmentLength { get; set; }
public int? MinSegments { get; set; }
}
public class VideoStreamRequest : StreamRequest
{
/// <summary>
/// Gets a value indicating whether this instance has fixed resolution.
/// </summary>
/// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
public bool HasFixedResolution
{
get
{
return Width.HasValue || Height.HasValue;
}
}
public bool EnableSubtitlesInManifest { get; set; }
}
}

View File

@ -0,0 +1,259 @@
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using MediaBrowser.Controller.MediaEncoding;
namespace MediaBrowser.Api.Playback
{
public class StreamState : EncodingJobInfo, IDisposable
{
private readonly ILogger _logger;
private readonly IMediaSourceManager _mediaSourceManager;
public string RequestedUrl { get; set; }
public StreamRequest Request
{
get { return (StreamRequest)BaseRequest; }
set
{
BaseRequest = value;
IsVideoRequest = VideoRequest != null;
}
}
public TranscodingThrottler TranscodingThrottler { get; set; }
public VideoStreamRequest VideoRequest
{
get { return Request as VideoStreamRequest; }
}
/// <summary>
/// Gets or sets the log file stream.
/// </summary>
/// <value>The log file stream.</value>
public Stream LogFileStream { get; set; }
public IDirectStreamProvider DirectStreamProvider { get; set; }
public string WaitForPath { get; set; }
public bool IsOutputVideo
{
get { return Request is VideoStreamRequest; }
}
public int SegmentLength
{
get
{
if (Request.SegmentLength.HasValue)
{
return Request.SegmentLength.Value;
}
if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
var userAgent = UserAgent ?? string.Empty;
if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 ||
userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 ||
userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
{
if (IsSegmentedLiveStream)
{
return 6;
}
return 6;
}
if (IsSegmentedLiveStream)
{
return 3;
}
return 6;
}
return 3;
}
}
public int MinSegments
{
get
{
if (Request.MinSegments.HasValue)
{
return Request.MinSegments.Value;
}
return SegmentLength >= 10 ? 2 : 3;
}
}
public int HlsListSize
{
get
{
return 0;
}
}
public string UserAgent { get; set; }
public StreamState(IMediaSourceManager mediaSourceManager, ILogger logger, TranscodingJobType transcodingType)
: base(logger, mediaSourceManager, transcodingType)
{
_mediaSourceManager = mediaSourceManager;
_logger = logger;
}
public string MimeType { get; set; }
public bool EstimateContentLength { get; set; }
public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
public long? EncodingDurationTicks { get; set; }
public string GetMimeType(string outputPath, bool enableStreamDefault = true)
{
if (!string.IsNullOrEmpty(MimeType))
{
return MimeType;
}
return MimeTypes.GetMimeType(outputPath, enableStreamDefault);
}
public bool EnableDlnaHeaders { get; set; }
public void Dispose()
{
DisposeTranscodingThrottler();
DisposeLiveStream();
DisposeLogStream();
DisposeIsoMount();
TranscodingJob = null;
}
private void DisposeLogStream()
{
if (LogFileStream != null)
{
try
{
LogFileStream.Dispose();
}
catch (Exception ex)
{
_logger.ErrorException("Error disposing log stream", ex);
}
LogFileStream = null;
}
}
private void DisposeTranscodingThrottler()
{
if (TranscodingThrottler != null)
{
try
{
TranscodingThrottler.Dispose();
}
catch (Exception ex)
{
_logger.ErrorException("Error disposing TranscodingThrottler", ex);
}
TranscodingThrottler = null;
}
}
private async void DisposeLiveStream()
{
if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Request.LiveStreamId) && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
{
try
{
await _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error closing media source", ex);
}
}
}
public string OutputFilePath { get; set; }
public string ActualOutputVideoCodec
{
get
{
var codec = OutputVideoCodec;
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
{
var stream = VideoStream;
if (stream != null)
{
return stream.Codec;
}
return null;
}
return codec;
}
}
public string ActualOutputAudioCodec
{
get
{
var codec = OutputAudioCodec;
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
{
var stream = AudioStream;
if (stream != null)
{
return stream.Codec;
}
return null;
}
return codec;
}
}
public DeviceProfile DeviceProfile { get; set; }
public TranscodingJob TranscodingJob;
public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
{
ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
}
}
}

View File

@ -0,0 +1,176 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Logging;
using System;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Threading;
namespace MediaBrowser.Api.Playback
{
public class TranscodingThrottler : IDisposable
{
private readonly TranscodingJob _job;
private readonly ILogger _logger;
private ITimer _timer;
private bool _isPaused;
private readonly IConfigurationManager _config;
private readonly ITimerFactory _timerFactory;
private readonly IFileSystem _fileSystem;
public TranscodingThrottler(TranscodingJob job, ILogger logger, IConfigurationManager config, ITimerFactory timerFactory, IFileSystem fileSystem)
{
_job = job;
_logger = logger;
_config = config;
_timerFactory = timerFactory;
_fileSystem = fileSystem;
}
private EncodingOptions GetOptions()
{
return _config.GetConfiguration<EncodingOptions>("encoding");
}
public void Start()
{
_timer = _timerFactory.Create(TimerCallback, null, 5000, 5000);
}
private void TimerCallback(object state)
{
if (_job.HasExited)
{
DisposeTimer();
return;
}
var options = GetOptions();
if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds))
{
PauseTranscoding();
}
else
{
UnpauseTranscoding();
}
}
private void PauseTranscoding()
{
if (!_isPaused)
{
_logger.Debug("Sending pause command to ffmpeg");
try
{
_job.Process.StandardInput.Write("c");
_isPaused = true;
}
catch (Exception ex)
{
_logger.ErrorException("Error pausing transcoding", ex);
}
}
}
public void UnpauseTranscoding()
{
if (_isPaused)
{
_logger.Debug("Sending unpause command to ffmpeg");
try
{
_job.Process.StandardInput.WriteLine();
_isPaused = false;
}
catch (Exception ex)
{
_logger.ErrorException("Error unpausing transcoding", ex);
}
}
}
private bool IsThrottleAllowed(TranscodingJob job, int thresholdSeconds)
{
var bytesDownloaded = job.BytesDownloaded ?? 0;
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
var path = job.Path;
var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks;
if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
{
// HLS - time-based consideration
var targetGap = gapLengthInTicks;
var gap = transcodingPositionTicks - downloadPositionTicks;
if (gap < targetGap)
{
//_logger.Debug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
return false;
}
//_logger.Debug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
return true;
}
if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
{
// Progressive Streaming - byte-based consideration
try
{
var bytesTranscoded = job.BytesTranscoded ?? _fileSystem.GetFileInfo(path).Length;
// Estimate the bytes the transcoder should be ahead
double gapFactor = gapLengthInTicks;
gapFactor /= transcodingPositionTicks;
var targetGap = bytesTranscoded * gapFactor;
var gap = bytesTranscoded - bytesDownloaded;
if (gap < targetGap)
{
//_logger.Debug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
return false;
}
//_logger.Debug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
return true;
}
catch
{
//_logger.Error("Error getting output size");
return false;
}
}
//_logger.Debug("No throttle data for " + path);
return false;
}
public void Stop()
{
DisposeTimer();
UnpauseTranscoding();
}
public void Dispose()
{
DisposeTimer();
}
private void DisposeTimer()
{
if (_timer != null)
{
_timer.Dispose();
_timer = null;
}
}
}
}

View File

@ -0,0 +1,349 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Api.Playback.Hls;
using MediaBrowser.Api.Playback.Progressive;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
namespace MediaBrowser.Api.Playback
{
public class BaseUniversalRequest
{
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public Guid Id { get; set; }
[ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string MediaSourceId { get; set; }
[ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string DeviceId { get; set; }
public Guid UserId { get; set; }
public string AudioCodec { get; set; }
public string Container { get; set; }
public int? MaxAudioChannels { get; set; }
public int? TranscodingAudioChannels { get; set; }
public long? MaxStreamingBitrate { get; set; }
[ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public long? StartTimeTicks { get; set; }
public string TranscodingContainer { get; set; }
public string TranscodingProtocol { get; set; }
public int? MaxAudioSampleRate { get; set; }
public int? MaxAudioBitDepth { get; set; }
public bool EnableRedirection { get; set; }
public bool EnableRemoteMedia { get; set; }
public bool BreakOnNonKeyFrames { get; set; }
public BaseUniversalRequest()
{
EnableRedirection = true;
}
}
[Route("/Audio/{Id}/universal.{Container}", "GET", Summary = "Gets an audio stream")]
[Route("/Audio/{Id}/universal", "GET", Summary = "Gets an audio stream")]
[Route("/Audio/{Id}/universal.{Container}", "HEAD", Summary = "Gets an audio stream")]
[Route("/Audio/{Id}/universal", "HEAD", Summary = "Gets an audio stream")]
public class GetUniversalAudioStream : BaseUniversalRequest
{
}
[Authenticated]
public class UniversalAudioService : BaseApiService
{
public UniversalAudioService(IServerConfigurationManager serverConfigurationManager, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, IDeviceManager deviceManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, IImageProcessor imageProcessor, INetworkManager networkManager, IEnvironmentInfo environmentInfo)
{
ServerConfigurationManager = serverConfigurationManager;
UserManager = userManager;
LibraryManager = libraryManager;
IsoManager = isoManager;
MediaEncoder = mediaEncoder;
FileSystem = fileSystem;
DlnaManager = dlnaManager;
DeviceManager = deviceManager;
SubtitleEncoder = subtitleEncoder;
MediaSourceManager = mediaSourceManager;
ZipClient = zipClient;
JsonSerializer = jsonSerializer;
AuthorizationContext = authorizationContext;
ImageProcessor = imageProcessor;
NetworkManager = networkManager;
EnvironmentInfo = environmentInfo;
}
protected IServerConfigurationManager ServerConfigurationManager { get; private set; }
protected IUserManager UserManager { get; private set; }
protected ILibraryManager LibraryManager { get; private set; }
protected IIsoManager IsoManager { get; private set; }
protected IMediaEncoder MediaEncoder { get; private set; }
protected IFileSystem FileSystem { get; private set; }
protected IDlnaManager DlnaManager { get; private set; }
protected IDeviceManager DeviceManager { get; private set; }
protected ISubtitleEncoder SubtitleEncoder { get; private set; }
protected IMediaSourceManager MediaSourceManager { get; private set; }
protected IZipClient ZipClient { get; private set; }
protected IJsonSerializer JsonSerializer { get; private set; }
protected IAuthorizationContext AuthorizationContext { get; private set; }
protected IImageProcessor ImageProcessor { get; private set; }
protected INetworkManager NetworkManager { get; private set; }
protected IEnvironmentInfo EnvironmentInfo { get; private set; }
public Task<object> Get(GetUniversalAudioStream request)
{
return GetUniversalStream(request, false);
}
public Task<object> Head(GetUniversalAudioStream request)
{
return GetUniversalStream(request, true);
}
private DeviceProfile GetDeviceProfile(GetUniversalAudioStream request)
{
var deviceProfile = new DeviceProfile();
var directPlayProfiles = new List<DirectPlayProfile>();
var containers = (request.Container ?? string.Empty).Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var container in containers)
{
var parts = container.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
directPlayProfiles.Add(new DirectPlayProfile
{
Type = DlnaProfileType.Audio,
Container = parts[0],
AudioCodec = audioCodecs
});
}
deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
deviceProfile.TranscodingProfiles = new[]
{
new TranscodingProfile
{
Type = DlnaProfileType.Audio,
Context = EncodingContext.Streaming,
Container = request.TranscodingContainer,
AudioCodec = request.AudioCodec,
Protocol = request.TranscodingProtocol,
BreakOnNonKeyFrames = request.BreakOnNonKeyFrames,
MaxAudioChannels = request.TranscodingAudioChannels.HasValue ? request.TranscodingAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : null
}
};
var codecProfiles = new List<CodecProfile>();
var conditions = new List<ProfileCondition>();
if (request.MaxAudioSampleRate.HasValue)
{
// codec profile
conditions.Add(new ProfileCondition
{
Condition = ProfileConditionType.LessThanEqual,
IsRequired = false,
Property = ProfileConditionValue.AudioSampleRate,
Value = request.MaxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
});
}
if (request.MaxAudioBitDepth.HasValue)
{
// codec profile
conditions.Add(new ProfileCondition
{
Condition = ProfileConditionType.LessThanEqual,
IsRequired = false,
Property = ProfileConditionValue.AudioBitDepth,
Value = request.MaxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
});
}
if (request.MaxAudioChannels.HasValue)
{
// codec profile
conditions.Add(new ProfileCondition
{
Condition = ProfileConditionType.LessThanEqual,
IsRequired = false,
Property = ProfileConditionValue.AudioChannels,
Value = request.MaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
});
}
if (conditions.Count > 0)
{
// codec profile
codecProfiles.Add(new CodecProfile
{
Type = CodecType.Audio,
Container = request.Container,
Conditions = conditions.ToArray()
});
}
deviceProfile.CodecProfiles = codecProfiles.ToArray();
return deviceProfile;
}
private async Task<object> GetUniversalStream(GetUniversalAudioStream request, bool isHeadRequest)
{
var deviceProfile = GetDeviceProfile(request);
AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
var mediaInfoService = new MediaInfoService(MediaSourceManager, DeviceManager, LibraryManager, ServerConfigurationManager, NetworkManager, MediaEncoder, UserManager, JsonSerializer, AuthorizationContext)
{
Request = Request
};
var playbackInfoResult = await mediaInfoService.GetPlaybackInfo(new GetPostedPlaybackInfo
{
Id = request.Id,
MaxAudioChannels = request.MaxAudioChannels,
MaxStreamingBitrate = request.MaxStreamingBitrate,
StartTimeTicks = request.StartTimeTicks,
UserId = request.UserId,
DeviceProfile = deviceProfile,
MediaSourceId = request.MediaSourceId
}).ConfigureAwait(false);
var mediaSource = playbackInfoResult.MediaSources[0];
if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
{
if (request.EnableRedirection)
{
if (mediaSource.IsRemote && request.EnableRemoteMedia)
{
return ResultFactory.GetRedirectResult(mediaSource.Path);
}
}
}
var isStatic = mediaSource.SupportsDirectStream;
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
{
var service = new DynamicHlsService(ServerConfigurationManager,
UserManager,
LibraryManager,
IsoManager,
MediaEncoder,
FileSystem,
DlnaManager,
SubtitleEncoder,
DeviceManager,
MediaSourceManager,
ZipClient,
JsonSerializer,
AuthorizationContext,
NetworkManager)
{
Request = Request
};
var transcodingProfile = deviceProfile.TranscodingProfiles[0];
var newRequest = new GetMasterHlsAudioPlaylist
{
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
AudioCodec = transcodingProfile.AudioCodec,
Container = ".m3u8",
DeviceId = request.DeviceId,
Id = request.Id,
MaxAudioChannels = request.MaxAudioChannels,
MediaSourceId = mediaSource.Id,
PlaySessionId = playbackInfoResult.PlaySessionId,
StartTimeTicks = request.StartTimeTicks,
Static = isStatic,
SegmentContainer = request.TranscodingContainer,
AudioSampleRate = request.MaxAudioSampleRate,
MaxAudioBitDepth = request.MaxAudioBitDepth,
BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames,
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
};
if (isHeadRequest)
{
return await service.Head(newRequest).ConfigureAwait(false);
}
return await service.Get(newRequest).ConfigureAwait(false);
}
else
{
var service = new AudioService(ServerConfigurationManager,
UserManager,
LibraryManager,
IsoManager,
MediaEncoder,
FileSystem,
DlnaManager,
SubtitleEncoder,
DeviceManager,
MediaSourceManager,
ZipClient,
JsonSerializer,
AuthorizationContext,
ImageProcessor,
EnvironmentInfo)
{
Request = Request
};
var newRequest = new GetAudioStream
{
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
AudioCodec = request.AudioCodec,
Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
DeviceId = request.DeviceId,
Id = request.Id,
MaxAudioChannels = request.MaxAudioChannels,
MediaSourceId = mediaSource.Id,
PlaySessionId = playbackInfoResult.PlaySessionId,
StartTimeTicks = request.StartTimeTicks,
Static = isStatic,
AudioSampleRate = request.MaxAudioSampleRate,
MaxAudioBitDepth = request.MaxAudioBitDepth,
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
};
if (isHeadRequest)
{
return await service.Head(newRequest).ConfigureAwait(false);
}
return await service.Get(newRequest).ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,201 @@
using BDInfo;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Text;
namespace MediaBrowser.MediaEncoding.BdInfo
{
/// <summary>
/// Class BdInfoExaminer
/// </summary>
public class BdInfoExaminer : IBlurayExaminer
{
private readonly IFileSystem _fileSystem;
private readonly ITextEncoding _textEncoding;
public BdInfoExaminer(IFileSystem fileSystem, ITextEncoding textEncoding)
{
_fileSystem = fileSystem;
_textEncoding = textEncoding;
}
/// <summary>
/// Gets the disc info.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>BlurayDiscInfo.</returns>
public BlurayDiscInfo GetDiscInfo(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentNullException("path");
}
var bdrom = new BDROM(path, _fileSystem, _textEncoding);
bdrom.Scan();
// Get the longest playlist
var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid);
var outputStream = new BlurayDiscInfo
{
MediaStreams = new MediaStream[] {}
};
if (playlist == null)
{
return outputStream;
}
outputStream.Chapters = playlist.Chapters.ToArray();
outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks;
var mediaStreams = new List<MediaStream>();
foreach (var stream in playlist.SortedStreams)
{
var videoStream = stream as TSVideoStream;
if (videoStream != null)
{
AddVideoStream(mediaStreams, videoStream);
continue;
}
var audioStream = stream as TSAudioStream;
if (audioStream != null)
{
AddAudioStream(mediaStreams, audioStream);
continue;
}
var textStream = stream as TSTextStream;
if (textStream != null)
{
AddSubtitleStream(mediaStreams, textStream);
continue;
}
var graphicsStream = stream as TSGraphicsStream;
if (graphicsStream != null)
{
AddSubtitleStream(mediaStreams, graphicsStream);
}
}
outputStream.MediaStreams = mediaStreams.ToArray();
outputStream.PlaylistName = playlist.Name;
if (playlist.StreamClips != null && playlist.StreamClips.Any())
{
// Get the files in the playlist
outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToArray();
}
return outputStream;
}
/// <summary>
/// Adds the video stream.
/// </summary>
/// <param name="streams">The streams.</param>
/// <param name="videoStream">The video stream.</param>
private void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream)
{
var mediaStream = new MediaStream
{
BitRate = Convert.ToInt32(videoStream.BitRate),
Width = videoStream.Width,
Height = videoStream.Height,
Codec = videoStream.CodecShortName,
IsInterlaced = videoStream.IsInterlaced,
Type = MediaStreamType.Video,
Index = streams.Count
};
if (videoStream.FrameRateDenominator > 0)
{
float frameRateEnumerator = videoStream.FrameRateEnumerator;
float frameRateDenominator = videoStream.FrameRateDenominator;
mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator;
}
streams.Add(mediaStream);
}
/// <summary>
/// Adds the audio stream.
/// </summary>
/// <param name="streams">The streams.</param>
/// <param name="audioStream">The audio stream.</param>
private void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream)
{
var stream = new MediaStream
{
Codec = audioStream.CodecShortName,
Language = audioStream.LanguageCode,
Channels = audioStream.ChannelCount,
SampleRate = audioStream.SampleRate,
Type = MediaStreamType.Audio,
Index = streams.Count
};
var bitrate = Convert.ToInt32(audioStream.BitRate);
if (bitrate > 0)
{
stream.BitRate = bitrate;
}
if (audioStream.LFE > 0)
{
stream.Channels = audioStream.ChannelCount + 1;
}
streams.Add(stream);
}
/// <summary>
/// Adds the subtitle stream.
/// </summary>
/// <param name="streams">The streams.</param>
/// <param name="textStream">The text stream.</param>
private void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream)
{
streams.Add(new MediaStream
{
Language = textStream.LanguageCode,
Codec = textStream.CodecShortName,
Type = MediaStreamType.Subtitle,
Index = streams.Count
});
}
/// <summary>
/// Adds the subtitle stream.
/// </summary>
/// <param name="streams">The streams.</param>
/// <param name="textStream">The text stream.</param>
private void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream)
{
streams.Add(new MediaStream
{
Language = textStream.LanguageCode,
Codec = textStream.CodecShortName,
Type = MediaStreamType.Subtitle,
Index = streams.Count
});
}
}
}

View File

@ -0,0 +1,58 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Configuration;
using System.Collections.Generic;
using System.IO;
using MediaBrowser.Controller.IO;
using MediaBrowser.Model.IO;
namespace MediaBrowser.MediaEncoding.Configuration
{
public class EncodingConfigurationFactory : IConfigurationFactory
{
private readonly IFileSystem _fileSystem;
public EncodingConfigurationFactory(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new[]
{
new EncodingConfigurationStore(_fileSystem)
};
}
}
public class EncodingConfigurationStore : ConfigurationStore, IValidatingConfiguration
{
private readonly IFileSystem _fileSystem;
public EncodingConfigurationStore(IFileSystem fileSystem)
{
ConfigurationType = typeof(EncodingOptions);
Key = "encoding";
_fileSystem = fileSystem;
}
public void Validate(object oldConfig, object newConfig)
{
var oldEncodingConfig = (EncodingOptions)oldConfig;
var newEncodingConfig = (EncodingOptions)newConfig;
var newPath = newEncodingConfig.TranscodingTempPath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(oldEncodingConfig.TranscodingTempPath ?? string.Empty, newPath))
{
// Validate
if (!_fileSystem.DirectoryExists(newPath))
{
throw new FileNotFoundException(string.Format("{0} does not exist.", newPath));
}
}
}
}
}

View File

@ -0,0 +1,62 @@
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using System;
using MediaBrowser.Model.Diagnostics;
namespace MediaBrowser.MediaEncoding.Encoder
{
public class AudioEncoder : BaseEncoder
{
public AudioEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IIsoManager isoManager, ILibraryManager libraryManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IProcessFactory processFactory) : base(mediaEncoder, logger, configurationManager, fileSystem, isoManager, libraryManager, sessionManager, subtitleEncoder, mediaSourceManager, processFactory)
{
}
protected override string GetCommandLineArguments(EncodingJob state)
{
var encodingOptions = GetEncodingOptions();
return EncodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, state.OutputFilePath);
}
protected override string GetOutputFileExtension(EncodingJob state)
{
var ext = base.GetOutputFileExtension(state);
if (!string.IsNullOrEmpty(ext))
{
return ext;
}
var audioCodec = state.Options.AudioCodec;
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".aac";
}
if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".mp3";
}
if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".ogg";
}
if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".wma";
}
return null;
}
protected override bool IsVideoEncoder
{
get { return false; }
}
}
}

View File

@ -0,0 +1,375 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Diagnostics;
using MediaBrowser.Model.Dlna;
namespace MediaBrowser.MediaEncoding.Encoder
{
public abstract class BaseEncoder
{
protected readonly MediaEncoder MediaEncoder;
protected readonly ILogger Logger;
protected readonly IServerConfigurationManager ConfigurationManager;
protected readonly IFileSystem FileSystem;
protected readonly IIsoManager IsoManager;
protected readonly ILibraryManager LibraryManager;
protected readonly ISessionManager SessionManager;
protected readonly ISubtitleEncoder SubtitleEncoder;
protected readonly IMediaSourceManager MediaSourceManager;
protected IProcessFactory ProcessFactory;
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
protected EncodingHelper EncodingHelper;
protected BaseEncoder(MediaEncoder mediaEncoder,
ILogger logger,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
IIsoManager isoManager,
ILibraryManager libraryManager,
ISessionManager sessionManager,
ISubtitleEncoder subtitleEncoder,
IMediaSourceManager mediaSourceManager, IProcessFactory processFactory)
{
MediaEncoder = mediaEncoder;
Logger = logger;
ConfigurationManager = configurationManager;
FileSystem = fileSystem;
IsoManager = isoManager;
LibraryManager = libraryManager;
SessionManager = sessionManager;
SubtitleEncoder = subtitleEncoder;
MediaSourceManager = mediaSourceManager;
ProcessFactory = processFactory;
EncodingHelper = new EncodingHelper(MediaEncoder, FileSystem, SubtitleEncoder);
}
public async Task<EncodingJob> Start(EncodingJobOptions options,
IProgress<double> progress,
CancellationToken cancellationToken)
{
var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, ConfigurationManager, MediaEncoder)
.CreateJob(options, EncodingHelper, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false);
encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
FileSystem.CreateDirectory(FileSystem.GetDirectoryName(encodingJob.OutputFilePath));
encodingJob.ReadInputAtNativeFramerate = options.ReadInputAtNativeFramerate;
await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false);
var commandLineArgs = GetCommandLineArguments(encodingJob);
var process = ProcessFactory.Create(new ProcessOptions
{
CreateNoWindow = true,
UseShellExecute = false,
// Must consume both stdout and stderr or deadlocks may occur
//RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
FileName = MediaEncoder.EncoderPath,
Arguments = commandLineArgs,
IsHidden = true,
ErrorDialog = false,
EnableRaisingEvents = true
});
var workingDirectory = GetWorkingDirectory(options);
if (!string.IsNullOrWhiteSpace(workingDirectory))
{
process.StartInfo.WorkingDirectory = workingDirectory;
}
OnTranscodeBeginning(encodingJob);
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
Logger.Info(commandLineLogMessage);
var logFilePath = Path.Combine(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath, "transcode-" + Guid.NewGuid() + ".txt");
FileSystem.CreateDirectory(FileSystem.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
encodingJob.LogFileStream = FileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine);
await encodingJob.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationToken).ConfigureAwait(false);
process.Exited += (sender, args) => OnFfMpegProcessExited(process, encodingJob);
try
{
process.Start();
}
catch (Exception ex)
{
Logger.ErrorException("Error starting ffmpeg", ex);
OnTranscodeFailedToStart(encodingJob.OutputFilePath, encodingJob);
throw;
}
cancellationToken.Register(() => Cancel(process, encodingJob));
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
//process.BeginOutputReadLine();
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
new JobLogger(Logger).StartStreamingLog(encodingJob, process.StandardError.BaseStream, encodingJob.LogFileStream);
// Wait for the file to exist before proceeeding
while (!FileSystem.FileExists(encodingJob.OutputFilePath) && !encodingJob.HasExited)
{
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
return encodingJob;
}
private void Cancel(IProcess process, EncodingJob job)
{
Logger.Info("Killing ffmpeg process for {0}", job.OutputFilePath);
//process.Kill();
process.StandardInput.WriteLine("q");
job.IsCancelled = true;
}
/// <summary>
/// Processes the exited.
/// </summary>
/// <param name="process">The process.</param>
/// <param name="job">The job.</param>
private void OnFfMpegProcessExited(IProcess process, EncodingJob job)
{
job.HasExited = true;
Logger.Debug("Disposing stream resources");
job.Dispose();
var isSuccesful = false;
try
{
var exitCode = process.ExitCode;
Logger.Info("FFMpeg exited with code {0}", exitCode);
isSuccesful = exitCode == 0;
}
catch
{
Logger.Error("FFMpeg exited with an error.");
}
if (isSuccesful && !job.IsCancelled)
{
job.TaskCompletionSource.TrySetResult(true);
}
else if (job.IsCancelled)
{
try
{
DeleteFiles(job);
}
catch
{
}
try
{
job.TaskCompletionSource.TrySetException(new OperationCanceledException());
}
catch
{
}
}
else
{
try
{
DeleteFiles(job);
}
catch
{
}
try
{
job.TaskCompletionSource.TrySetException(new Exception("Encoding failed"));
}
catch
{
}
}
// This causes on exited to be called twice:
//try
//{
// // Dispose the process
// process.Dispose();
//}
//catch (Exception ex)
//{
// Logger.ErrorException("Error disposing ffmpeg.", ex);
//}
}
protected virtual void DeleteFiles(EncodingJob job)
{
FileSystem.DeleteFile(job.OutputFilePath);
}
private void OnTranscodeBeginning(EncodingJob job)
{
job.ReportTranscodingProgress(null, null, null, null, null);
}
private void OnTranscodeFailedToStart(string path, EncodingJob job)
{
if (!string.IsNullOrWhiteSpace(job.Options.DeviceId))
{
SessionManager.ClearTranscodingInfo(job.Options.DeviceId);
}
}
protected abstract bool IsVideoEncoder { get; }
protected virtual string GetWorkingDirectory(EncodingJobOptions options)
{
return null;
}
protected EncodingOptions GetEncodingOptions()
{
return ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
}
protected abstract string GetCommandLineArguments(EncodingJob job);
private string GetOutputFilePath(EncodingJob state)
{
var folder = string.IsNullOrWhiteSpace(state.Options.TempDirectory) ?
ConfigurationManager.ApplicationPaths.TranscodingTempPath :
state.Options.TempDirectory;
var outputFileExtension = GetOutputFileExtension(state);
var filename = state.Id + (outputFileExtension ?? string.Empty).ToLower();
return Path.Combine(folder, filename);
}
protected virtual string GetOutputFileExtension(EncodingJob state)
{
if (!string.IsNullOrWhiteSpace(state.Options.Container))
{
return "." + state.Options.Container;
}
return null;
}
/// <summary>
/// Gets the name of the output video codec
/// </summary>
/// <param name="state">The state.</param>
/// <returns>System.String.</returns>
protected string GetVideoDecoder(EncodingJob state)
{
if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
return null;
}
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
// Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
if (state.VideoType != VideoType.VideoFile)
{
return null;
}
if (state.VideoStream != null && !string.IsNullOrWhiteSpace(state.VideoStream.Codec))
{
if (string.Equals(GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
{
switch (state.MediaSource.VideoStream.Codec.ToLower())
{
case "avc":
case "h264":
if (MediaEncoder.SupportsDecoder("h264_qsv"))
{
// Seeing stalls and failures with decoding. Not worth it compared to encoding.
return "-c:v h264_qsv ";
}
break;
case "mpeg2video":
if (MediaEncoder.SupportsDecoder("mpeg2_qsv"))
{
return "-c:v mpeg2_qsv ";
}
break;
case "vc1":
if (MediaEncoder.SupportsDecoder("vc1_qsv"))
{
return "-c:v vc1_qsv ";
}
break;
}
}
}
// leave blank so ffmpeg will decide
return null;
}
private async Task AcquireResources(EncodingJob state, CancellationToken cancellationToken)
{
if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
{
state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationToken).ConfigureAwait(false);
}
if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Options.LiveStreamId))
{
var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest
{
OpenToken = state.MediaSource.OpenToken
}, cancellationToken).ConfigureAwait(false);
EncodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, null);
if (state.IsVideoRequest)
{
EncodingHelper.TryStreamCopy(state);
}
}
if (state.MediaSource.BufferMs.HasValue)
{
await Task.Delay(state.MediaSource.BufferMs.Value, cancellationToken).ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using MediaBrowser.Model.Diagnostics;
using MediaBrowser.Model.Logging;
namespace MediaBrowser.MediaEncoding.Encoder
{
public class EncoderValidator
{
private readonly ILogger _logger;
private readonly IProcessFactory _processFactory;
public EncoderValidator(ILogger logger, IProcessFactory processFactory)
{
_logger = logger;
_processFactory = processFactory;
}
public Tuple<List<string>, List<string>> Validate(string encoderPath)
{
_logger.Info("Validating media encoder at {0}", encoderPath);
var decoders = GetDecoders(encoderPath);
var encoders = GetEncoders(encoderPath);
_logger.Info("Encoder validation complete");
return new Tuple<List<string>, List<string>>(decoders, encoders);
}
public bool ValidateVersion(string encoderAppPath, bool logOutput)
{
string output = string.Empty;
try
{
output = GetProcessOutput(encoderAppPath, "-version");
}
catch (Exception ex)
{
if (logOutput)
{
_logger.ErrorException("Error validating encoder", ex);
}
}
if (string.IsNullOrWhiteSpace(output))
{
return false;
}
_logger.Info("ffmpeg info: {0}", output);
if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
{
return false;
}
output = " " + output + " ";
for (var i = 2013; i <= 2015; i++)
{
var yearString = i.ToString(CultureInfo.InvariantCulture);
if (output.IndexOf(" " + yearString + " ", StringComparison.OrdinalIgnoreCase) != -1)
{
return false;
}
}
return true;
}
private List<string> GetDecoders(string encoderAppPath)
{
string output = string.Empty;
try
{
output = GetProcessOutput(encoderAppPath, "-decoders");
}
catch (Exception )
{
//_logger.ErrorException("Error detecting available decoders", ex);
}
var found = new List<string>();
var required = new[]
{
"mpeg2video",
"h264_qsv",
"hevc_qsv",
"mpeg2_qsv",
"vc1_qsv",
"h264_cuvid",
"hevc_cuvid",
"dts",
"ac3",
"aac",
"mp3",
"h264",
"hevc"
};
foreach (var codec in required)
{
var srch = " " + codec + " ";
if (output.IndexOf(srch, StringComparison.OrdinalIgnoreCase) != -1)
{
_logger.Info("Decoder available: " + codec);
found.Add(codec);
}
}
return found;
}
private List<string> GetEncoders(string encoderAppPath)
{
string output = null;
try
{
output = GetProcessOutput(encoderAppPath, "-encoders");
}
catch
{
}
var found = new List<string>();
var required = new[]
{
"libx264",
"libx265",
"mpeg4",
"msmpeg4",
"libvpx",
"libvpx-vp9",
"aac",
"libmp3lame",
"libopus",
"libvorbis",
"srt",
"h264_nvenc",
"hevc_nvenc",
"h264_qsv",
"hevc_qsv",
"h264_omx",
"hevc_omx",
"h264_vaapi",
"hevc_vaapi",
"ac3"
};
output = output ?? string.Empty;
var index = 0;
foreach (var codec in required)
{
var srch = " " + codec + " ";
if (output.IndexOf(srch, StringComparison.OrdinalIgnoreCase) != -1)
{
if (index < required.Length - 1)
{
_logger.Info("Encoder available: " + codec);
}
found.Add(codec);
}
index++;
}
return found;
}
private string GetProcessOutput(string path, string arguments)
{
var process = _processFactory.Create(new ProcessOptions
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = path,
Arguments = arguments,
IsHidden = true,
ErrorDialog = false,
RedirectStandardOutput = true
});
_logger.Info("Running {0} {1}", path, arguments);
using (process)
{
process.Start();
try
{
return process.StandardOutput.ReadToEnd();
}
catch
{
_logger.Info("Killing process {0} {1}", path, arguments);
// Hate having to do this
try
{
process.Kill();
}
catch (Exception ex1)
{
_logger.ErrorException("Error killing process", ex1);
}
throw;
}
}
}
}
}

View File

@ -0,0 +1,197 @@
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.MediaEncoding.Encoder
{
public class EncodingJob : EncodingJobInfo, IDisposable
{
public bool HasExited { get; internal set; }
public bool IsCancelled { get; internal set; }
public Stream LogFileStream { get; set; }
public IProgress<double> Progress { get; set; }
public TaskCompletionSource<bool> TaskCompletionSource;
public EncodingJobOptions Options
{
get { return (EncodingJobOptions) BaseRequest; }
set { BaseRequest = value; }
}
public string Id { get; set; }
public string MimeType { get; set; }
public bool EstimateContentLength { get; set; }
public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
public long? EncodingDurationTicks { get; set; }
public string ItemType { get; set; }
public string GetMimeType(string outputPath)
{
if (!string.IsNullOrEmpty(MimeType))
{
return MimeType;
}
return MimeTypes.GetMimeType(outputPath);
}
private readonly ILogger _logger;
private readonly IMediaSourceManager _mediaSourceManager;
public EncodingJob(ILogger logger, IMediaSourceManager mediaSourceManager) :
base(logger, mediaSourceManager, TranscodingJobType.Progressive)
{
_logger = logger;
_mediaSourceManager = mediaSourceManager;
Id = Guid.NewGuid().ToString("N");
_logger = logger;
TaskCompletionSource = new TaskCompletionSource<bool>();
}
public override void Dispose()
{
DisposeLiveStream();
DisposeLogStream();
DisposeIsoMount();
}
private void DisposeLogStream()
{
if (LogFileStream != null)
{
try
{
LogFileStream.Dispose();
}
catch (Exception ex)
{
_logger.ErrorException("Error disposing log stream", ex);
}
LogFileStream = null;
}
}
private async void DisposeLiveStream()
{
if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Options.LiveStreamId) && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
{
try
{
await _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error closing media source", ex);
}
}
}
public string OutputFilePath { get; set; }
public string ActualOutputVideoCodec
{
get
{
var codec = OutputVideoCodec;
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
{
var stream = VideoStream;
if (stream != null)
{
return stream.Codec;
}
return null;
}
return codec;
}
}
public string ActualOutputAudioCodec
{
get
{
var codec = OutputAudioCodec;
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
{
var stream = AudioStream;
if (stream != null)
{
return stream.Codec;
}
return null;
}
return codec;
}
}
public void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
{
var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null;
// job.Framerate = framerate;
if (!percentComplete.HasValue && ticks.HasValue && RunTimeTicks.HasValue)
{
var pct = ticks.Value / RunTimeTicks.Value;
percentComplete = pct * 100;
}
if (percentComplete.HasValue)
{
Progress.Report(percentComplete.Value);
}
// job.TranscodingPositionTicks = ticks;
// job.BytesTranscoded = bytesTranscoded;
var deviceId = Options.DeviceId;
if (!string.IsNullOrWhiteSpace(deviceId))
{
var audioCodec = ActualOutputVideoCodec;
var videoCodec = ActualOutputVideoCodec;
// SessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
// {
// Bitrate = job.TotalOutputBitrate,
// AudioCodec = audioCodec,
// VideoCodec = videoCodec,
// Container = job.Options.OutputContainer,
// Framerate = framerate,
// CompletionPercentage = percentComplete,
// Width = job.OutputWidth,
// Height = job.OutputHeight,
// AudioChannels = job.OutputAudioChannels,
// IsAudioDirect = string.Equals(job.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase),
// IsVideoDirect = string.Equals(job.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)
// });
}
}
}
}

View File

@ -0,0 +1,309 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.MediaEncoding.Encoder
{
public class EncodingJobFactory
{
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _config;
private readonly IMediaEncoder _mediaEncoder;
protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IConfigurationManager config, IMediaEncoder mediaEncoder)
{
_logger = logger;
_libraryManager = libraryManager;
_mediaSourceManager = mediaSourceManager;
_config = config;
_mediaEncoder = mediaEncoder;
}
public async Task<EncodingJob> CreateJob(EncodingJobOptions options, EncodingHelper encodingHelper, bool isVideoRequest, IProgress<double> progress, CancellationToken cancellationToken)
{
var request = options;
if (string.IsNullOrEmpty(request.AudioCodec))
{
request.AudioCodec = InferAudioCodec(request.Container);
}
var state = new EncodingJob(_logger, _mediaSourceManager)
{
Options = options,
IsVideoRequest = isVideoRequest,
Progress = progress
};
if (!string.IsNullOrWhiteSpace(request.VideoCodec))
{
state.SupportedVideoCodecs = request.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
}
if (!string.IsNullOrWhiteSpace(request.AudioCodec))
{
state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault();
}
if (!string.IsNullOrWhiteSpace(request.SubtitleCodec))
{
state.SupportedSubtitleCodecs = request.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToSubtitleCodec(i))
?? state.SupportedSubtitleCodecs.FirstOrDefault();
}
var item = _libraryManager.GetItemById(request.Id);
state.ItemType = item.GetType().Name;
state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
// TODO
// var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ??
// item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null);
// if (primaryImage != null)
// {
// state.AlbumCoverPath = primaryImage.Path;
// }
// TODO network path substition useful ?
var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, true, cancellationToken).ConfigureAwait(false);
var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
? mediaSources.First()
: mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId));
var videoRequest = state.Options;
encodingHelper.AttachMediaSourceInfo(state, mediaSource, null);
//var container = Path.GetExtension(state.RequestedUrl);
//if (string.IsNullOrEmpty(container))
//{
// container = request.Static ?
// state.InputContainer :
// (Path.GetExtension(GetOutputFilePath(state)) ?? string.Empty).TrimStart('.');
//}
//state.OutputContainer = (container ?? string.Empty).TrimStart('.');
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(state.Options, state.AudioStream);
state.OutputAudioCodec = state.Options.AudioCodec;
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
if (videoRequest != null)
{
state.OutputVideoCodec = state.Options.VideoCodec;
state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.Options, state.VideoStream, state.OutputVideoCodec);
if (state.OutputVideoBitrate.HasValue)
{
var resolution = ResolutionNormalizer.Normalize(
state.VideoStream == null ? (int?)null : state.VideoStream.BitRate,
state.VideoStream == null ? (int?)null : state.VideoStream.Width,
state.VideoStream == null ? (int?)null : state.VideoStream.Height,
state.OutputVideoBitrate.Value,
state.VideoStream == null ? null : state.VideoStream.Codec,
state.OutputVideoCodec,
videoRequest.MaxWidth,
videoRequest.MaxHeight);
videoRequest.MaxWidth = resolution.MaxWidth;
videoRequest.MaxHeight = resolution.MaxHeight;
}
}
ApplyDeviceProfileSettings(state);
if (videoRequest != null)
{
encodingHelper.TryStreamCopy(state);
}
//state.OutputFilePath = GetOutputFilePath(state);
return state;
}
protected EncodingOptions GetEncodingOptions()
{
return _config.GetConfiguration<EncodingOptions>("encoding");
}
/// <summary>
/// Infers the video codec.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>System.Nullable{VideoCodecs}.</returns>
private static string InferVideoCodec(string container)
{
var ext = "." + (container ?? string.Empty);
if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase))
{
return "wmv";
}
if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
{
return "vpx";
}
if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
{
return "theora";
}
if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase))
{
return "h264";
}
return "copy";
}
private string InferAudioCodec(string container)
{
var ext = "." + (container ?? string.Empty);
if (string.Equals(ext, ".mp3", StringComparison.OrdinalIgnoreCase))
{
return "mp3";
}
if (string.Equals(ext, ".aac", StringComparison.OrdinalIgnoreCase))
{
return "aac";
}
if (string.Equals(ext, ".wma", StringComparison.OrdinalIgnoreCase))
{
return "wma";
}
if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase))
{
return "vorbis";
}
if (string.Equals(ext, ".oga", StringComparison.OrdinalIgnoreCase))
{
return "vorbis";
}
if (string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
{
return "vorbis";
}
if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
{
return "vorbis";
}
if (string.Equals(ext, ".webma", StringComparison.OrdinalIgnoreCase))
{
return "vorbis";
}
return "copy";
}
/// <summary>
/// Determines whether the specified stream is H264.
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
protected bool IsH264(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 ||
codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
}
private static int GetVideoProfileScore(string profile)
{
var list = new List<string>
{
"Constrained Baseline",
"Baseline",
"Extended",
"Main",
"High",
"Progressive High",
"Constrained High"
};
return Array.FindIndex(list.ToArray(), t => string.Equals(t, profile, StringComparison.OrdinalIgnoreCase));
}
private void ApplyDeviceProfileSettings(EncodingJob state)
{
var profile = state.Options.DeviceProfile;
if (profile == null)
{
// Don't use settings from the default profile.
// Only use a specific profile if it was requested.
return;
}
var audioCodec = state.ActualOutputAudioCodec;
var videoCodec = state.ActualOutputVideoCodec;
var outputContainer = state.Options.Container;
var mediaProfile = state.IsVideoRequest ?
profile.GetAudioMediaProfile(outputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth) :
profile.GetVideoMediaProfile(outputContainer,
audioCodec,
videoCodec,
state.OutputWidth,
state.OutputHeight,
state.TargetVideoBitDepth,
state.OutputVideoBitrate,
state.TargetVideoProfile,
state.TargetVideoLevel,
state.TargetFramerate,
state.TargetPacketLength,
state.TargetTimestamp,
state.IsTargetAnamorphic,
state.IsTargetInterlaced,
state.TargetRefFrames,
state.TargetVideoStreamCount,
state.TargetAudioStreamCount,
state.TargetVideoCodecTag,
state.IsTargetAVC);
if (mediaProfile != null)
{
state.MimeType = mediaProfile.MimeType;
}
var transcodingProfile = state.IsVideoRequest ?
profile.GetAudioTranscodingProfile(outputContainer, audioCodec) :
profile.GetVideoTranscodingProfile(outputContainer, audioCodec, videoCodec);
if (transcodingProfile != null)
{
state.EstimateContentLength = transcodingProfile.EstimateContentLength;
//state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
state.Options.CopyTimestamps = transcodingProfile.CopyTimestamps;
}
}
}
}

View File

@ -0,0 +1,70 @@
using MediaBrowser.Model.MediaInfo;
using System.Collections.Generic;
using System.Linq;
namespace MediaBrowser.MediaEncoding.Encoder
{
public static class EncodingUtils
{
public static string GetInputArgument(List<string> inputFiles, MediaProtocol protocol)
{
if (protocol != MediaProtocol.File)
{
var url = inputFiles.First();
return string.Format("\"{0}\"", url);
}
return GetConcatInputArgument(inputFiles);
}
/// <summary>
/// Gets the concat input argument.
/// </summary>
/// <param name="inputFiles">The input files.</param>
/// <returns>System.String.</returns>
private static string GetConcatInputArgument(IReadOnlyList<string> inputFiles)
{
// Get all streams
// If there's more than one we'll need to use the concat command
if (inputFiles.Count > 1)
{
var files = string.Join("|", inputFiles.Select(NormalizePath).ToArray());
return string.Format("concat:\"{0}\"", files);
}
// Determine the input path for video files
return GetFileInputArgument(inputFiles[0]);
}
/// <summary>
/// Gets the file input argument.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>System.String.</returns>
private static string GetFileInputArgument(string path)
{
if (path.IndexOf("://") != -1)
{
return string.Format("\"{0}\"", path);
}
// Quotes are valid path characters in linux and they need to be escaped here with a leading \
path = NormalizePath(path);
return string.Format("file:\"{0}\"", path);
}
/// <summary>
/// Normalizes the path.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>System.String.</returns>
private static string NormalizePath(string path)
{
// Quotes are valid path characters in linux and they need to be escaped here with a leading \
return path.Replace("\"", "\\\"");
}
}
}

View File

@ -0,0 +1,182 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Model.IO;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.IO;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Net;
namespace MediaBrowser.MediaEncoding.Encoder
{
public class FontConfigLoader
{
private readonly IHttpClient _httpClient;
private readonly IApplicationPaths _appPaths;
private readonly ILogger _logger;
private readonly IZipClient _zipClient;
private readonly IFileSystem _fileSystem;
private readonly string[] _fontUrls =
{
"https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/ARIALUNI.7z"
};
public FontConfigLoader(IHttpClient httpClient, IApplicationPaths appPaths, ILogger logger, IZipClient zipClient, IFileSystem fileSystem)
{
_httpClient = httpClient;
_appPaths = appPaths;
_logger = logger;
_zipClient = zipClient;
_fileSystem = fileSystem;
}
/// <summary>
/// Extracts the fonts.
/// </summary>
/// <param name="targetPath">The target path.</param>
/// <returns>Task.</returns>
public async Task DownloadFonts(string targetPath)
{
try
{
var fontsDirectory = Path.Combine(targetPath, "fonts");
_fileSystem.CreateDirectory(fontsDirectory);
const string fontFilename = "ARIALUNI.TTF";
var fontFile = Path.Combine(fontsDirectory, fontFilename);
if (_fileSystem.FileExists(fontFile))
{
await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false);
}
else
{
// Kick this off, but no need to wait on it
var task = Task.Run(async () =>
{
await DownloadFontFile(fontsDirectory, fontFilename, new SimpleProgress<double>()).ConfigureAwait(false);
await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false);
});
}
}
catch (HttpException ex)
{
// Don't let the server crash because of this
_logger.ErrorException("Error downloading ffmpeg font files", ex);
}
catch (Exception ex)
{
// Don't let the server crash because of this
_logger.ErrorException("Error writing ffmpeg font files", ex);
}
}
/// <summary>
/// Downloads the font file.
/// </summary>
/// <param name="fontsDirectory">The fonts directory.</param>
/// <param name="fontFilename">The font filename.</param>
/// <returns>Task.</returns>
private async Task DownloadFontFile(string fontsDirectory, string fontFilename, IProgress<double> progress)
{
var existingFile = _fileSystem
.GetFilePaths(_appPaths.ProgramDataPath, true)
.FirstOrDefault(i => string.Equals(fontFilename, Path.GetFileName(i), StringComparison.OrdinalIgnoreCase));
if (existingFile != null)
{
try
{
_fileSystem.CopyFile(existingFile, Path.Combine(fontsDirectory, fontFilename), true);
return;
}
catch (IOException ex)
{
// Log this, but don't let it fail the operation
_logger.ErrorException("Error copying file", ex);
}
}
string tempFile = null;
foreach (var url in _fontUrls)
{
progress.Report(0);
try
{
tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
{
Url = url,
Progress = progress
}).ConfigureAwait(false);
break;
}
catch (Exception ex)
{
// The core can function without the font file, so handle this
_logger.ErrorException("Failed to download ffmpeg font file from {0}", ex, url);
}
}
if (string.IsNullOrEmpty(tempFile))
{
return;
}
Extract7zArchive(tempFile, fontsDirectory);
try
{
_fileSystem.DeleteFile(tempFile);
}
catch (IOException ex)
{
// Log this, but don't let it fail the operation
_logger.ErrorException("Error deleting temp file {0}", ex, tempFile);
}
}
private void Extract7zArchive(string archivePath, string targetPath)
{
_logger.Info("Extracting {0} to {1}", archivePath, targetPath);
_zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
}
/// <summary>
/// Writes the font config file.
/// </summary>
/// <param name="fontsDirectory">The fonts directory.</param>
/// <returns>Task.</returns>
private async Task WriteFontConfigFile(string fontsDirectory)
{
const string fontConfigFilename = "fonts.conf";
var fontConfigFile = Path.Combine(fontsDirectory, fontConfigFilename);
if (!_fileSystem.FileExists(fontConfigFile))
{
var contents = string.Format("<?xml version=\"1.0\"?><fontconfig><dir>{0}</dir><alias><family>Arial</family><prefer>Arial Unicode MS</prefer></alias></fontconfig>", fontsDirectory);
var bytes = Encoding.UTF8.GetBytes(contents);
using (var fileStream = _fileSystem.GetFileStream(fontConfigFile, FileOpenMode.Create, FileAccessMode.Write,
FileShareMode.Read, true))
{
await fileStream.WriteAsync(bytes, 0, bytes.Length);
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,66 @@
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using System;
using System.IO;
using System.Threading.Tasks;
using MediaBrowser.Model.Diagnostics;
namespace MediaBrowser.MediaEncoding.Encoder
{
public class VideoEncoder : BaseEncoder
{
public VideoEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IIsoManager isoManager, ILibraryManager libraryManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IProcessFactory processFactory) : base(mediaEncoder, logger, configurationManager, fileSystem, isoManager, libraryManager, sessionManager, subtitleEncoder, mediaSourceManager, processFactory)
{
}
protected override string GetCommandLineArguments(EncodingJob state)
{
// Get the output codec name
var encodingOptions = GetEncodingOptions();
return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, state.OutputFilePath, "superfast");
}
protected override string GetOutputFileExtension(EncodingJob state)
{
var ext = base.GetOutputFileExtension(state);
if (!string.IsNullOrEmpty(ext))
{
return ext;
}
var videoCodec = state.Options.VideoCodec;
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
return ".ts";
}
if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
{
return ".ogv";
}
if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
{
return ".webm";
}
if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
{
return ".asf";
}
return null;
}
protected override bool IsVideoEncoder
{
get { return true; }
}
}
}

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\SharedVersion.cs">
<Link>Properties\SharedVersion.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BDInfo\BDInfo.csproj">
<Project>{88ae38df-19d7-406f-a6a9-09527719a21e}</Project>
<Name>BDInfo</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
<Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
<Name>MediaBrowser.Common</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
<Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
<Name>MediaBrowser.Controller</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
<Name>MediaBrowser.Model</Name>
</ProjectReference>
<ProjectReference Include="..\OpenSubtitlesHandler\OpenSubtitlesHandler.csproj">
<Project>{4a4402d4-e910-443b-b8fc-2c18286a2ca0}</Project>
<Name>OpenSubtitlesHandler</Name>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="EmitMSBuildWarning" BeforeTargets="Build">
<Warning Text="Packages containing MSBuild targets and props files cannot be fully installed in projects targeting multiple frameworks. The MSBuild targets and props files have been ignored." />
</Target>
</Project>

View File

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
namespace MediaBrowser.MediaEncoding.Probing
{
public static class FFProbeHelpers
{
/// <summary>
/// Normalizes the FF probe result.
/// </summary>
/// <param name="result">The result.</param>
public static void NormalizeFFProbeResult(InternalMediaInfoResult result)
{
if (result == null)
{
throw new ArgumentNullException("result");
}
if (result.format != null && result.format.tags != null)
{
result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
}
if (result.streams != null)
{
// Convert all dictionaries to case insensitive
foreach (var stream in result.streams)
{
if (stream.tags != null)
{
stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
}
if (stream.disposition != null)
{
stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition);
}
}
}
}
/// <summary>
/// Gets a string from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.String.</returns>
public static string GetDictionaryValue(Dictionary<string, string> tags, string key)
{
if (tags == null)
{
return null;
}
string val;
tags.TryGetValue(key, out val);
return val;
}
/// <summary>
/// Gets an int from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.Nullable{System.Int32}.</returns>
public static int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
{
var val = GetDictionaryValue(tags, key);
if (!string.IsNullOrEmpty(val))
{
int i;
if (int.TryParse(val, out i))
{
return i;
}
}
return null;
}
/// <summary>
/// Gets a DateTime from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.Nullable{DateTime}.</returns>
public static DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
{
var val = GetDictionaryValue(tags, key);
if (!string.IsNullOrEmpty(val))
{
DateTime i;
if (DateTime.TryParse(val, out i))
{
return i.ToUniversalTime();
}
}
return null;
}
/// <summary>
/// Converts a dictionary to case insensitive
/// </summary>
/// <param name="dict">The dict.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private static Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
{
return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
}
}
}

View File

@ -0,0 +1,341 @@
using System.Collections.Generic;
namespace MediaBrowser.MediaEncoding.Probing
{
/// <summary>
/// Class MediaInfoResult
/// </summary>
public class InternalMediaInfoResult
{
/// <summary>
/// Gets or sets the streams.
/// </summary>
/// <value>The streams.</value>
public MediaStreamInfo[] streams { get; set; }
/// <summary>
/// Gets or sets the format.
/// </summary>
/// <value>The format.</value>
public MediaFormatInfo format { get; set; }
/// <summary>
/// Gets or sets the chapters.
/// </summary>
/// <value>The chapters.</value>
public MediaChapter[] Chapters { get; set; }
}
public class MediaChapter
{
public int id { get; set; }
public string time_base { get; set; }
public long start { get; set; }
public string start_time { get; set; }
public long end { get; set; }
public string end_time { get; set; }
public Dictionary<string, string> tags { get; set; }
}
/// <summary>
/// Represents a stream within the output
/// </summary>
public class MediaStreamInfo
{
/// <summary>
/// Gets or sets the index.
/// </summary>
/// <value>The index.</value>
public int index { get; set; }
/// <summary>
/// Gets or sets the profile.
/// </summary>
/// <value>The profile.</value>
public string profile { get; set; }
/// <summary>
/// Gets or sets the codec_name.
/// </summary>
/// <value>The codec_name.</value>
public string codec_name { get; set; }
/// <summary>
/// Gets or sets the codec_long_name.
/// </summary>
/// <value>The codec_long_name.</value>
public string codec_long_name { get; set; }
/// <summary>
/// Gets or sets the codec_type.
/// </summary>
/// <value>The codec_type.</value>
public string codec_type { get; set; }
/// <summary>
/// Gets or sets the sample_rate.
/// </summary>
/// <value>The sample_rate.</value>
public string sample_rate { get; set; }
/// <summary>
/// Gets or sets the channels.
/// </summary>
/// <value>The channels.</value>
public int channels { get; set; }
/// <summary>
/// Gets or sets the channel_layout.
/// </summary>
/// <value>The channel_layout.</value>
public string channel_layout { get; set; }
/// <summary>
/// Gets or sets the avg_frame_rate.
/// </summary>
/// <value>The avg_frame_rate.</value>
public string avg_frame_rate { get; set; }
/// <summary>
/// Gets or sets the duration.
/// </summary>
/// <value>The duration.</value>
public string duration { get; set; }
/// <summary>
/// Gets or sets the bit_rate.
/// </summary>
/// <value>The bit_rate.</value>
public string bit_rate { get; set; }
/// <summary>
/// Gets or sets the width.
/// </summary>
/// <value>The width.</value>
public int width { get; set; }
/// <summary>
/// Gets or sets the refs.
/// </summary>
/// <value>The refs.</value>
public int refs { get; set; }
/// <summary>
/// Gets or sets the height.
/// </summary>
/// <value>The height.</value>
public int height { get; set; }
/// <summary>
/// Gets or sets the display_aspect_ratio.
/// </summary>
/// <value>The display_aspect_ratio.</value>
public string display_aspect_ratio { get; set; }
/// <summary>
/// Gets or sets the tags.
/// </summary>
/// <value>The tags.</value>
public Dictionary<string, string> tags { get; set; }
/// <summary>
/// Gets or sets the bits_per_sample.
/// </summary>
/// <value>The bits_per_sample.</value>
public int bits_per_sample { get; set; }
/// <summary>
/// Gets or sets the bits_per_raw_sample.
/// </summary>
/// <value>The bits_per_raw_sample.</value>
public int bits_per_raw_sample { get; set; }
/// <summary>
/// Gets or sets the r_frame_rate.
/// </summary>
/// <value>The r_frame_rate.</value>
public string r_frame_rate { get; set; }
/// <summary>
/// Gets or sets the has_b_frames.
/// </summary>
/// <value>The has_b_frames.</value>
public int has_b_frames { get; set; }
/// <summary>
/// Gets or sets the sample_aspect_ratio.
/// </summary>
/// <value>The sample_aspect_ratio.</value>
public string sample_aspect_ratio { get; set; }
/// <summary>
/// Gets or sets the pix_fmt.
/// </summary>
/// <value>The pix_fmt.</value>
public string pix_fmt { get; set; }
/// <summary>
/// Gets or sets the level.
/// </summary>
/// <value>The level.</value>
public int level { get; set; }
/// <summary>
/// Gets or sets the time_base.
/// </summary>
/// <value>The time_base.</value>
public string time_base { get; set; }
/// <summary>
/// Gets or sets the start_time.
/// </summary>
/// <value>The start_time.</value>
public string start_time { get; set; }
/// <summary>
/// Gets or sets the codec_time_base.
/// </summary>
/// <value>The codec_time_base.</value>
public string codec_time_base { get; set; }
/// <summary>
/// Gets or sets the codec_tag.
/// </summary>
/// <value>The codec_tag.</value>
public string codec_tag { get; set; }
/// <summary>
/// Gets or sets the codec_tag_string.
/// </summary>
/// <value>The codec_tag_string.</value>
public string codec_tag_string { get; set; }
/// <summary>
/// Gets or sets the sample_fmt.
/// </summary>
/// <value>The sample_fmt.</value>
public string sample_fmt { get; set; }
/// <summary>
/// Gets or sets the dmix_mode.
/// </summary>
/// <value>The dmix_mode.</value>
public string dmix_mode { get; set; }
/// <summary>
/// Gets or sets the start_pts.
/// </summary>
/// <value>The start_pts.</value>
public string start_pts { get; set; }
/// <summary>
/// Gets or sets the is_avc.
/// </summary>
/// <value>The is_avc.</value>
public string is_avc { get; set; }
/// <summary>
/// Gets or sets the nal_length_size.
/// </summary>
/// <value>The nal_length_size.</value>
public string nal_length_size { get; set; }
/// <summary>
/// Gets or sets the ltrt_cmixlev.
/// </summary>
/// <value>The ltrt_cmixlev.</value>
public string ltrt_cmixlev { get; set; }
/// <summary>
/// Gets or sets the ltrt_surmixlev.
/// </summary>
/// <value>The ltrt_surmixlev.</value>
public string ltrt_surmixlev { get; set; }
/// <summary>
/// Gets or sets the loro_cmixlev.
/// </summary>
/// <value>The loro_cmixlev.</value>
public string loro_cmixlev { get; set; }
/// <summary>
/// Gets or sets the loro_surmixlev.
/// </summary>
/// <value>The loro_surmixlev.</value>
public string loro_surmixlev { get; set; }
public string field_order { get; set; }
/// <summary>
/// Gets or sets the disposition.
/// </summary>
/// <value>The disposition.</value>
public Dictionary<string, string> disposition { get; set; }
}
/// <summary>
/// Class MediaFormat
/// </summary>
public class MediaFormatInfo
{
/// <summary>
/// Gets or sets the filename.
/// </summary>
/// <value>The filename.</value>
public string filename { get; set; }
/// <summary>
/// Gets or sets the nb_streams.
/// </summary>
/// <value>The nb_streams.</value>
public int nb_streams { get; set; }
/// <summary>
/// Gets or sets the format_name.
/// </summary>
/// <value>The format_name.</value>
public string format_name { get; set; }
/// <summary>
/// Gets or sets the format_long_name.
/// </summary>
/// <value>The format_long_name.</value>
public string format_long_name { get; set; }
/// <summary>
/// Gets or sets the start_time.
/// </summary>
/// <value>The start_time.</value>
public string start_time { get; set; }
/// <summary>
/// Gets or sets the duration.
/// </summary>
/// <value>The duration.</value>
public string duration { get; set; }
/// <summary>
/// Gets or sets the size.
/// </summary>
/// <value>The size.</value>
public string size { get; set; }
/// <summary>
/// Gets or sets the bit_rate.
/// </summary>
/// <value>The bit_rate.</value>
public string bit_rate { get; set; }
/// <summary>
/// Gets or sets the probe_score.
/// </summary>
/// <value>The probe_score.</value>
public int probe_score { get; set; }
/// <summary>
/// Gets or sets the tags.
/// </summary>
/// <value>The tags.</value>
public Dictionary<string, string> tags { get; set; }
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MediaBrowser.MediaEncoding")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MediaBrowser.MediaEncoding")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("05f49ab9-2a90-4332-9d41-7817a9cccd90")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]

View File

@ -0,0 +1,122 @@
using MediaBrowser.Model.Extensions;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
public class AssParser : ISubtitleParser
{
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
{
var trackInfo = new SubtitleTrackInfo();
List<SubtitleTrackEvent> trackEvents = new List<SubtitleTrackEvent>();
var eventIndex = 1;
using (var reader = new StreamReader(stream))
{
string line;
while (reader.ReadLine() != "[Events]")
{}
var headers = ParseFieldHeaders(reader.ReadLine());
while ((line = reader.ReadLine()) != null)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
if(line.StartsWith("["))
break;
if(string.IsNullOrEmpty(line))
continue;
var subEvent = new SubtitleTrackEvent { Id = eventIndex.ToString(_usCulture) };
eventIndex++;
var sections = line.Substring(10).Split(',');
subEvent.StartPositionTicks = GetTicks(sections[headers["Start"]]);
subEvent.EndPositionTicks = GetTicks(sections[headers["End"]]);
subEvent.Text = string.Join(",", sections.Skip(headers["Text"]));
RemoteNativeFormatting(subEvent);
subEvent.Text = subEvent.Text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
subEvent.Text = Regex.Replace(subEvent.Text, @"\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}", string.Empty, RegexOptions.IgnoreCase);
trackEvents.Add(subEvent);
}
}
trackInfo.TrackEvents = trackEvents.ToArray();
return trackInfo;
}
long GetTicks(string time)
{
TimeSpan span;
return TimeSpan.TryParseExact(time, @"h\:mm\:ss\.ff", _usCulture, out span)
? span.Ticks: 0;
}
private Dictionary<string,int> ParseFieldHeaders(string line) {
var fields = line.Substring(8).Split(',').Select(x=>x.Trim()).ToList();
var result = new Dictionary<string, int> {
{"Start", fields.IndexOf("Start")},
{"End", fields.IndexOf("End")},
{"Text", fields.IndexOf("Text")}
};
return result;
}
/// <summary>
/// Credit: https://github.com/SubtitleEdit/subtitleedit/blob/master/src/Logic/SubtitleFormats/AdvancedSubStationAlpha.cs
/// </summary>
private void RemoteNativeFormatting(SubtitleTrackEvent p)
{
int indexOfBegin = p.Text.IndexOf('{');
string pre = string.Empty;
while (indexOfBegin >= 0 && p.Text.IndexOf('}') > indexOfBegin)
{
string s = p.Text.Substring(indexOfBegin);
if (s.StartsWith("{\\an1}", StringComparison.Ordinal) ||
s.StartsWith("{\\an2}", StringComparison.Ordinal) ||
s.StartsWith("{\\an3}", StringComparison.Ordinal) ||
s.StartsWith("{\\an4}", StringComparison.Ordinal) ||
s.StartsWith("{\\an5}", StringComparison.Ordinal) ||
s.StartsWith("{\\an6}", StringComparison.Ordinal) ||
s.StartsWith("{\\an7}", StringComparison.Ordinal) ||
s.StartsWith("{\\an8}", StringComparison.Ordinal) ||
s.StartsWith("{\\an9}", StringComparison.Ordinal))
{
pre = s.Substring(0, 6);
}
else if (s.StartsWith("{\\an1\\", StringComparison.Ordinal) ||
s.StartsWith("{\\an2\\", StringComparison.Ordinal) ||
s.StartsWith("{\\an3\\", StringComparison.Ordinal) ||
s.StartsWith("{\\an4\\", StringComparison.Ordinal) ||
s.StartsWith("{\\an5\\", StringComparison.Ordinal) ||
s.StartsWith("{\\an6\\", StringComparison.Ordinal) ||
s.StartsWith("{\\an7\\", StringComparison.Ordinal) ||
s.StartsWith("{\\an8\\", StringComparison.Ordinal) ||
s.StartsWith("{\\an9\\", StringComparison.Ordinal))
{
pre = s.Substring(0, 5) + "}";
}
int indexOfEnd = p.Text.IndexOf('}');
p.Text = p.Text.Remove(indexOfBegin, (indexOfEnd - indexOfBegin) + 1);
indexOfBegin = p.Text.IndexOf('{');
}
p.Text = pre + p.Text;
}
}
}

View File

@ -0,0 +1,29 @@
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.MediaEncoding.Subtitles
{
public static class ConfigurationExtension
{
public static SubtitleOptions GetSubtitleConfiguration(this IConfigurationManager manager)
{
return manager.GetConfiguration<SubtitleOptions>("subtitles");
}
}
public class SubtitleConfigurationFactory : IConfigurationFactory
{
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new List<ConfigurationStore>
{
new ConfigurationStore
{
Key = "subtitles",
ConfigurationType = typeof (SubtitleOptions)
}
};
}
}
}

View File

@ -0,0 +1,17 @@
using System.IO;
using System.Threading;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
public interface ISubtitleParser
{
/// <summary>
/// Parses the specified stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>SubtitleTrackInfo.</returns>
SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,20 @@
using System.IO;
using System.Threading;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
/// <summary>
/// Interface ISubtitleWriter
/// </summary>
public interface ISubtitleWriter
{
/// <summary>
/// Writes the specified information.
/// </summary>
/// <param name="info">The information.</param>
/// <param name="stream">The stream.</param>
/// <param name="cancellationToken">The cancellation token.</param>
void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,28 @@
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using System.IO;
using System.Text;
using System.Threading;
namespace MediaBrowser.MediaEncoding.Subtitles
{
public class JsonWriter : ISubtitleWriter
{
private readonly IJsonSerializer _json;
public JsonWriter(IJsonSerializer json)
{
_json = json;
}
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
var json = _json.SerializeToString(info);
writer.Write(json);
}
}
}
}

View File

@ -0,0 +1,349 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
using OpenSubtitlesHandler;
namespace MediaBrowser.MediaEncoding.Subtitles
{
public class OpenSubtitleDownloader : ISubtitleProvider, IDisposable
{
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IServerConfigurationManager _config;
private readonly IEncryptionManager _encryption;
private readonly IJsonSerializer _json;
private readonly IFileSystem _fileSystem;
public OpenSubtitleDownloader(ILogManager logManager, IHttpClient httpClient, IServerConfigurationManager config, IEncryptionManager encryption, IJsonSerializer json, IFileSystem fileSystem)
{
_logger = logManager.GetLogger(GetType().Name);
_httpClient = httpClient;
_config = config;
_encryption = encryption;
_json = json;
_fileSystem = fileSystem;
_config.NamedConfigurationUpdating += _config_NamedConfigurationUpdating;
Utilities.HttpClient = httpClient;
OpenSubtitles.SetUserAgent("mediabrowser.tv");
}
private const string PasswordHashPrefix = "h:";
void _config_NamedConfigurationUpdating(object sender, ConfigurationUpdateEventArgs e)
{
if (!string.Equals(e.Key, "subtitles", StringComparison.OrdinalIgnoreCase))
{
return;
}
var options = (SubtitleOptions)e.NewConfiguration;
if (options != null &&
!string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) &&
!options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
{
options.OpenSubtitlesPasswordHash = EncryptPassword(options.OpenSubtitlesPasswordHash);
}
}
private string EncryptPassword(string password)
{
return PasswordHashPrefix + _encryption.EncryptString(password);
}
private string DecryptPassword(string password)
{
if (password == null ||
!password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
{
return string.Empty;
}
return _encryption.DecryptString(password.Substring(2));
}
public string Name
{
get { return "Open Subtitles"; }
}
private SubtitleOptions GetOptions()
{
return _config.GetSubtitleConfiguration();
}
public IEnumerable<VideoContentType> SupportedMediaTypes
{
get
{
var options = GetOptions();
if (string.IsNullOrWhiteSpace(options.OpenSubtitlesUsername) ||
string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash))
{
return new VideoContentType[] { };
}
return new[] { VideoContentType.Episode, VideoContentType.Movie };
}
}
public Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
{
return GetSubtitlesInternal(id, GetOptions(), cancellationToken);
}
private DateTime _lastRateLimitException;
private async Task<SubtitleResponse> GetSubtitlesInternal(string id,
SubtitleOptions options,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(id))
{
throw new ArgumentNullException("id");
}
var idParts = id.Split(new[] { '-' }, 3);
var format = idParts[0];
var language = idParts[1];
var ossId = idParts[2];
var downloadsList = new[] { int.Parse(ossId, _usCulture) };
await Login(cancellationToken).ConfigureAwait(false);
if ((DateTime.UtcNow - _lastRateLimitException).TotalHours < 1)
{
throw new RateLimitExceededException("OpenSubtitles rate limit reached");
}
var resultDownLoad = await OpenSubtitles.DownloadSubtitlesAsync(downloadsList, cancellationToken).ConfigureAwait(false);
if ((resultDownLoad.Status ?? string.Empty).IndexOf("407", StringComparison.OrdinalIgnoreCase) != -1)
{
_lastRateLimitException = DateTime.UtcNow;
throw new RateLimitExceededException("OpenSubtitles rate limit reached");
}
if (!(resultDownLoad is MethodResponseSubtitleDownload))
{
throw new Exception("Invalid response type");
}
var results = ((MethodResponseSubtitleDownload)resultDownLoad).Results;
_lastRateLimitException = DateTime.MinValue;
if (results.Count == 0)
{
var msg = string.Format("Subtitle with Id {0} was not found. Name: {1}. Status: {2}. Message: {3}",
ossId,
resultDownLoad.Name ?? string.Empty,
resultDownLoad.Status ?? string.Empty,
resultDownLoad.Message ?? string.Empty);
throw new ResourceNotFoundException(msg);
}
var data = Convert.FromBase64String(results.First().Data);
return new SubtitleResponse
{
Format = format,
Language = language,
Stream = new MemoryStream(Utilities.Decompress(new MemoryStream(data)))
};
}
private DateTime _lastLogin;
private async Task Login(CancellationToken cancellationToken)
{
if ((DateTime.UtcNow - _lastLogin).TotalSeconds < 60)
{
return;
}
var options = GetOptions();
var user = options.OpenSubtitlesUsername ?? string.Empty;
var password = DecryptPassword(options.OpenSubtitlesPasswordHash);
var loginResponse = await OpenSubtitles.LogInAsync(user, password, "en", cancellationToken).ConfigureAwait(false);
if (!(loginResponse is MethodResponseLogIn))
{
throw new Exception("Authentication to OpenSubtitles failed.");
}
_lastLogin = DateTime.UtcNow;
}
public async Task<IEnumerable<NameIdPair>> GetSupportedLanguages(CancellationToken cancellationToken)
{
await Login(cancellationToken).ConfigureAwait(false);
var result = OpenSubtitles.GetSubLanguages("en");
if (!(result is MethodResponseGetSubLanguages))
{
_logger.Error("Invalid response type");
return new List<NameIdPair>();
}
var results = ((MethodResponseGetSubLanguages)result).Languages;
return results.Select(i => new NameIdPair
{
Name = i.LanguageName,
Id = i.SubLanguageID
});
}
private string NormalizeLanguage(string language)
{
// Problem with Greek subtitle download #1349
if (string.Equals(language, "gre", StringComparison.OrdinalIgnoreCase))
{
return "ell";
}
return language;
}
public async Task<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken)
{
var imdbIdText = request.GetProviderId(MetadataProviders.Imdb);
long imdbId = 0;
switch (request.ContentType)
{
case VideoContentType.Episode:
if (!request.IndexNumber.HasValue || !request.ParentIndexNumber.HasValue || string.IsNullOrEmpty(request.SeriesName))
{
_logger.Debug("Episode information missing");
return new List<RemoteSubtitleInfo>();
}
break;
case VideoContentType.Movie:
if (string.IsNullOrEmpty(request.Name))
{
_logger.Debug("Movie name missing");
return new List<RemoteSubtitleInfo>();
}
if (string.IsNullOrWhiteSpace(imdbIdText) || !long.TryParse(imdbIdText.TrimStart('t'), NumberStyles.Any, _usCulture, out imdbId))
{
_logger.Debug("Imdb id missing");
return new List<RemoteSubtitleInfo>();
}
break;
}
if (string.IsNullOrEmpty(request.MediaPath))
{
_logger.Debug("Path Missing");
return new List<RemoteSubtitleInfo>();
}
await Login(cancellationToken).ConfigureAwait(false);
var subLanguageId = NormalizeLanguage(request.Language);
string hash;
using (var fileStream = _fileSystem.OpenRead(request.MediaPath))
{
hash = Utilities.ComputeHash(fileStream);
}
var fileInfo = _fileSystem.GetFileInfo(request.MediaPath);
var movieByteSize = fileInfo.Length;
var searchImdbId = request.ContentType == VideoContentType.Movie ? imdbId.ToString(_usCulture) : "";
var subtitleSearchParameters = request.ContentType == VideoContentType.Episode
? new List<SubtitleSearchParameters> {
new SubtitleSearchParameters(subLanguageId,
query: request.SeriesName,
season: request.ParentIndexNumber.Value.ToString(_usCulture),
episode: request.IndexNumber.Value.ToString(_usCulture))
}
: new List<SubtitleSearchParameters> {
new SubtitleSearchParameters(subLanguageId, imdbid: searchImdbId),
new SubtitleSearchParameters(subLanguageId, query: request.Name, imdbid: searchImdbId)
};
var parms = new List<SubtitleSearchParameters> {
new SubtitleSearchParameters( subLanguageId,
movieHash: hash,
movieByteSize: movieByteSize,
imdbid: searchImdbId ),
};
parms.AddRange(subtitleSearchParameters);
var result = await OpenSubtitles.SearchSubtitlesAsync(parms.ToArray(), cancellationToken).ConfigureAwait(false);
if (!(result is MethodResponseSubtitleSearch))
{
_logger.Error("Invalid response type");
return new List<RemoteSubtitleInfo>();
}
Predicate<SubtitleSearchResult> mediaFilter =
x =>
request.ContentType == VideoContentType.Episode
? !string.IsNullOrEmpty(x.SeriesSeason) && !string.IsNullOrEmpty(x.SeriesEpisode) &&
int.Parse(x.SeriesSeason, _usCulture) == request.ParentIndexNumber &&
int.Parse(x.SeriesEpisode, _usCulture) == request.IndexNumber
: !string.IsNullOrEmpty(x.IDMovieImdb) && long.Parse(x.IDMovieImdb, _usCulture) == imdbId;
var results = ((MethodResponseSubtitleSearch)result).Results;
// Avoid implicitly captured closure
var hasCopy = hash;
return results.Where(x => x.SubBad == "0" && mediaFilter(x) && (!request.IsPerfectMatch || string.Equals(x.MovieHash, hash, StringComparison.OrdinalIgnoreCase)))
.OrderBy(x => (string.Equals(x.MovieHash, hash, StringComparison.OrdinalIgnoreCase) ? 0 : 1))
.ThenBy(x => Math.Abs(long.Parse(x.MovieByteSize, _usCulture) - movieByteSize))
.ThenByDescending(x => int.Parse(x.SubDownloadsCnt, _usCulture))
.ThenByDescending(x => double.Parse(x.SubRating, _usCulture))
.Select(i => new RemoteSubtitleInfo
{
Author = i.UserNickName,
Comment = i.SubAuthorComment,
CommunityRating = float.Parse(i.SubRating, _usCulture),
DownloadCount = int.Parse(i.SubDownloadsCnt, _usCulture),
Format = i.SubFormat,
ProviderName = Name,
ThreeLetterISOLanguageName = i.SubLanguageID,
Id = i.SubFormat + "-" + i.SubLanguageID + "-" + i.IDSubtitleFile,
Name = i.SubFileName,
DateCreated = DateTime.Parse(i.SubAddDate, _usCulture),
IsHashMatch = i.MovieHash == hasCopy
}).Where(i => !string.Equals(i.Format, "sub", StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Format, "idx", StringComparison.OrdinalIgnoreCase));
}
public void Dispose()
{
_config.NamedConfigurationUpdating -= _config_NamedConfigurationUpdating;
}
}
}

View File

@ -0,0 +1,7 @@
namespace MediaBrowser.MediaEncoding.Subtitles
{
public class ParserValues
{
public const string NewLine = "\r\n";
}
}

View File

@ -0,0 +1,92 @@
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Logging;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
public class SrtParser : ISubtitleParser
{
private readonly ILogger _logger;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public SrtParser(ILogger logger)
{
_logger = logger;
}
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
{
var trackInfo = new SubtitleTrackInfo();
List<SubtitleTrackEvent> trackEvents = new List<SubtitleTrackEvent>();
using ( var reader = new StreamReader(stream))
{
string line;
while ((line = reader.ReadLine()) != null)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
var subEvent = new SubtitleTrackEvent {Id = line};
line = reader.ReadLine();
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
var time = Regex.Split(line, @"[\t ]*-->[\t ]*");
if (time.Length < 2)
{
// This occurs when subtitle text has an empty line as part of the text.
// Need to adjust the break statement below to resolve this.
_logger.Warn("Unrecognized line in srt: {0}", line);
continue;
}
subEvent.StartPositionTicks = GetTicks(time[0]);
var endTime = time[1];
var idx = endTime.IndexOf(" ", StringComparison.Ordinal);
if (idx > 0)
endTime = endTime.Substring(0, idx);
subEvent.EndPositionTicks = GetTicks(endTime);
var multiline = new List<string>();
while ((line = reader.ReadLine()) != null)
{
if (string.IsNullOrEmpty(line))
{
break;
}
multiline.Add(line);
}
subEvent.Text = string.Join(ParserValues.NewLine, multiline);
subEvent.Text = subEvent.Text.Replace(@"\N", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
subEvent.Text = Regex.Replace(subEvent.Text, @"\{(?:\\\d?[\w.-]+(?:\([^\)]*\)|&H?[0-9A-Fa-f]+&|))+\}", string.Empty, RegexOptions.IgnoreCase);
subEvent.Text = Regex.Replace(subEvent.Text, "<", "&lt;", RegexOptions.IgnoreCase);
subEvent.Text = Regex.Replace(subEvent.Text, ">", "&gt;", RegexOptions.IgnoreCase);
subEvent.Text = Regex.Replace(subEvent.Text, "&lt;(\\/?(font|b|u|i|s))((\\s+(\\w|\\w[\\w\\-]*\\w)(\\s*=\\s*(?:\\\".*?\\\"|'.*?'|[^'\\\">\\s]+))?)+\\s*|\\s*)(\\/?)&gt;", "<$1$3$7>", RegexOptions.IgnoreCase);
trackEvents.Add(subEvent);
}
}
trackInfo.TrackEvents = trackEvents.ToArray();
return trackInfo;
}
long GetTicks(string time) {
TimeSpan span;
return TimeSpan.TryParseExact(time, @"hh\:mm\:ss\.fff", _usCulture, out span)
? span.Ticks
: (TimeSpan.TryParseExact(time, @"hh\:mm\:ss\,fff", _usCulture, out span)
? span.Ticks : 0);
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
public class SrtWriter : ISubtitleWriter
{
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
var index = 1;
foreach (var trackEvent in info.TrackEvents)
{
cancellationToken.ThrowIfCancellationRequested();
writer.WriteLine(index.ToString(CultureInfo.InvariantCulture));
writer.WriteLine(@"{0:hh\:mm\:ss\,fff} --> {1:hh\:mm\:ss\,fff}", TimeSpan.FromTicks(trackEvent.StartPositionTicks), TimeSpan.FromTicks(trackEvent.EndPositionTicks));
var text = trackEvent.Text;
// TODO: Not sure how to handle these
text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase);
writer.WriteLine(text);
writer.WriteLine(string.Empty);
index++;
}
}
}
}
}

View File

@ -0,0 +1,397 @@
using MediaBrowser.Model.Extensions;
using System;
using System.IO;
using System.Text;
using System.Threading;
using MediaBrowser.Model.MediaInfo;
using System.Collections.Generic;
namespace MediaBrowser.MediaEncoding.Subtitles
{
/// <summary>
/// Credit to https://github.com/SubtitleEdit/subtitleedit/blob/a299dc4407a31796364cc6ad83f0d3786194ba22/src/Logic/SubtitleFormats/SubStationAlpha.cs
/// </summary>
public class SsaParser : ISubtitleParser
{
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
{
var trackInfo = new SubtitleTrackInfo();
List<SubtitleTrackEvent> trackEvents = new List<SubtitleTrackEvent>();
using (var reader = new StreamReader(stream))
{
bool eventsStarted = false;
string[] format = "Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text".Split(',');
int indexLayer = 0;
int indexStart = 1;
int indexEnd = 2;
int indexStyle = 3;
int indexName = 4;
int indexEffect = 8;
int indexText = 9;
int lineNumber = 0;
var header = new StringBuilder();
string line;
while ((line = reader.ReadLine()) != null)
{
cancellationToken.ThrowIfCancellationRequested();
lineNumber++;
if (!eventsStarted)
header.AppendLine(line);
if (line.Trim().ToLower() == "[events]")
{
eventsStarted = true;
}
else if (!string.IsNullOrEmpty(line) && line.Trim().StartsWith(";"))
{
// skip comment lines
}
else if (eventsStarted && line.Trim().Length > 0)
{
string s = line.Trim().ToLower();
if (s.StartsWith("format:"))
{
if (line.Length > 10)
{
format = line.ToLower().Substring(8).Split(',');
for (int i = 0; i < format.Length; i++)
{
if (format[i].Trim().ToLower() == "layer")
indexLayer = i;
else if (format[i].Trim().ToLower() == "start")
indexStart = i;
else if (format[i].Trim().ToLower() == "end")
indexEnd = i;
else if (format[i].Trim().ToLower() == "text")
indexText = i;
else if (format[i].Trim().ToLower() == "effect")
indexEffect = i;
else if (format[i].Trim().ToLower() == "style")
indexStyle = i;
}
}
}
else if (!string.IsNullOrEmpty(s))
{
string text = string.Empty;
string start = string.Empty;
string end = string.Empty;
string style = string.Empty;
string layer = string.Empty;
string effect = string.Empty;
string name = string.Empty;
string[] splittedLine;
if (s.StartsWith("dialogue:"))
splittedLine = line.Substring(10).Split(',');
else
splittedLine = line.Split(',');
for (int i = 0; i < splittedLine.Length; i++)
{
if (i == indexStart)
start = splittedLine[i].Trim();
else if (i == indexEnd)
end = splittedLine[i].Trim();
else if (i == indexLayer)
layer = splittedLine[i];
else if (i == indexEffect)
effect = splittedLine[i];
else if (i == indexText)
text = splittedLine[i];
else if (i == indexStyle)
style = splittedLine[i];
else if (i == indexName)
name = splittedLine[i];
else if (i > indexText)
text += "," + splittedLine[i];
}
try
{
var p = new SubtitleTrackEvent();
p.StartPositionTicks = GetTimeCodeFromString(start);
p.EndPositionTicks = GetTimeCodeFromString(end);
p.Text = GetFormattedText(text);
trackEvents.Add(p);
}
catch
{
}
}
}
}
//if (header.Length > 0)
//subtitle.Header = header.ToString();
//subtitle.Renumber(1);
}
trackInfo.TrackEvents = trackEvents.ToArray();
return trackInfo;
}
private static long GetTimeCodeFromString(string time)
{
// h:mm:ss.cc
string[] timeCode = time.Split(':', '.');
return new TimeSpan(0, int.Parse(timeCode[0]),
int.Parse(timeCode[1]),
int.Parse(timeCode[2]),
int.Parse(timeCode[3]) * 10).Ticks;
}
public static string GetFormattedText(string text)
{
text = text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
bool italic = false;
for (int i = 0; i < 10; i++) // just look ten times...
{
if (text.Contains(@"{\fn"))
{
int start = text.IndexOf(@"{\fn");
int end = text.IndexOf('}', start);
if (end > 0 && !text.Substring(start).StartsWith("{\\fn}"))
{
string fontName = text.Substring(start + 4, end - (start + 4));
string extraTags = string.Empty;
CheckAndAddSubTags(ref fontName, ref extraTags, out italic);
text = text.Remove(start, end - start + 1);
if (italic)
text = text.Insert(start, "<font face=\"" + fontName + "\"" + extraTags + "><i>");
else
text = text.Insert(start, "<font face=\"" + fontName + "\"" + extraTags + ">");
int indexOfEndTag = text.IndexOf("{\\fn}", start);
if (indexOfEndTag > 0)
text = text.Remove(indexOfEndTag, "{\\fn}".Length).Insert(indexOfEndTag, "</font>");
else
text += "</font>";
}
}
if (text.Contains(@"{\fs"))
{
int start = text.IndexOf(@"{\fs");
int end = text.IndexOf('}', start);
if (end > 0 && !text.Substring(start).StartsWith("{\\fs}"))
{
string fontSize = text.Substring(start + 4, end - (start + 4));
string extraTags = string.Empty;
CheckAndAddSubTags(ref fontSize, ref extraTags, out italic);
if (IsInteger(fontSize))
{
text = text.Remove(start, end - start + 1);
if (italic)
text = text.Insert(start, "<font size=\"" + fontSize + "\"" + extraTags + "><i>");
else
text = text.Insert(start, "<font size=\"" + fontSize + "\"" + extraTags + ">");
int indexOfEndTag = text.IndexOf("{\\fs}", start);
if (indexOfEndTag > 0)
text = text.Remove(indexOfEndTag, "{\\fs}".Length).Insert(indexOfEndTag, "</font>");
else
text += "</font>";
}
}
}
if (text.Contains(@"{\c"))
{
int start = text.IndexOf(@"{\c");
int end = text.IndexOf('}', start);
if (end > 0 && !text.Substring(start).StartsWith("{\\c}"))
{
string color = text.Substring(start + 4, end - (start + 4));
string extraTags = string.Empty;
CheckAndAddSubTags(ref color, ref extraTags, out italic);
color = color.Replace("&", string.Empty).TrimStart('H');
color = color.PadLeft(6, '0');
// switch to rrggbb from bbggrr
color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2);
color = color.ToLower();
text = text.Remove(start, end - start + 1);
if (italic)
text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + "><i>");
else
text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">");
int indexOfEndTag = text.IndexOf("{\\c}", start);
if (indexOfEndTag > 0)
text = text.Remove(indexOfEndTag, "{\\c}".Length).Insert(indexOfEndTag, "</font>");
else
text += "</font>";
}
}
if (text.Contains(@"{\1c")) // "1" specifices primary color
{
int start = text.IndexOf(@"{\1c");
int end = text.IndexOf('}', start);
if (end > 0 && !text.Substring(start).StartsWith("{\\1c}"))
{
string color = text.Substring(start + 5, end - (start + 5));
string extraTags = string.Empty;
CheckAndAddSubTags(ref color, ref extraTags, out italic);
color = color.Replace("&", string.Empty).TrimStart('H');
color = color.PadLeft(6, '0');
// switch to rrggbb from bbggrr
color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2);
color = color.ToLower();
text = text.Remove(start, end - start + 1);
if (italic)
text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + "><i>");
else
text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">");
text += "</font>";
}
}
}
text = text.Replace(@"{\i1}", "<i>");
text = text.Replace(@"{\i0}", "</i>");
text = text.Replace(@"{\i}", "</i>");
if (CountTagInText(text, "<i>") > CountTagInText(text, "</i>"))
text += "</i>";
text = text.Replace(@"{\u1}", "<u>");
text = text.Replace(@"{\u0}", "</u>");
text = text.Replace(@"{\u}", "</u>");
if (CountTagInText(text, "<u>") > CountTagInText(text, "</u>"))
text += "</u>";
text = text.Replace(@"{\b1}", "<b>");
text = text.Replace(@"{\b0}", "</b>");
text = text.Replace(@"{\b}", "</b>");
if (CountTagInText(text, "<b>") > CountTagInText(text, "</b>"))
text += "</b>";
return text;
}
private static bool IsInteger(string s)
{
int i;
if (int.TryParse(s, out i))
return true;
return false;
}
private static int CountTagInText(string text, string tag)
{
int count = 0;
int index = text.IndexOf(tag);
while (index >= 0)
{
count++;
if (index == text.Length)
return count;
index = text.IndexOf(tag, index + 1);
}
return count;
}
private static void CheckAndAddSubTags(ref string tagName, ref string extraTags, out bool italic)
{
italic = false;
int indexOfSPlit = tagName.IndexOf(@"\");
if (indexOfSPlit > 0)
{
string rest = tagName.Substring(indexOfSPlit).TrimStart('\\');
tagName = tagName.Remove(indexOfSPlit);
for (int i = 0; i < 10; i++)
{
if (rest.StartsWith("fs") && rest.Length > 2)
{
indexOfSPlit = rest.IndexOf(@"\");
string fontSize = rest;
if (indexOfSPlit > 0)
{
fontSize = rest.Substring(0, indexOfSPlit);
rest = rest.Substring(indexOfSPlit).TrimStart('\\');
}
else
{
rest = string.Empty;
}
extraTags += " size=\"" + fontSize.Substring(2) + "\"";
}
else if (rest.StartsWith("fn") && rest.Length > 2)
{
indexOfSPlit = rest.IndexOf(@"\");
string fontName = rest;
if (indexOfSPlit > 0)
{
fontName = rest.Substring(0, indexOfSPlit);
rest = rest.Substring(indexOfSPlit).TrimStart('\\');
}
else
{
rest = string.Empty;
}
extraTags += " face=\"" + fontName.Substring(2) + "\"";
}
else if (rest.StartsWith("c") && rest.Length > 2)
{
indexOfSPlit = rest.IndexOf(@"\");
string fontColor = rest;
if (indexOfSPlit > 0)
{
fontColor = rest.Substring(0, indexOfSPlit);
rest = rest.Substring(indexOfSPlit).TrimStart('\\');
}
else
{
rest = string.Empty;
}
string color = fontColor.Substring(2);
color = color.Replace("&", string.Empty).TrimStart('H');
color = color.PadLeft(6, '0');
// switch to rrggbb from bbggrr
color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2);
color = color.ToLower();
extraTags += " color=\"" + color + "\"";
}
else if (rest.StartsWith("i1") && rest.Length > 1)
{
indexOfSPlit = rest.IndexOf(@"\");
italic = true;
if (indexOfSPlit > 0)
{
rest = rest.Substring(indexOfSPlit).TrimStart('\\');
}
else
{
rest = string.Empty;
}
}
else if (rest.Length > 0 && rest.Contains("\\"))
{
indexOfSPlit = rest.IndexOf(@"\");
rest = rest.Substring(indexOfSPlit).TrimStart('\\');
}
}
}
}
}
}

View File

@ -0,0 +1,733 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Diagnostics;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Text;
namespace MediaBrowser.MediaEncoding.Subtitles
{
public class SubtitleEncoder : ISubtitleEncoder
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly IMediaEncoder _mediaEncoder;
private readonly IJsonSerializer _json;
private readonly IHttpClient _httpClient;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IProcessFactory _processFactory;
private readonly ITextEncoding _textEncoding;
public SubtitleEncoder(ILibraryManager libraryManager, ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IJsonSerializer json, IHttpClient httpClient, IMediaSourceManager mediaSourceManager, IProcessFactory processFactory, ITextEncoding textEncoding)
{
_libraryManager = libraryManager;
_logger = logger;
_appPaths = appPaths;
_fileSystem = fileSystem;
_mediaEncoder = mediaEncoder;
_json = json;
_httpClient = httpClient;
_processFactory = processFactory;
_textEncoding = textEncoding;
}
private string SubtitleCachePath
{
get
{
return Path.Combine(_appPaths.DataPath, "subtitles");
}
}
private Stream ConvertSubtitles(Stream stream,
string inputFormat,
string outputFormat,
long startTimeTicks,
long? endTimeTicks,
bool preserveOriginalTimestamps,
CancellationToken cancellationToken)
{
var ms = new MemoryStream();
try
{
var reader = GetReader(inputFormat, true);
var trackInfo = reader.Parse(stream, cancellationToken);
FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
var writer = GetWriter(outputFormat);
writer.Write(trackInfo, ms, cancellationToken);
ms.Position = 0;
}
catch
{
ms.Dispose();
throw;
}
return ms;
}
private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long? endTimeTicks, bool preserveTimestamps)
{
// Drop subs that are earlier than what we're looking for
track.TrackEvents = track.TrackEvents
.SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 || (i.EndPositionTicks - startPositionTicks) < 0)
.ToArray();
if (endTimeTicks.HasValue)
{
var endTime = endTimeTicks.Value;
track.TrackEvents = track.TrackEvents
.TakeWhile(i => i.StartPositionTicks <= endTime)
.ToArray();
}
if (!preserveTimestamps)
{
foreach (var trackEvent in track.TrackEvents)
{
trackEvent.EndPositionTicks -= startPositionTicks;
trackEvent.StartPositionTicks -= startPositionTicks;
}
}
}
async Task<Stream> ISubtitleEncoder.GetSubtitles(BaseItem item, string mediaSourceId, int subtitleStreamIndex, string outputFormat, long startTimeTicks, long endTimeTicks, bool preserveOriginalTimestamps, CancellationToken cancellationToken)
{
if (item == null)
{
throw new ArgumentNullException("item");
}
if (string.IsNullOrWhiteSpace(mediaSourceId))
{
throw new ArgumentNullException("mediaSourceId");
}
// TODO network path substition useful ?
var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, true, cancellationToken).ConfigureAwait(false);
var mediaSource = mediaSources
.First(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
var subtitleStream = mediaSource.MediaStreams
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
var subtitle = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
.ConfigureAwait(false);
var inputFormat = subtitle.Item2;
var writer = TryGetWriter(outputFormat);
// Return the original if we don't have any way of converting it
if (writer == null)
{
return subtitle.Item1;
}
// Return the original if the same format is being requested
// Character encoding was already handled in GetSubtitleStream
if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase))
{
return subtitle.Item1;
}
using (var stream = subtitle.Item1)
{
return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken);
}
}
private async Task<Tuple<Stream, string>> GetSubtitleStream(MediaSourceInfo mediaSource,
MediaStream subtitleStream,
CancellationToken cancellationToken)
{
var inputFiles = new[] { mediaSource.Path };
if (mediaSource.VideoType.HasValue)
{
if (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd)
{
var mediaSourceItem = (Video)_libraryManager.GetItemById(new Guid(mediaSource.Id));
inputFiles = mediaSourceItem.GetPlayableStreamFileNames(_mediaEncoder).ToArray();
}
}
var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
var stream = await GetSubtitleStream(fileInfo.Item1, subtitleStream.Language, fileInfo.Item2, fileInfo.Item4, cancellationToken).ConfigureAwait(false);
return new Tuple<Stream, string>(stream, fileInfo.Item3);
}
private async Task<Stream> GetSubtitleStream(string path, string language, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken)
{
if (requiresCharset)
{
var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false);
var charset = _textEncoding.GetDetectedEncodingName(bytes, bytes.Length, language, true);
_logger.Debug("charset {0} detected for {1}", charset ?? "null", path);
if (!string.IsNullOrEmpty(charset))
{
using (var inputStream = new MemoryStream(bytes))
{
using (var reader = new StreamReader(inputStream, _textEncoding.GetEncodingFromCharset(charset)))
{
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
bytes = Encoding.UTF8.GetBytes(text);
return new MemoryStream(bytes);
}
}
}
}
return _fileSystem.OpenRead(path);
}
private async Task<Tuple<string, MediaProtocol, string, bool>> GetReadableFile(string mediaPath,
string[] inputFiles,
MediaProtocol protocol,
MediaStream subtitleStream,
CancellationToken cancellationToken)
{
if (!subtitleStream.IsExternal)
{
string outputFormat;
string outputCodec;
if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) ||
string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase) ||
string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase))
{
// Extract
outputCodec = "copy";
outputFormat = subtitleStream.Codec;
}
else if (string.Equals(subtitleStream.Codec, "subrip", StringComparison.OrdinalIgnoreCase))
{
// Extract
outputCodec = "copy";
outputFormat = "srt";
}
else
{
// Extract
outputCodec = "srt";
outputFormat = "srt";
}
// Extract
var outputPath = GetSubtitleCachePath(mediaPath, protocol, subtitleStream.Index, "." + outputFormat);
await ExtractTextSubtitle(inputFiles, protocol, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
.ConfigureAwait(false);
return new Tuple<string, MediaProtocol, string, bool>(outputPath, MediaProtocol.File, outputFormat, false);
}
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
.TrimStart('.');
if (GetReader(currentFormat, false) == null)
{
// Convert
var outputPath = GetSubtitleCachePath(mediaPath, protocol, subtitleStream.Index, ".srt");
await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, protocol, outputPath, cancellationToken).ConfigureAwait(false);
return new Tuple<string, MediaProtocol, string, bool>(outputPath, MediaProtocol.File, "srt", true);
}
return new Tuple<string, MediaProtocol, string, bool>(subtitleStream.Path, protocol, currentFormat, true);
}
private ISubtitleParser GetReader(string format, bool throwIfMissing)
{
if (string.IsNullOrEmpty(format))
{
throw new ArgumentNullException("format");
}
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
{
return new SrtParser(_logger);
}
if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
{
return new SsaParser();
}
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
{
return new AssParser();
}
if (throwIfMissing)
{
throw new ArgumentException("Unsupported format: " + format);
}
return null;
}
private ISubtitleWriter TryGetWriter(string format)
{
if (string.IsNullOrEmpty(format))
{
throw new ArgumentNullException("format");
}
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
{
return new JsonWriter(_json);
}
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
{
return new SrtWriter();
}
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase))
{
return new VttWriter();
}
if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase))
{
return new TtmlWriter();
}
return null;
}
private ISubtitleWriter GetWriter(string format)
{
var writer = TryGetWriter(format);
if (writer != null)
{
return writer;
}
throw new ArgumentException("Unsupported format: " + format);
}
/// <summary>
/// The _semaphoreLocks
/// </summary>
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
new ConcurrentDictionary<string, SemaphoreSlim>();
/// <summary>
/// Gets the lock.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.Object.</returns>
private SemaphoreSlim GetLock(string filename)
{
return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
}
/// <summary>
/// Converts the text subtitle to SRT.
/// </summary>
/// <param name="inputPath">The input path.</param>
/// <param name="inputProtocol">The input protocol.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
private async Task ConvertTextSubtitleToSrt(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken)
{
var semaphore = GetLock(outputPath);
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (!_fileSystem.FileExists(outputPath))
{
await ConvertTextSubtitleToSrtInternal(inputPath, language, inputProtocol, outputPath, cancellationToken).ConfigureAwait(false);
}
}
finally
{
semaphore.Release();
}
}
/// <summary>
/// Converts the text subtitle to SRT internal.
/// </summary>
/// <param name="inputPath">The input path.</param>
/// <param name="inputProtocol">The input protocol.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
/// <exception cref="System.ArgumentNullException">
/// inputPath
/// or
/// outputPath
/// </exception>
private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(inputPath))
{
throw new ArgumentNullException("inputPath");
}
if (string.IsNullOrEmpty(outputPath))
{
throw new ArgumentNullException("outputPath");
}
_fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath));
var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, inputProtocol, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(encodingParam))
{
encodingParam = " -sub_charenc " + encodingParam;
}
var process = _processFactory.Create(new ProcessOptions
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = _mediaEncoder.EncoderPath,
Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
IsHidden = true,
ErrorDialog = false
});
_logger.Info("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
try
{
process.Start();
}
catch (Exception ex)
{
_logger.ErrorException("Error starting ffmpeg", ex);
throw;
}
var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false);
if (!ranToCompletion)
{
try
{
_logger.Info("Killing ffmpeg subtitle conversion process");
process.Kill();
}
catch (Exception ex)
{
_logger.ErrorException("Error killing subtitle conversion process", ex);
}
}
var exitCode = ranToCompletion ? process.ExitCode : -1;
process.Dispose();
var failed = false;
if (exitCode == -1)
{
failed = true;
if (_fileSystem.FileExists(outputPath))
{
try
{
_logger.Info("Deleting converted subtitle due to failure: ", outputPath);
_fileSystem.DeleteFile(outputPath);
}
catch (IOException ex)
{
_logger.ErrorException("Error deleting converted subtitle {0}", ex, outputPath);
}
}
}
else if (!_fileSystem.FileExists(outputPath))
{
failed = true;
}
if (failed)
{
var msg = string.Format("ffmpeg subtitle conversion failed for {0}", inputPath);
_logger.Error(msg);
throw new Exception(msg);
}
await SetAssFont(outputPath).ConfigureAwait(false);
_logger.Info("ffmpeg subtitle conversion succeeded for {0}", inputPath);
}
/// <summary>
/// Extracts the text subtitle.
/// </summary>
/// <param name="inputFiles">The input files.</param>
/// <param name="protocol">The protocol.</param>
/// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
/// <param name="outputCodec">The output codec.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
/// <exception cref="System.ArgumentException">Must use inputPath list overload</exception>
private async Task ExtractTextSubtitle(string[] inputFiles, MediaProtocol protocol, int subtitleStreamIndex,
string outputCodec, string outputPath, CancellationToken cancellationToken)
{
var semaphore = GetLock(outputPath);
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (!_fileSystem.FileExists(outputPath))
{
await ExtractTextSubtitleInternal(_mediaEncoder.GetInputArgument(inputFiles, protocol), subtitleStreamIndex, outputCodec, outputPath, cancellationToken).ConfigureAwait(false);
}
}
finally
{
semaphore.Release();
}
}
private async Task ExtractTextSubtitleInternal(string inputPath, int subtitleStreamIndex,
string outputCodec, string outputPath, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(inputPath))
{
throw new ArgumentNullException("inputPath");
}
if (string.IsNullOrEmpty(outputPath))
{
throw new ArgumentNullException("outputPath");
}
_fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath));
var processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", inputPath,
subtitleStreamIndex, outputCodec, outputPath);
var process = _processFactory.Create(new ProcessOptions
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = _mediaEncoder.EncoderPath,
Arguments = processArgs,
IsHidden = true,
ErrorDialog = false
});
_logger.Info("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
try
{
process.Start();
}
catch (Exception ex)
{
_logger.ErrorException("Error starting ffmpeg", ex);
throw;
}
var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false);
if (!ranToCompletion)
{
try
{
_logger.Info("Killing ffmpeg subtitle extraction process");
process.Kill();
}
catch (Exception ex)
{
_logger.ErrorException("Error killing subtitle extraction process", ex);
}
}
var exitCode = ranToCompletion ? process.ExitCode : -1;
process.Dispose();
var failed = false;
if (exitCode == -1)
{
failed = true;
try
{
_logger.Info("Deleting extracted subtitle due to failure: {0}", outputPath);
_fileSystem.DeleteFile(outputPath);
}
catch (FileNotFoundException)
{
}
catch (IOException ex)
{
_logger.ErrorException("Error deleting extracted subtitle {0}", ex, outputPath);
}
}
else if (!_fileSystem.FileExists(outputPath))
{
failed = true;
}
if (failed)
{
var msg = string.Format("ffmpeg subtitle extraction failed for {0} to {1}", inputPath, outputPath);
_logger.Error(msg);
throw new Exception(msg);
}
else
{
var msg = string.Format("ffmpeg subtitle extraction completed for {0} to {1}", inputPath, outputPath);
_logger.Info(msg);
}
if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase))
{
await SetAssFont(outputPath).ConfigureAwait(false);
}
}
/// <summary>
/// Sets the ass font.
/// </summary>
/// <param name="file">The file.</param>
/// <returns>Task.</returns>
private async Task SetAssFont(string file)
{
_logger.Info("Setting ass font within {0}", file);
string text;
Encoding encoding;
using (var fileStream = _fileSystem.OpenRead(file))
{
using (var reader = new StreamReader(fileStream, true))
{
encoding = reader.CurrentEncoding;
text = await reader.ReadToEndAsync().ConfigureAwait(false);
}
}
var newText = text.Replace(",Arial,", ",Arial Unicode MS,");
if (!string.Equals(text, newText))
{
using (var fileStream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
{
using (var writer = new StreamWriter(fileStream, encoding))
{
writer.Write(newText);
}
}
}
}
private string GetSubtitleCachePath(string mediaPath, MediaProtocol protocol, int subtitleStreamIndex, string outputSubtitleExtension)
{
if (protocol == MediaProtocol.File)
{
var ticksParam = string.Empty;
var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
var prefix = filename.Substring(0, 1);
return Path.Combine(SubtitleCachePath, prefix, filename);
}
else
{
var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
var prefix = filename.Substring(0, 1);
return Path.Combine(SubtitleCachePath, prefix, filename);
}
}
public async Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken)
{
var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false);
var charset = _textEncoding.GetDetectedEncodingName(bytes, bytes.Length, language, true);
_logger.Debug("charset {0} detected for {1}", charset ?? "null", path);
return charset;
}
private async Task<byte[]> GetBytes(string path, MediaProtocol protocol, CancellationToken cancellationToken)
{
if (protocol == MediaProtocol.Http)
{
HttpRequestOptions opts = new HttpRequestOptions();
opts.Url = path;
opts.CancellationToken = cancellationToken;
using (var file = await _httpClient.Get(opts).ConfigureAwait(false))
{
using (var memoryStream = new MemoryStream())
{
await file.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Position = 0;
return memoryStream.ToArray();
}
}
}
if (protocol == MediaProtocol.File)
{
return _fileSystem.ReadAllBytes(path);
}
throw new ArgumentOutOfRangeException("protocol");
}
}
}

View File

@ -0,0 +1,60 @@
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
public class TtmlWriter : ISubtitleWriter
{
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
// Example: https://github.com/zmalltalker/ttml2vtt/blob/master/data/sample.xml
// Parser example: https://github.com/mozilla/popcorn-js/blob/master/parsers/parserTTML/popcorn.parserTTML.js
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
writer.WriteLine("<tt xmlns=\"http://www.w3.org/ns/ttml\" xmlns:tts=\"http://www.w3.org/2006/04/ttaf1#styling\" lang=\"no\">");
writer.WriteLine("<head>");
writer.WriteLine("<styling>");
writer.WriteLine("<style id=\"italic\" tts:fontStyle=\"italic\" />");
writer.WriteLine("<style id=\"left\" tts:textAlign=\"left\" />");
writer.WriteLine("<style id=\"center\" tts:textAlign=\"center\" />");
writer.WriteLine("<style id=\"right\" tts:textAlign=\"right\" />");
writer.WriteLine("</styling>");
writer.WriteLine("</head>");
writer.WriteLine("<body>");
writer.WriteLine("<div>");
foreach (var trackEvent in info.TrackEvents)
{
var text = trackEvent.Text;
text = Regex.Replace(text, @"\\n", "<br/>", RegexOptions.IgnoreCase);
writer.WriteLine("<p begin=\"{0}\" dur=\"{1}\">{2}</p>",
trackEvent.StartPositionTicks,
(trackEvent.EndPositionTicks - trackEvent.StartPositionTicks),
text);
}
writer.WriteLine("</div>");
writer.WriteLine("</body>");
writer.WriteLine("</tt>");
}
}
private string FormatTime(long ticks)
{
var time = TimeSpan.FromTicks(ticks);
return string.Format(@"{0:hh\:mm\:ss\,fff}", time);
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
public class VttWriter : ISubtitleWriter
{
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
writer.WriteLine("WEBVTT");
writer.WriteLine(string.Empty);
foreach (var trackEvent in info.TrackEvents)
{
cancellationToken.ThrowIfCancellationRequested();
TimeSpan startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks);
TimeSpan endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks);
// make sure the start and end times are different and seqential
if (endTime.TotalMilliseconds <= startTime.TotalMilliseconds)
{
endTime = startTime.Add(TimeSpan.FromMilliseconds(1));
}
writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff}", startTime, endTime);
var text = trackEvent.Text;
// TODO: Not sure how to handle these
text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase);
writer.WriteLine(text);
writer.WriteLine(string.Empty);
}
}
}
}
}

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
</packages>

View File

@ -1,60 +0,0 @@
using System;
using System.Configuration;
using System.IO;
using System.Runtime.InteropServices;
namespace MediaBrowser.Server.Mono
{
public static class ApplicationPathHelper
{
/// <summary>
/// Gets the path to the application's ProgramDataFolder
/// </summary>
/// <returns>System.String.</returns>
public static string GetProgramDataPath(string applicationPath)
{
var useDebugPath = false;
#if DEBUG
useDebugPath = true;
#endif
var programDataPath = useDebugPath ?
ConfigurationManager.AppSettings["DebugProgramDataPath"] :
ConfigurationManager.AppSettings["ReleaseProgramDataPath"];
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
programDataPath = programDataPath.Replace("%ApplicationData%", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
}
else
{
programDataPath = programDataPath.Replace("%ApplicationData%", "/var/lib");
}
programDataPath = programDataPath
.Replace('/', Path.DirectorySeparatorChar)
.Replace('\\', Path.DirectorySeparatorChar);
// If it's a relative path, e.g. "..\"
if (!Path.IsPathRooted(programDataPath))
{
var path = Path.GetDirectoryName(applicationPath);
if (string.IsNullOrEmpty(path))
{
throw new ApplicationException("Unable to determine running assembly location");
}
programDataPath = Path.Combine(path, programDataPath);
programDataPath = Path.GetFullPath(programDataPath);
}
Directory.CreateDirectory(programDataPath);
return programDataPath;
}
}
}

View File

@ -22,7 +22,6 @@
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.11" />
<PackageReference Include="SQLitePCLRaw.core" Version="1.1.11" />
<PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.11" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.5.0" />
</ItemGroup>
<ItemGroup>

View File

@ -24,6 +24,7 @@ using Mono.Unix.Native;
using ILogger = MediaBrowser.Model.Logging.ILogger;
using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate;
using System.Threading;
using InteropServices = System.Runtime.InteropServices;
namespace MediaBrowser.Server.Mono
{
@ -87,12 +88,25 @@ namespace MediaBrowser.Server.Mono
{
if (string.IsNullOrEmpty(programDataPath))
{
programDataPath = ApplicationPathHelper.GetProgramDataPath(applicationPath);
if (InteropServices.RuntimeInformation.IsOSPlatform(InteropServices.OSPlatform.Windows))
{
programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
}
else
{
// $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored.
programDataPath = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
// If $XDG_DATA_HOME is either not set or empty, $HOME/.local/share should be used.
if (string.IsNullOrEmpty(programDataPath)){
programDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share");
}
}
programDataPath = Path.Combine(programDataPath, "jellyfin");
}
var appFolderPath = Path.GetDirectoryName(applicationPath);
return new ServerApplicationPaths(programDataPath, appFolderPath, Path.GetDirectoryName(applicationPath));
return new ServerApplicationPaths(programDataPath, appFolderPath, appFolderPath);
}
private static void RunApplication(ServerApplicationPaths appPaths, ILogManager logManager, StartupOptions options)

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="DebugProgramDataPath" value="%ApplicationData%/jellyfin-debug/" />
<add key="ReleaseProgramDataPath" value="%ApplicationData%/jellyfin/" />
</appSettings>
</configuration>

View File

@ -15,7 +15,7 @@ While our first priority is a stable build, we will eventually add features that
## Contributing to Jellyfin
If you're interested in contributing, please see [CONTRIBUTING.md](https://github.com/jellyfin/jellyfin/blob/master/CONTRIBUTING.md).
If you're interested in contributing, please see [our wiki for guidelines](https://github.com/jellyfin/jellyfin/wiki/Contributing-to-Jellyfin).
## Prebuilt Jellyfin packages
@ -37,6 +37,8 @@ An Unraid Docker template is available. See [this documentation page](https://gi
A package repository is available at https://repo.jellyfin.org.
NOTE: Ubuntu users may find that the `ffmpeg` dependency package is not present in their release or is simply a rebranded `libav` which is not directly compatible. Please [obtain the ffmpeg package directly from the FFMPEG site](https://ffmpeg.org/download.html#build-linux) to use Jellyfin on Ubuntu.
#### Clean install
0. Install the `dotnet-runtime-2.2` package via [Microsoft's repositories](https://dotnet.microsoft.com/download/dotnet-core/2.2).

View File

@ -76,4 +76,4 @@ namespace Rssdp
#endregion
}
}
}

24
build-deb.sh Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env sh
# Build a Jellyfin .deb file with Docker on Linux
# Places the output .deb file in the parent directory
set -o errexit
set -o xtrace
set -o nounset
package_temporary_dir="`mktemp -d`"
current_user="`whoami`"
image_name="jellyfin-debuild"
cleanup() {
set +o errexit
docker image rm $image_name --force
rm -rf "$package_temporary_dir"
}
trap cleanup EXIT INT
docker build . -t "$image_name" -f ./Dockerfile.debian_package
docker run --rm -v "$package_temporary_dir:/temp" "$image_name" cp -r /dist /temp/
sudo chown -R "$current_user" "$package_temporary_dir"
mv "$package_temporary_dir"/dist/*.deb ../

6
debian/changelog vendored
View File

@ -1,3 +1,9 @@
jellyfin (3.5.2-2) unstable; urgency=medium
* Major code updates related to rebranding and cleanup
-- Joshua Boniface <joshua@boniface.me> Fri, 14 Dec 2018 00:07:46 -0500
jellyfin (3.5.2-1) unstable; urgency=medium
* Add ffmpeg dependency and cleanup work

View File

@ -15,8 +15,6 @@
# General options
#
# Path to the jellyfin.dll executable
JELLYFIN_EXE="/usr/lib/jellyfin/bin/jellyfin.dll"
# Data directory
JELLYFIN_DATA="/var/lib/jellyfin"
# Restart script for in-app server control
@ -30,7 +28,5 @@ JELLYFIN_ADD_OPTS=""
# Application username
JELLYFIN_USER="jellyfin"
# .NET Core runtime binary
JELLYFIN_DOTNET="/usr/bin/dotnet"
# Full application command
JELLYFIN_COMMAND="$JELLYFIN_EXE -programdata $JELLYFIN_DATA -restartpath $JELLYFIN_RESTART_SCRIPT $JELLYFIN_ADD_OPTS"
JELLYFIN_ARGS="-programdata $JELLYFIN_DATA -restartpath $JELLYFIN_RESTART_SCRIPT $JELLYFIN_ADD_OPTS"

7
debian/control vendored
View File

@ -4,14 +4,15 @@ Priority: optional
Maintainer: Vasily <just.one.man@yandex.ru>
Build-Depends: debhelper (>= 9),
dotnet-sdk-2.2,
libc6-dev
libc6-dev,
libcurl4-openssl-dev
Standards-Version: 3.9.4
Package: jellyfin
Replaces: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
Breaks: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
Architecture: all
Depends: ${shlibs:Depends}, ${misc:Depends}, at, libsqlite3-0, dotnet-runtime-2.2, ffmpeg
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, at, libsqlite3-0, ffmpeg
Description: Jellyfin is a home media server.
It is built on top of other popular open source technologies such as Service Stack, jQuery, jQuery mobile, and Mono. It features a REST-based api with built-in documentation to facilitate client development. We also have client libraries for our api to enable rapid development.

View File

@ -9,14 +9,17 @@
### END INIT INFO
# Carry out specific functions when asked to by the system
pid=`ps -fA|grep dotnet|grep jellyfin|awk '{print $2}'| tr -d '\n'`
pidfile="/var/run/jellyfin.pid"
pid=`cat $pidfile`
case "$1" in
start)
if [ "$pid" == "" ]; then
echo "Starting Jellyfin..."
. /etc/default/jellyfin
nohup su -u $JELLYFIN_USER -c $JELLYFIN_DOTNET $JELLYFIN_COMMAND
nohup su -u $JELLYFIN_USER -c /usr/bin/jellyfin $JELLYFIN_ARGS
echo ?? > $pidfile
else
echo "Jellyfin already running"
fi
@ -26,6 +29,7 @@ case "$1" in
echo "Stopping Jellyfin..."
kill $pid
sleep 2
rm -f $pidfile
else
echo "Jellyfin not running"
fi

View File

@ -6,7 +6,7 @@ After = network.target
Type = simple
EnvironmentFile = /etc/default/jellyfin
User = jellyfin
ExecStart = /usr/bin/dotnet ${JELLYFIN_EXE} -programdata ${JELLYFIN_DATA} -restartpath ${JELLYFIN_RESTART_SCRIPT} ${JELLYFIN_ADD_OPTS}
ExecStart = /usr/bin/jellyfin -programdata ${JELLYFIN_DATA} -restartpath ${JELLYFIN_RESTART_SCRIPT} ${JELLYFIN_ADD_OPTS}
Restart = on-abort
TimeoutSec = 20

View File

@ -16,5 +16,5 @@ script
# Log file
logger -t "$0" "DEBUG: `set`"
. /etc/default/jellyfin
exec su -u $JELLYFIN_USER -c $JELLYFIN_DOTNET $JELLYFIN_COMMAND
exec su -u $JELLYFIN_USER -c /usr/bin/jellyfin $JELLYFIN_ARGS
end script

4
debian/postinst vendored
View File

@ -37,8 +37,8 @@ case "$1" in
chmod +x ${JELLYFIN_DIR}/restart.sh > /dev/null 2>&1 || true
# Link hardcoded /var/lib/emby to /var/lib/jellyfin (TEMPORARY)
ln -sf /var/lib/jellyfin /var/lib/emby
# Install jellyfin symlink into /usr/bin
ln -sf /usr/lib/jellyfin/bin/jellyfin /usr/bin/jellyfin
;;
abort-upgrade|abort-remove|abort-deconfigure)

6
debian/rules vendored
View File

@ -15,8 +15,8 @@ override_dh_auto_test:
override_dh_clistrip:
override_dh_auto_build:
dotnet publish --configuration $(CONFIG) $(CURDIR)/MediaBrowser.sln --output='$(CURDIR)/usr/lib/jellyfin/bin'
dotnet publish --configuration $(CONFIG) $(CURDIR)/MediaBrowser.sln --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime linux-x64
override_dh_auto_clean:
dotnet clean --configuration $(CONFIG) $(CURDIR)/MediaBrowser.sln || true
rm -rf '$(CURDIR)/usr/lib/jellyfin'
dotnet clean -maxcpucount:1 --configuration $(CONFIG) $(CURDIR)/MediaBrowser.sln || true
rm -rf '$(CURDIR)/usr'