private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, CancellationToken cancellationToken) { if (timer == null) { throw new ArgumentNullException("timer"); } if (string.IsNullOrWhiteSpace(timer.ProgramId)) { throw new InvalidOperationException("timer.ProgramId is null. Cannot record."); } var info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId); if (info == null) { throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId)); } var recordPath = RecordingPath; if (info.IsMovie) { recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name).Trim()); } else if (info.IsSeries) { recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name).Trim()); } else if (info.IsKids) { recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name).Trim()); } else if (info.IsSports) { recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name).Trim()); } else { recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name).Trim()); } var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts"; recordPath = Path.Combine(recordPath, recordingFileName); _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath)); var recordingId = info.Id.GetMD5().ToString("N"); var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, recordingId, StringComparison.OrdinalIgnoreCase)); if (recording == null) { recording = new RecordingInfo { ChannelId = info.ChannelId, Id = recordingId, StartDate = info.StartDate, EndDate = info.EndDate, Genres = info.Genres, IsKids = info.IsKids, IsLive = info.IsLive, IsMovie = info.IsMovie, IsHD = info.IsHD, IsNews = info.IsNews, IsPremiere = info.IsPremiere, IsSeries = info.IsSeries, IsSports = info.IsSports, IsRepeat = !info.IsPremiere, Name = info.Name, EpisodeTitle = info.EpisodeTitle, ProgramId = info.Id, ImagePath = info.ImagePath, ImageUrl = info.ImageUrl, OriginalAirDate = info.OriginalAirDate, Status = RecordingStatus.Scheduled, Overview = info.Overview, SeriesTimerId = timer.SeriesTimerId, TimerId = timer.Id, ShowId = info.ShowId }; _recordingProvider.AddOrUpdate(recording); } try { var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None); var mediaStreamInfo = result.Item1; var isResourceOpen = true; // Unfortunately due to the semaphore we have to have a nested try/finally try { // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg await Task.Delay(3000, cancellationToken).ConfigureAwait(false); var duration = recordingEndDate - DateTime.UtcNow; HttpRequestOptions httpRequestOptions = new HttpRequestOptions() { Url = mediaStreamInfo.Path }; recording.Path = recordPath; recording.Status = RecordingStatus.InProgress; recording.DateLastUpdated = DateTime.UtcNow; _recordingProvider.AddOrUpdate(recording); _logger.Info("Beginning recording."); httpRequestOptions.BufferContent = false; var durationToken = new CancellationTokenSource(duration); var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; httpRequestOptions.CancellationToken = linkedToken; _logger.Info("Writing file to path: " + recordPath); using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET")) { using (var output = _fileSystem.GetFileStream(recordPath, FileMode.Create, FileAccess.Write, FileShare.Read)) { result.Item2.Release(); isResourceOpen = false; await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken); } } recording.Status = RecordingStatus.Completed; _logger.Info("Recording completed"); } finally { if (isResourceOpen) { result.Item2.Release(); } } } catch (OperationCanceledException) { _logger.Info("Recording stopped"); recording.Status = RecordingStatus.Completed; } catch (Exception ex) { _logger.ErrorException("Error recording", ex); recording.Status = RecordingStatus.Error; } finally { CancellationTokenSource removed; _activeRecordings.TryRemove(timer.Id, out removed); } recording.DateLastUpdated = DateTime.UtcNow; _recordingProvider.AddOrUpdate(recording); if (recording.Status == RecordingStatus.Completed) { OnSuccessfulRecording(recording); _timerProvider.Delete(timer); } else if (DateTime.UtcNow < timer.EndDate) { const int retryIntervalSeconds = 60; _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds); _timerProvider.StartTimer(timer, TimeSpan.FromSeconds(retryIntervalSeconds)); } else { _timerProvider.Delete(timer); _recordingProvider.Delete(recording); } }
private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, CancellationToken cancellationToken) { if (timer == null) { throw new ArgumentNullException("timer"); } ProgramInfo info = null; if (string.IsNullOrWhiteSpace(timer.ProgramId)) { _logger.Info("Timer {0} has null programId", timer.Id); } else { info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId); } if (info == null) { _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId); info = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate); } if (info == null) { throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId)); } var recordPath = RecordingPath; if (info.IsMovie) { recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name).Trim()); } else if (info.IsSeries) { recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name).Trim()); } else if (info.IsKids) { recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name).Trim()); } else if (info.IsSports) { recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name).Trim()); } else { recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name).Trim()); } var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts"; recordPath = Path.Combine(recordPath, recordingFileName); _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath)); var recordingId = info.Id.GetMD5().ToString("N"); var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, recordingId, StringComparison.OrdinalIgnoreCase)); if (recording == null) { recording = new RecordingInfo { ChannelId = info.ChannelId, Id = recordingId, StartDate = info.StartDate, EndDate = info.EndDate, Genres = info.Genres, IsKids = info.IsKids, IsLive = info.IsLive, IsMovie = info.IsMovie, IsHD = info.IsHD, IsNews = info.IsNews, IsPremiere = info.IsPremiere, IsSeries = info.IsSeries, IsSports = info.IsSports, IsRepeat = !info.IsPremiere, Name = info.Name, EpisodeTitle = info.EpisodeTitle, ProgramId = info.Id, ImagePath = info.ImagePath, ImageUrl = info.ImageUrl, OriginalAirDate = info.OriginalAirDate, Status = RecordingStatus.Scheduled, Overview = info.Overview, SeriesTimerId = timer.SeriesTimerId, TimerId = timer.Id, ShowId = info.ShowId }; _recordingProvider.AddOrUpdate(recording); } try { var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false); var mediaStreamInfo = result.Item1; var isResourceOpen = true; // Unfortunately due to the semaphore we have to have a nested try/finally try { // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg await Task.Delay(3000, cancellationToken).ConfigureAwait(false); var duration = recordingEndDate - DateTime.UtcNow; var recorder = await GetRecorder().ConfigureAwait(false); if (recorder is EncodedRecorder) { recordPath = Path.ChangeExtension(recordPath, ".mp4"); } recording.Path = recordPath; recording.Status = RecordingStatus.InProgress; recording.DateLastUpdated = DateTime.UtcNow; _recordingProvider.AddOrUpdate(recording); _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture)); var durationToken = new CancellationTokenSource(duration); var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; _logger.Info("Writing file to path: " + recordPath); _logger.Info("Opening recording stream from tuner provider"); Action onStarted = () => { result.Item2.Release(); isResourceOpen = false; }; await recorder.Record(mediaStreamInfo, recordPath, onStarted, linkedToken).ConfigureAwait(false); recording.Status = RecordingStatus.Completed; _logger.Info("Recording completed"); } finally { if (isResourceOpen) { result.Item2.Release(); } } } catch (OperationCanceledException) { _logger.Info("Recording stopped"); recording.Status = RecordingStatus.Completed; } catch (Exception ex) { _logger.ErrorException("Error recording", ex); recording.Status = RecordingStatus.Error; } finally { CancellationTokenSource removed; _activeRecordings.TryRemove(timer.Id, out removed); } recording.DateLastUpdated = DateTime.UtcNow; _recordingProvider.AddOrUpdate(recording); if (recording.Status == RecordingStatus.Completed) { OnSuccessfulRecording(recording); _timerProvider.Delete(timer); } else if (DateTime.UtcNow < timer.EndDate) { const int retryIntervalSeconds = 60; _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds); _timerProvider.StartTimer(timer, TimeSpan.FromSeconds(retryIntervalSeconds)); } else { _timerProvider.Delete(timer); _recordingProvider.Delete(recording); } }