Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
883492bd33 | ||
|
|
a4eac4feea | ||
|
|
dab58f5840 | ||
|
|
176f136c23 | ||
|
|
816d77e15b | ||
|
|
7c4d47a211 | ||
|
|
d9d2cfa8be | ||
|
|
8036e46966 | ||
|
|
594ce437fb |
+19
-1
@@ -5,6 +5,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.3.6-alpha] - 2022-01-10
|
||||
### Fixed
|
||||
- Properly index `minutes` field when adding new items during scan (vs when rebuilding index)
|
||||
- Fix some nvenc edge cases where only padding is needed for normalization
|
||||
- Properly overwrite environment variables for ffmpeg processes (`LIBVA_DRIVER_NAME`, `FFREPORT`)
|
||||
|
||||
### Added
|
||||
- Add music video `artist` to search index
|
||||
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
|
||||
|
||||
### Changed
|
||||
- Remove `HLS Hybrid` streaming mode; all channels have been reconfigured to use the superior `HLS Segmenter` streaming mode
|
||||
- Update `MPEG-TS` streaming mode to internally use the HLS segmenter
|
||||
- This improves compatibility with many clients and also improves performance at program boundaries
|
||||
- Renamed existing `MPEG-TS` mode as `MPEG-TS (Legacy)`
|
||||
- This mode will be removed in a future release
|
||||
|
||||
## [0.3.5-alpha] - 2022-01-05
|
||||
### Fixed
|
||||
- Fix bundled ffmpeg version in base docker image (NOT nvidia or vaapi) which prevented playback since v0.3.0-alpha
|
||||
@@ -876,7 +893,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Initial release to facilitate testing outside of Docker.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.5-alpha...HEAD
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.6-alpha...HEAD
|
||||
[0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.5-alpha...v0.3.6-alpha
|
||||
[0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.4-alpha...v0.3.5-alpha
|
||||
[0.3.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.3-alpha...v0.3.4-alpha
|
||||
[0.3.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.2-alpha...v0.3.3-alpha
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
|
||||
public async Task<Either<BaseError, PlayoutItemProcessModel>> Handle(T request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Tuple<Channel, string>> validation = await Validate(dbContext, request);
|
||||
return await validation.Match(
|
||||
tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2),
|
||||
@@ -56,7 +56,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
"hls-direct" => StreamingMode.HttpLiveStreamingDirect,
|
||||
"segmenter" => StreamingMode.HttpLiveStreamingSegmenter,
|
||||
"ts" => StreamingMode.TransportStream,
|
||||
"ts" => StreamingMode.TransportStreamHybrid,
|
||||
"ts-legacy" => StreamingMode.TransportStream,
|
||||
_ => channel.StreamingMode
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
public GetConcatProcessByChannelNumber(string scheme, string host, string channelNumber) : base(
|
||||
channelNumber,
|
||||
"ts",
|
||||
"ts-legacy",
|
||||
DateTimeOffset.Now,
|
||||
false,
|
||||
true)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
public record GetWrappedProcessByChannelNumber : FFmpegProcessRequest
|
||||
{
|
||||
public GetWrappedProcessByChannelNumber(string scheme, string host, string channelNumber) : base(
|
||||
channelNumber,
|
||||
"ts",
|
||||
DateTimeOffset.Now,
|
||||
false,
|
||||
true)
|
||||
{
|
||||
Scheme = scheme;
|
||||
Host = host;
|
||||
}
|
||||
|
||||
public string Scheme { get; }
|
||||
public string Host { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetWrappedProcessByChannelNumber>
|
||||
{
|
||||
private readonly IFFmpegProcessService _ffmpegProcessService;
|
||||
|
||||
public GetWrappedProcessByChannelNumberHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IFFmpegProcessService ffmpegProcessService)
|
||||
: base(dbContextFactory)
|
||||
{
|
||||
_ffmpegProcessService = ffmpegProcessService;
|
||||
}
|
||||
|
||||
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
|
||||
TvContext dbContext,
|
||||
GetWrappedProcessByChannelNumber request,
|
||||
Channel channel,
|
||||
string ffmpegPath)
|
||||
{
|
||||
bool saveReports = await dbContext.ConfigElements
|
||||
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
.Map(result => result.IfNone(false));
|
||||
|
||||
Process process = _ffmpegProcessService.WrapSegmenter(
|
||||
ffmpegPath,
|
||||
saveReports,
|
||||
channel,
|
||||
request.Scheme,
|
||||
request.Host);
|
||||
|
||||
return new PlayoutItemProcessModel(process, DateTimeOffset.MaxValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,7 +409,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
|
||||
.WithHardwareAcceleration(HardwareAccelerationKind.Nvenc)
|
||||
.WithDeinterlace(deinterlace);
|
||||
.WithDeinterlace(deinterlace)
|
||||
.WithInputPixelFormat("h264");
|
||||
|
||||
if (scale)
|
||||
{
|
||||
|
||||
@@ -104,6 +104,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
string inputPixelFormat,
|
||||
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))]
|
||||
Resolution profileResolution,
|
||||
[Values(true, false)]
|
||||
bool pad,
|
||||
// [ValueSource(typeof(TestData), nameof(TestData.SoftwareCodecs))] string profileCodec,
|
||||
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||
[ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] string profileCodec,
|
||||
@@ -112,13 +114,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||
{
|
||||
string name = GetStringSha256Hash(
|
||||
$"{inputCodec}_{inputPixelFormat}_{profileResolution}_{profileCodec}_{profileAcceleration}");
|
||||
$"{inputCodec}_{inputPixelFormat}_{pad}_{profileResolution}_{profileCodec}_{profileAcceleration}");
|
||||
|
||||
string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
string resolution = pad ? "1920x1060" : "1920x1080";
|
||||
|
||||
var args =
|
||||
$"-y -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 -f lavfi -i testsrc=duration=1:size=1920x1080:rate=30 -c:a aac -c:v {inputCodec} -shortest -pix_fmt {inputPixelFormat} -strict -2 {file}";
|
||||
$"-y -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 -c:a aac -c:v {inputCodec} -shortest -pix_fmt {inputPixelFormat} -strict -2 {file}";
|
||||
var p1 = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
@@ -146,8 +150,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
var metadataRepository = new Mock<IMetadataRepository>();
|
||||
metadataRepository
|
||||
.Setup(r => r.UpdateLocalStatistics(It.IsAny<int>(), It.IsAny<MediaVersion>(), It.IsAny<bool>()))
|
||||
.Callback<int, MediaVersion, bool>((_, version, _) => v = version);
|
||||
.Setup(r => r.UpdateLocalStatistics(It.IsAny<MediaItem>(), It.IsAny<MediaVersion>(), It.IsAny<bool>()))
|
||||
.Callback<MediaItem, MediaVersion, bool>((_, version, _) => v = version);
|
||||
|
||||
var localStatisticsProvider = new LocalStatisticsProvider(
|
||||
metadataRepository.Object,
|
||||
@@ -200,6 +204,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
TimeSpan.FromSeconds(5));
|
||||
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
|
||||
// Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}");
|
||||
|
||||
process.Start().Should().BeTrue();
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
{
|
||||
TransportStream = 1,
|
||||
HttpLiveStreamingDirect = 2,
|
||||
HttpLiveStreamingHybrid = 3,
|
||||
HttpLiveStreamingSegmenter = 4
|
||||
// HttpLiveStreamingHybrid = 3,
|
||||
HttpLiveStreamingSegmenter = 4,
|
||||
TransportStreamHybrid = 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,11 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
bool isHardwareDecode = acceleration switch
|
||||
{
|
||||
HardwareAccelerationKind.Vaapi => !isSong && _inputCodec != "mpeg4",
|
||||
|
||||
// we need an initial hwupload_cuda when only padding with these pixel formats
|
||||
HardwareAccelerationKind.Nvenc when _scaleToSize.IsNone && _padToSize.IsSome =>
|
||||
!isSong && !_pixelFormat.Contains("p10le") && !_pixelFormat.Contains("444"),
|
||||
|
||||
HardwareAccelerationKind.Nvenc => !isSong,
|
||||
HardwareAccelerationKind.Qsv => !isSong,
|
||||
_ => false
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
result.VideoCodec = "copy";
|
||||
result.Deinterlace = false;
|
||||
break;
|
||||
case StreamingMode.HttpLiveStreamingHybrid:
|
||||
case StreamingMode.TransportStreamHybrid:
|
||||
case StreamingMode.HttpLiveStreamingSegmenter:
|
||||
case StreamingMode.TransportStream:
|
||||
result.HardwareAcceleration = ffmpegProfile.HardwareAcceleration;
|
||||
|
||||
@@ -202,6 +202,13 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithCopyCodec()
|
||||
{
|
||||
_arguments.Add("-c");
|
||||
_arguments.Add("copy");
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithWatermark(
|
||||
Option<WatermarkOptions> watermarkOptions,
|
||||
IDisplaySize resolution)
|
||||
@@ -626,13 +633,13 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
switch (_vaapiDriver)
|
||||
{
|
||||
case VaapiDriver.i965:
|
||||
startInfo.EnvironmentVariables.Add("LIBVA_DRIVER_NAME", "i965");
|
||||
startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "i965";
|
||||
break;
|
||||
case VaapiDriver.iHD:
|
||||
startInfo.EnvironmentVariables.Add("LIBVA_DRIVER_NAME", "iHD");
|
||||
startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "iHD";
|
||||
break;
|
||||
case VaapiDriver.RadeonSI:
|
||||
startInfo.EnvironmentVariables.Add("LIBVA_DRIVER_NAME", "radeonsi");
|
||||
startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "radeonsi";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -652,8 +659,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
// colon after drive letter needs to be escaped
|
||||
fileName = fileName.Replace(@":/", @"\:/");
|
||||
}
|
||||
|
||||
startInfo.EnvironmentVariables.Add("FFREPORT", $"file={fileName}:level=32");
|
||||
|
||||
startInfo.EnvironmentVariables["FFREPORT"] = $"file={fileName}:level=32";
|
||||
}
|
||||
|
||||
startInfo.ArgumentList.Add("-nostdin");
|
||||
|
||||
@@ -247,6 +247,24 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
.Build();
|
||||
}
|
||||
|
||||
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
|
||||
{
|
||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
|
||||
|
||||
return new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
|
||||
.WithThreads(1)
|
||||
.WithQuiet()
|
||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
||||
.WithInput($"http://localhost:{Settings.ListenPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter")
|
||||
.WithMap("0")
|
||||
.WithCopyCodec()
|
||||
.WithMetadata(channel, None)
|
||||
.WithFormat("mpegts")
|
||||
.WithPipe()
|
||||
.Build();
|
||||
}
|
||||
|
||||
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile)
|
||||
{
|
||||
return new FFmpegProcessBuilder(ffmpegPath, false, _logger)
|
||||
|
||||
@@ -22,8 +22,7 @@ namespace ErsatzTV.Core.Hdhr
|
||||
|
||||
public string URL => _channel.StreamingMode switch
|
||||
{
|
||||
StreamingMode.HttpLiveStreamingDirect or StreamingMode.HttpLiveStreamingHybrid =>
|
||||
$"{_scheme}://{_host}/iptv/channel/{_channel.Number}.m3u8",
|
||||
StreamingMode.HttpLiveStreamingDirect => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.m3u8",
|
||||
_ => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.ts"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg
|
||||
|
||||
Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
||||
|
||||
Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
||||
|
||||
Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile);
|
||||
|
||||
Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile);
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<bool> RemoveActor(Actor actor);
|
||||
Task<bool> Update(Domain.Metadata metadata);
|
||||
Task<bool> Add(Domain.Metadata metadata);
|
||||
Task<bool> UpdateLocalStatistics(int mediaVersionId, MediaVersion incoming, bool updateVersion = true);
|
||||
Task<bool> UpdateLocalStatistics(MediaItem mediaItem, MediaVersion incoming, bool updateVersion = true);
|
||||
Task<bool> UpdatePlexStatistics(int mediaVersionId, MediaVersion incoming);
|
||||
Task<Unit> UpdateArtworkPath(Artwork artwork);
|
||||
Task<Unit> AddArtwork(Domain.Metadata metadata, Artwork artwork);
|
||||
|
||||
@@ -43,9 +43,9 @@ namespace ErsatzTV.Core.Iptv
|
||||
string format = channel.StreamingMode switch
|
||||
{
|
||||
StreamingMode.HttpLiveStreamingDirect => "m3u8?mode=hls-direct",
|
||||
StreamingMode.HttpLiveStreamingHybrid => "m3u8",
|
||||
StreamingMode.HttpLiveStreamingSegmenter => "m3u8?mode=segmenter",
|
||||
_ => "ts"
|
||||
StreamingMode.TransportStreamHybrid => "ts",
|
||||
_ => "ts?mode=legacy"
|
||||
};
|
||||
|
||||
string vcodec = channel.FFmpegProfile.VideoCodec.Split("_").Head();
|
||||
|
||||
@@ -132,7 +132,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
|
||||
version.DateUpdated = _localFileSystem.GetLastWriteTime(filePath);
|
||||
|
||||
return await _metadataRepository.UpdateLocalStatistics(mediaItemVersion.Id, version) && durationChange;
|
||||
return await _metadataRepository.UpdateLocalStatistics(mediaItem, version) && durationChange;
|
||||
}
|
||||
|
||||
private Task<Either<BaseError, FFprobe>> GetProbeOutput(string ffprobePath, string filePath)
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -110,14 +111,17 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateLocalStatistics(
|
||||
int mediaVersionId,
|
||||
MediaItem mediaItem,
|
||||
MediaVersion incoming,
|
||||
bool updateVersion = true)
|
||||
{
|
||||
int mediaVersionId = mediaItem.GetHeadVersion().Id;
|
||||
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
Option<MediaVersion> maybeVersion = await dbContext.MediaVersions
|
||||
.Include(v => v.Streams)
|
||||
.Include(v => v.Chapters)
|
||||
.Include(v => v.MediaFiles)
|
||||
.OrderBy(v => v.Id)
|
||||
.SingleOrDefaultAsync(v => v.Id == mediaVersionId)
|
||||
.Map(Optional);
|
||||
@@ -191,7 +195,37 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
existingChapter.Title = incomingChapter.Title;
|
||||
}
|
||||
|
||||
return await dbContext.SaveChangesAsync() > 0;
|
||||
if (await dbContext.SaveChangesAsync() <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// reload the media versions so we can properly index the duration
|
||||
switch (mediaItem)
|
||||
{
|
||||
case Movie movie:
|
||||
movie.MediaVersions.Clear();
|
||||
movie.MediaVersions.Add(existing);
|
||||
break;
|
||||
case Episode episode:
|
||||
episode.MediaVersions.Clear();
|
||||
episode.MediaVersions.Add(existing);
|
||||
break;
|
||||
case MusicVideo musicVideo:
|
||||
musicVideo.MediaVersions.Clear();
|
||||
musicVideo.MediaVersions.Add(existing);
|
||||
break;
|
||||
case OtherVideo otherVideo:
|
||||
otherVideo.MediaVersions.Clear();
|
||||
otherVideo.MediaVersions.Add(existing);
|
||||
break;
|
||||
case Song song:
|
||||
song.MediaVersions.Clear();
|
||||
song.MediaVersions.Add(existing);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
() => Task.FromResult(false));
|
||||
}
|
||||
|
||||
@@ -30,9 +30,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
LibraryPath libraryPath,
|
||||
string path)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
Option<MusicVideo> maybeExisting = await dbContext.MusicVideos
|
||||
.AsNoTracking()
|
||||
.Include(mv => mv.Artist)
|
||||
.ThenInclude(a => a.ArtistMetadata)
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Artwork)
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
@@ -196,6 +198,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
await dbContext.MusicVideos.AddAsync(musicVideo);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContext.Entry(musicVideo).Reference(m => m.Artist).LoadAsync();
|
||||
await dbContext.Entry(musicVideo.Artist).Collection(a => a.ArtistMetadata).LoadAsync();
|
||||
await dbContext.Entry(musicVideo).Reference(m => m.LibraryPath).LoadAsync();
|
||||
await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
||||
return new MediaItemScanResult<MusicVideo>(musicVideo) { IsAdded = true };
|
||||
|
||||
@@ -83,6 +83,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.ThenInclude(mm => mm.Studios)
|
||||
.Include(mi => (mi as Show).ShowMetadata)
|
||||
.ThenInclude(mm => mm.Actors)
|
||||
.Include(mi => (mi as MusicVideo).Artist)
|
||||
.ThenInclude(mm => mm.ArtistMetadata)
|
||||
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
|
||||
.ThenInclude(mm => mm.Genres)
|
||||
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
|
||||
|
||||
Generated
+3855
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Remove_HLSHybridStreamingMode : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// replace HLS Hybrid with HLS Segmenter
|
||||
migrationBuilder.Sql("UPDATE Channel SET StreamingMode = 4 WHERE StreamingMode = 3");
|
||||
|
||||
// replace MPEG-TS (Legacy) with new MPEG-TS
|
||||
migrationBuilder.Sql("UPDATE Channel SET StreamingMode = 5 WHERE StreamingMode = 1");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.0");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.1");
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
public int Version => 18;
|
||||
public int Version => 19;
|
||||
|
||||
public Task<bool> Initialize(ILocalFileSystem localFileSystem)
|
||||
{
|
||||
@@ -675,6 +675,14 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
{
|
||||
doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
if (musicVideo.Artist != null)
|
||||
{
|
||||
foreach (ArtistMetadata artistMetadata in musicVideo.Artist.ArtistMetadata)
|
||||
{
|
||||
doc.Add(new TextField(ArtistField, artistMetadata.Title, Field.Store.NO));
|
||||
}
|
||||
}
|
||||
|
||||
_writer.UpdateDocument(new Term(IdField, musicVideo.Id.ToString()), doc);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace ErsatzTV.Controllers
|
||||
public Task<IActionResult> GetConcatPlaylist(string channelNumber) =>
|
||||
_mediator.Send(new GetConcatPlaylistByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber))
|
||||
.ToActionResult();
|
||||
|
||||
|
||||
[HttpGet("ffmpeg/stream/{channelNumber}")]
|
||||
public Task<IActionResult> GetStream(
|
||||
string channelNumber,
|
||||
|
||||
@@ -52,14 +52,24 @@ namespace ErsatzTV.Controllers
|
||||
.Map<ChannelGuide, IActionResult>(Ok);
|
||||
|
||||
[HttpGet("iptv/channel/{channelNumber}.ts")]
|
||||
public Task<IActionResult> GetTransportStreamVideo(string channelNumber) =>
|
||||
_mediator.Send(new GetConcatProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber))
|
||||
public async Task<IActionResult> GetTransportStreamVideo(
|
||||
string channelNumber,
|
||||
[FromQuery]
|
||||
string mode = null)
|
||||
{
|
||||
FFmpegProcessRequest request = mode switch
|
||||
{
|
||||
"legacy" => new GetConcatProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber),
|
||||
_ => new GetWrappedProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber)
|
||||
};
|
||||
|
||||
return await _mediator.Send(request)
|
||||
.Map(
|
||||
result => result.Match<IActionResult>(
|
||||
processModel =>
|
||||
{
|
||||
Process process = processModel.Process;
|
||||
|
||||
|
||||
_logger.LogInformation("Starting ts stream for channel {ChannelNumber}", channelNumber);
|
||||
// _logger.LogDebug(
|
||||
// "ffmpeg concat arguments {FFmpegArguments}",
|
||||
@@ -68,6 +78,7 @@ namespace ErsatzTV.Controllers
|
||||
return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t");
|
||||
},
|
||||
error => BadRequest(error.Value)));
|
||||
}
|
||||
|
||||
[HttpGet("iptv/session/{channelNumber}/hls.m3u8")]
|
||||
public async Task<IActionResult> GetLivePlaylist(string channelNumber)
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
<MudTextField Label="Number" @bind-Value="_model.Number" For="@(() => _model.Number)" Immediate="true"/>
|
||||
<MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/>
|
||||
<MudSelect Class="mt-3" Label="Streaming Mode" @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)">
|
||||
<MudSelectItem Value="@(StreamingMode.TransportStream)">MPEG-TS</MudSelectItem>
|
||||
<MudSelectItem Value="@(StreamingMode.TransportStreamHybrid)">MPEG-TS</MudSelectItem>
|
||||
<MudSelectItem Value="@(StreamingMode.TransportStream)">MPEG-TS (Legacy)</MudSelectItem>
|
||||
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingDirect)">HLS Direct</MudSelectItem>
|
||||
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingHybrid)">HLS Hybrid</MudSelectItem>
|
||||
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenter)">HLS Segmenter</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudSelect Class="mt-3" Label="FFmpeg Profile" @bind-Value="_model.FFmpegProfileId" For="@(() => _model.FFmpegProfileId)"
|
||||
@@ -154,7 +154,7 @@
|
||||
_model.Number = (maxNumber + 1).ToString();
|
||||
_model.Name = "New Channel";
|
||||
_model.FFmpegProfileId = ffmpegSettings.DefaultFFmpegProfileId;
|
||||
_model.StreamingMode = StreamingMode.TransportStream;
|
||||
_model.StreamingMode = StreamingMode.TransportStreamHybrid;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,9 +140,9 @@
|
||||
|
||||
private static string GetStreamingMode(StreamingMode streamingMode) => streamingMode switch {
|
||||
StreamingMode.HttpLiveStreamingDirect => "HLS Direct",
|
||||
StreamingMode.HttpLiveStreamingHybrid => "HLS Hybrid",
|
||||
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
|
||||
_ => "MPEG-TS"
|
||||
StreamingMode.TransportStreamHybrid => "MPEG-TS",
|
||||
_ => "MPEG-TS (Legacy)"
|
||||
};
|
||||
|
||||
}
|
||||
@@ -8,12 +8,21 @@ Channel numbers can be whole numbers or can contain one decimal, like `500` or `
|
||||
|
||||
### Streaming Mode
|
||||
|
||||
Four streaming modes are currently supported: `MPEG-TS` (Transport Stream), `HLS Direct` (HTTP Live Streaming Direct), `HLS Hybrid` (HTTP Live Streaming Hybrid) and `HLS Segmenter` (HTTP Live Streaming Segmenter).
|
||||
Four streaming modes are currently supported. Ideally one of the two primary streaming modes should be used; secondary streaming modes are only needed in some edge cases.
|
||||
|
||||
#### Primary Streaming Modes
|
||||
|
||||
The two primary streaming modes are `HLS Segmenter` (HTTP Live Streaming Segmenter) and `MPEG-TS` (Transport Stream).
|
||||
|
||||
* `HLS Segmenter` transcodes content, supports watermarks and offers the best performance at program boundaries
|
||||
* `MPEG-TS` is a light wrapper over the `HLS Segmenter` streaming mode and provides support for clients that require an HDHomeRun tuner
|
||||
|
||||
#### Secondary Streaming Modes
|
||||
|
||||
The two secondary streaming modes are `HLS Direct` (HTTP Live Streaming Direct) and `MPEG-TS (Legacy)` (Legacy Transport Stream).
|
||||
|
||||
* `MPEG-TS` transcodes content and supports watermarks, though some clients will have issues at program boundaries
|
||||
* `HLS Direct` does not transcode content and can perform better on low power systems, but does not support watermarks and some clients will have issues at program boundaries
|
||||
* `HLS Hybrid` transcodes content and supports watermarks, and can perform very well at program boundaries with some clients
|
||||
* `HLS Segmenter` is currently *experimental*, and is the only "proper" HLS mode. It transcodes content, supports watermarks and offers the best performance at program boundaries.
|
||||
* `MPEG-TS (Legacy)` transcodes content and supports watermarks, though some clients will have issues at program boundaries. This mode will be removed with a future release.
|
||||
|
||||
### FFmpeg Profile
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ The following fields are available for searching artists:
|
||||
The following fields are available for searching music videos:
|
||||
|
||||
- `title`: The music video title
|
||||
- `artist`: The music video artist
|
||||
- `album`: The music video album
|
||||
- `genre`: The music video genre
|
||||
- `library_name`: The name of the library that contains the music video
|
||||
|
||||
Reference in New Issue
Block a user