Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a2d6a558c | ||
|
|
fb539d6a32 | ||
|
|
6ba5df6be9 | ||
|
|
33209c3b33 | ||
|
|
2177ef9814 |
@@ -288,8 +288,9 @@
|
|||||||
fetch(this.apiBase + '/All', { headers: this.getHeaders() })
|
fetch(this.apiBase + '/All', { headers: this.getHeaders() })
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(recordings) {
|
.then(function(recordings) {
|
||||||
|
var activeStates = ['Scheduled', 'WaitingForStream', 'Recording', 0, 1, 2];
|
||||||
var active = recordings.filter(function(r) {
|
var active = recordings.filter(function(r) {
|
||||||
return r.state === 0 || r.state === 1 || r.state === 2;
|
return activeStates.indexOf(r.state) !== -1;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (active.length === 0) {
|
if (active.length === 0) {
|
||||||
@@ -297,8 +298,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var stateLabels = {0: 'Scheduled', 1: 'Waiting', 2: 'Recording', 3: 'Completed', 4: 'Failed', 5: 'Cancelled'};
|
var stateLabels = {'Scheduled': 'Scheduled', 'WaitingForStream': 'Waiting', 'Recording': 'Recording', 'Completed': 'Completed', 'Failed': 'Failed', 'Cancelled': 'Cancelled', 0: 'Scheduled', 1: 'Waiting', 2: 'Recording', 3: 'Completed', 4: 'Failed', 5: 'Cancelled'};
|
||||||
var stateColors = {0: '#2196F3', 1: '#FF9800', 2: '#4CAF50', 4: '#f44336', 5: '#9E9E9E'};
|
var stateColors = {'Scheduled': '#2196F3', 'WaitingForStream': '#FF9800', 'Recording': '#4CAF50', 'Failed': '#f44336', 'Cancelled': '#9E9E9E', 0: '#2196F3', 1: '#FF9800', 2: '#4CAF50', 4: '#f44336', 5: '#9E9E9E'};
|
||||||
|
|
||||||
var html = '<table style="width:100%; border-collapse: collapse;">';
|
var html = '<table style="width:100%; border-collapse: collapse;">';
|
||||||
html += '<thead><tr style="text-align:left; border-bottom: 1px solid #444;">';
|
html += '<thead><tr style="text-align:left; border-bottom: 1px solid #444;">';
|
||||||
@@ -314,7 +315,7 @@
|
|||||||
html += '<td style="padding: 8px;"><span style="color:' + (stateColors[r.state] || '#fff') + ';">' + (stateLabels[r.state] || r.state) + '</span></td>';
|
html += '<td style="padding: 8px;"><span style="color:' + (stateColors[r.state] || '#fff') + ';">' + (stateLabels[r.state] || r.state) + '</span></td>';
|
||||||
html += '<td style="padding: 8px;">' + SRFPlayRecordings.formatDate(r.validFrom) + '</td>';
|
html += '<td style="padding: 8px;">' + SRFPlayRecordings.formatDate(r.validFrom) + '</td>';
|
||||||
html += '<td style="padding: 8px;">';
|
html += '<td style="padding: 8px;">';
|
||||||
if (r.state === 2) {
|
if (r.state === 2 || r.state === 'Recording') {
|
||||||
html += '<button is="emby-button" type="button" class="raised emby-button" ';
|
html += '<button is="emby-button" type="button" class="raised emby-button" ';
|
||||||
html += 'onclick="SRFPlayRecordings.stopRecording(\'' + r.id + '\')"><span>Stop</span></button>';
|
html += 'onclick="SRFPlayRecordings.stopRecording(\'' + r.id + '\')"><span>Stop</span></button>';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class RecordingSchedulerTask : IScheduledTask
|
|||||||
new TaskTriggerInfo
|
new TaskTriggerInfo
|
||||||
{
|
{
|
||||||
Type = TaskTriggerInfo.TriggerInterval,
|
Type = TaskTriggerInfo.TriggerInterval,
|
||||||
IntervalTicks = TimeSpan.FromMinutes(2).Ticks
|
IntervalTicks = TimeSpan.FromSeconds(30).Ticks
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using Jellyfin.Plugin.SRFPlay.Api.Models;
|
|||||||
using Jellyfin.Plugin.SRFPlay.Api.Models.PlayV3;
|
using Jellyfin.Plugin.SRFPlay.Api.Models.PlayV3;
|
||||||
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
|
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.SRFPlay.Services;
|
namespace Jellyfin.Plugin.SRFPlay.Services;
|
||||||
@@ -29,9 +30,11 @@ public class RecordingService : IRecordingService, IDisposable
|
|||||||
private readonly IStreamUrlResolver _streamUrlResolver;
|
private readonly IStreamUrlResolver _streamUrlResolver;
|
||||||
private readonly IMediaCompositionFetcher _mediaCompositionFetcher;
|
private readonly IMediaCompositionFetcher _mediaCompositionFetcher;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly ConcurrentDictionary<string, Process> _activeProcesses = new();
|
private readonly ConcurrentDictionary<string, Process> _activeProcesses = new();
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
|
private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
|
||||||
private readonly SemaphoreSlim _persistLock = new(1, 1);
|
private readonly SemaphoreSlim _persistLock = new(1, 1);
|
||||||
|
private readonly SemaphoreSlim _processLock = new(1, 1);
|
||||||
private List<RecordingEntry> _recordings = new();
|
private List<RecordingEntry> _recordings = new();
|
||||||
private bool _loaded;
|
private bool _loaded;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
@@ -45,13 +48,15 @@ public class RecordingService : IRecordingService, IDisposable
|
|||||||
/// <param name="streamUrlResolver">The stream URL resolver.</param>
|
/// <param name="streamUrlResolver">The stream URL resolver.</param>
|
||||||
/// <param name="mediaCompositionFetcher">The media composition fetcher.</param>
|
/// <param name="mediaCompositionFetcher">The media composition fetcher.</param>
|
||||||
/// <param name="appHost">The application host.</param>
|
/// <param name="appHost">The application host.</param>
|
||||||
|
/// <param name="mediaEncoder">The media encoder for ffmpeg path.</param>
|
||||||
public RecordingService(
|
public RecordingService(
|
||||||
ILogger<RecordingService> logger,
|
ILogger<RecordingService> logger,
|
||||||
ISRFApiClientFactory apiClientFactory,
|
ISRFApiClientFactory apiClientFactory,
|
||||||
IStreamProxyService proxyService,
|
IStreamProxyService proxyService,
|
||||||
IStreamUrlResolver streamUrlResolver,
|
IStreamUrlResolver streamUrlResolver,
|
||||||
IMediaCompositionFetcher mediaCompositionFetcher,
|
IMediaCompositionFetcher mediaCompositionFetcher,
|
||||||
IServerApplicationHost appHost)
|
IServerApplicationHost appHost,
|
||||||
|
IMediaEncoder mediaEncoder)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_apiClientFactory = apiClientFactory;
|
_apiClientFactory = apiClientFactory;
|
||||||
@@ -59,6 +64,7 @@ public class RecordingService : IRecordingService, IDisposable
|
|||||||
_streamUrlResolver = streamUrlResolver;
|
_streamUrlResolver = streamUrlResolver;
|
||||||
_mediaCompositionFetcher = mediaCompositionFetcher;
|
_mediaCompositionFetcher = mediaCompositionFetcher;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
|
_mediaEncoder = mediaEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetDataFilePath()
|
private string GetDataFilePath()
|
||||||
@@ -300,6 +306,25 @@ public class RecordingService : IRecordingService, IDisposable
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task ProcessRecordingsAsync(CancellationToken cancellationToken)
|
public async Task ProcessRecordingsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Prevent overlapping scheduler runs from spawning duplicate ffmpeg processes
|
||||||
|
if (!await _processLock.WaitAsync(0).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("ProcessRecordingsAsync already running, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessRecordingsCoreAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_processLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessRecordingsCoreAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await LoadRecordingsAsync().ConfigureAwait(false);
|
await LoadRecordingsAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -416,7 +441,7 @@ public class RecordingService : IRecordingService, IDisposable
|
|||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
StartInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = "ffmpeg",
|
FileName = _mediaEncoder.EncoderPath,
|
||||||
Arguments = $"-y -i \"{inputUrl}\" -c copy -movflags +faststart \"{outputPath}\"",
|
Arguments = $"-y -i \"{inputUrl}\" -c copy -movflags +faststart \"{outputPath}\"",
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
RedirectStandardInput = true,
|
RedirectStandardInput = true,
|
||||||
@@ -510,6 +535,7 @@ public class RecordingService : IRecordingService, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
_persistLock.Dispose();
|
_persistLock.Dispose();
|
||||||
|
_processLock.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|||||||
@@ -8,6 +8,22 @@
|
|||||||
"category": "Live TV",
|
"category": "Live TV",
|
||||||
"imageUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/assests/main%20logo.png",
|
"imageUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/assests/main%20logo.png",
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "1.0.25",
|
||||||
|
"changelog": "Release 1.0.25",
|
||||||
|
"targetAbi": "10.9.0.0",
|
||||||
|
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/v1.0.25/srfplay_1.0.25.0.zip",
|
||||||
|
"checksum": "5e4599cfeee7e0845a1be30ec288cc0b",
|
||||||
|
"timestamp": "2026-03-07T15:11:52Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.0.24",
|
||||||
|
"changelog": "Release 1.0.24",
|
||||||
|
"targetAbi": "10.9.0.0",
|
||||||
|
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/v1.0.24/srfplay_1.0.24.0.zip",
|
||||||
|
"checksum": "f54dfb8cd9b555471859ffc89c35fb90",
|
||||||
|
"timestamp": "2026-03-07T14:55:13Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "1.0.23",
|
"version": "1.0.23",
|
||||||
"changelog": "Release 1.0.23",
|
"changelog": "Release 1.0.23",
|
||||||
|
|||||||
Reference in New Issue
Block a user