fixes #888 - Support m3u8 subtitle playlists

This commit is contained in:
Luke Pulverenti 2014-08-05 21:09:03 -04:00
parent 3ba6364f25
commit 3ff3d04284
33 changed files with 187 additions and 84 deletions

View File

@ -1605,6 +1605,8 @@ namespace MediaBrowser.Api.Playback
{ {
state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true); state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
} }
state.AllMediaStreams = mediaStreams;
} }
private async Task<MediaSourceInfo> GetChannelMediaInfo(string id, private async Task<MediaSourceInfo> GetChannelMediaInfo(string id,

View File

@ -5,6 +5,8 @@ using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using ServiceStack; using ServiceStack;
using System; using System;
@ -18,20 +20,20 @@ using System.Threading.Tasks;
namespace MediaBrowser.Api.Playback.Hls namespace MediaBrowser.Api.Playback.Hls
{ {
[Route("/Videos/{Id}/master.m3u8", "GET")] [Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
[Api(Description = "Gets a video stream using HTTP live streaming.")]
public class GetMasterHlsVideoStream : VideoStreamRequest public class GetMasterHlsVideoStream : VideoStreamRequest
{ {
public bool EnableAdaptiveBitrateStreaming { get; set; } public bool EnableAdaptiveBitrateStreaming { get; set; }
public SubtitleDeliveryMethod SubtitleMethod { get; set; }
public GetMasterHlsVideoStream() public GetMasterHlsVideoStream()
{ {
EnableAdaptiveBitrateStreaming = true; EnableAdaptiveBitrateStreaming = true;
} }
} }
[Route("/Videos/{Id}/main.m3u8", "GET")] [Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
[Api(Description = "Gets a video stream using HTTP live streaming.")]
public class GetMainHlsVideoStream : VideoStreamRequest public class GetMainHlsVideoStream : VideoStreamRequest
{ {
} }
@ -359,7 +361,17 @@ namespace MediaBrowser.Api.Playback.Hls
var playlistUrl = (state.RunTimeTicks ?? 0) > 0 ? "main.m3u8" : "live.m3u8"; var playlistUrl = (state.RunTimeTicks ?? 0) > 0 ? "main.m3u8" : "live.m3u8";
playlistUrl += queryString; playlistUrl += queryString;
AppendPlaylist(builder, playlistUrl, totalBitrate); var request = (GetMasterHlsVideoStream) state.Request;
var subtitleStreams = state.AllMediaStreams
.Where(i => i.IsTextSubtitleStream)
.ToList();
var subtitleGroup = subtitleStreams.Count > 0 && request.SubtitleMethod == SubtitleDeliveryMethod.Hls ?
"subs" :
null;
AppendPlaylist(builder, playlistUrl, totalBitrate, subtitleGroup);
if (EnableAdaptiveBitrateStreaming(state)) if (EnableAdaptiveBitrateStreaming(state))
{ {
@ -369,16 +381,52 @@ namespace MediaBrowser.Api.Playback.Hls
var variation = GetBitrateVariation(totalBitrate); var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation; var newBitrate = totalBitrate - variation;
AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate); AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate, subtitleGroup);
variation *= 2; variation *= 2;
newBitrate = totalBitrate - variation; newBitrate = totalBitrate - variation;
AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate); AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate, subtitleGroup);
}
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
AddSubtitles(state, subtitleStreams, builder);
} }
return builder.ToString(); return builder.ToString();
} }
private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
{
var selectedIndex = state.SubtitleStream == null ? (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},URI=\"{3}\",LANGUAGE=\"{4}\"";
var name = stream.Language;
var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
var isForced = stream.IsForced;
if (string.IsNullOrWhiteSpace(name)) name = stream.Codec ?? "Unknown";
var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}",
state.Request.MediaSourceId,
stream.Index.ToString(UsCulture),
30.ToString(UsCulture));
var line = string.Format(format,
name,
isDefault ? "YES" : "NO",
isForced ? "YES" : "NO",
url,
stream.Language ?? "Unknown");
builder.AppendLine(line);
}
}
private bool EnableAdaptiveBitrateStreaming(StreamState state) private bool EnableAdaptiveBitrateStreaming(StreamState state)
{ {
var request = state.Request as GetMasterHlsVideoStream; var request = state.Request as GetMasterHlsVideoStream;
@ -397,9 +445,16 @@ namespace MediaBrowser.Api.Playback.Hls
return state.VideoRequest.VideoBitRate.HasValue; return state.VideoRequest.VideoBitRate.HasValue;
} }
private void AppendPlaylist(StringBuilder builder, string url, int bitrate) private void AppendPlaylist(StringBuilder builder, string url, int bitrate, string subtitleGroup)
{ {
builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + bitrate.ToString(UsCulture)); var header = "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + bitrate.ToString(UsCulture);
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
header += string.Format(",SUBTITLES=\"{0}\"", subtitleGroup);
}
builder.AppendLine(header);
builder.AppendLine(url); builder.AppendLine(url);
} }

View File

@ -38,6 +38,8 @@ namespace MediaBrowser.Api.Playback
public string InputContainer { get; set; } public string InputContainer { get; set; }
public List<MediaStream> AllMediaStreams { get; set; }
public MediaStream AudioStream { get; set; } public MediaStream AudioStream { get; set; }
public MediaStream VideoStream { get; set; } public MediaStream VideoStream { get; set; }
public MediaStream SubtitleStream { get; set; } public MediaStream SubtitleStream { get; set; }
@ -78,6 +80,7 @@ namespace MediaBrowser.Api.Playback
SupportedAudioCodecs = new List<string>(); SupportedAudioCodecs = new List<string>();
PlayableStreamFileNames = new List<string>(); PlayableStreamFileNames = new List<string>();
RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
AllMediaStreams = new List<MediaStream>();
} }
public string InputAudioSync { get; set; } public string InputAudioSync { get; set; }

View File

@ -9,8 +9,10 @@ using MediaBrowser.Model.Providers;
using ServiceStack; using ServiceStack;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -91,6 +93,29 @@ namespace MediaBrowser.Api.Subtitles
[ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] [ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public long StartPositionTicks { get; set; } public long StartPositionTicks { get; set; }
[ApiMember(Name = "EndPositionTicks", Description = "EndPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public long? EndPositionTicks { get; set; }
}
[Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")]
public class GetSubtitlePlaylist
{
/// <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 string Id { get; set; }
[ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string MediaSourceId { get; set; }
[ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
public int Index { get; set; }
[ApiMember(Name = "SegmentLength", Description = "The subtitle srgment length", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
public int SegmentLength { get; set; }
} }
public class SubtitleService : BaseApiService public class SubtitleService : BaseApiService
@ -106,6 +131,53 @@ namespace MediaBrowser.Api.Subtitles
_subtitleEncoder = subtitleEncoder; _subtitleEncoder = subtitleEncoder;
} }
public object Get(GetSubtitlePlaylist request)
{
var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
var mediaSource = item.GetMediaSources(false)
.First(i => string.Equals(i.Id, request.MediaSourceId ?? request.Id));
var builder = new StringBuilder();
var runtime = mediaSource.RunTimeTicks ?? -1;
if (runtime <= 0)
{
throw new ArgumentException("HLS Subtitles are not supported for this media.");
}
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture));
builder.AppendLine("#EXT-X-VERSION:3");
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
long positionTicks = 0;
var segmentLengthTicks = TimeSpan.FromSeconds(request.SegmentLength).Ticks;
while (positionTicks < runtime)
{
var remaining = runtime - positionTicks;
var lengthTicks = Math.Min(remaining, segmentLengthTicks);
builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
var url = string.Format("stream.srt?StartPositionTicks={0}&EndPositionTicks={1}",
positionTicks.ToString(CultureInfo.InvariantCulture),
endPositionTicks.ToString(CultureInfo.InvariantCulture));
builder.AppendLine(url);
positionTicks += segmentLengthTicks;
}
builder.AppendLine("#EXT-X-ENDLIST");
return ResultFactory.GetResult(builder.ToString(), Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
}
public object Get(GetSubtitle request) public object Get(GetSubtitle request)
{ {
if (string.IsNullOrEmpty(request.Format)) if (string.IsNullOrEmpty(request.Format))
@ -133,7 +205,7 @@ namespace MediaBrowser.Api.Subtitles
request.Index, request.Index,
request.Format, request.Format,
request.StartPositionTicks, request.StartPositionTicks,
null, request.EndPositionTicks,
CancellationToken.None).ConfigureAwait(false); CancellationToken.None).ConfigureAwait(false);
} }

View File

@ -193,11 +193,12 @@ namespace MediaBrowser.Dlna.Profiles
} }
}; };
SoftSubtitleProfiles = new[] SubtitleProfiles = new[]
{ {
new SubtitleProfile new SubtitleProfile
{ {
Format = "srt" Format = "srt",
Method = SubtitleDeliveryMethod.External
} }
}; };
} }

View File

@ -339,11 +339,12 @@ namespace MediaBrowser.Dlna.Profiles
} }
}; };
SoftSubtitleProfiles = new[] SubtitleProfiles = new[]
{ {
new SubtitleProfile new SubtitleProfile
{ {
Format = "smi" Format = "smi",
Method = SubtitleDeliveryMethod.External
} }
}; };
} }

View File

@ -155,11 +155,12 @@ namespace MediaBrowser.Dlna.Profiles
} }
}; };
ExternalSubtitleProfiles = new[] SubtitleProfiles = new[]
{ {
new SubtitleProfile new SubtitleProfile
{ {
Format = "ttml" Format = "ttml",
Method = SubtitleDeliveryMethod.External
} }
}; };
} }

View File

@ -65,6 +65,4 @@
</CodecProfile> </CodecProfile>
</CodecProfiles> </CodecProfiles>
<ResponseProfiles /> <ResponseProfiles />
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -35,6 +35,4 @@
<ContainerProfiles /> <ContainerProfiles />
<CodecProfiles /> <CodecProfiles />
<ResponseProfiles /> <ResponseProfiles />
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -39,6 +39,4 @@
<ContainerProfiles /> <ContainerProfiles />
<CodecProfiles /> <CodecProfiles />
<ResponseProfiles /> <ResponseProfiles />
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -73,6 +73,4 @@
</CodecProfile> </CodecProfile>
</CodecProfiles> </CodecProfiles>
<ResponseProfiles /> <ResponseProfiles />
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -39,6 +39,4 @@
<ContainerProfiles /> <ContainerProfiles />
<CodecProfiles /> <CodecProfiles />
<ResponseProfiles /> <ResponseProfiles />
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -45,6 +45,4 @@
<ContainerProfiles /> <ContainerProfiles />
<CodecProfiles /> <CodecProfiles />
<ResponseProfiles /> <ResponseProfiles />
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -68,8 +68,7 @@
</CodecProfile> </CodecProfile>
</CodecProfiles> </CodecProfiles>
<ResponseProfiles /> <ResponseProfiles />
<SoftSubtitleProfiles> <SubtitleProfiles>
<SubtitleProfile format="srt" /> <SubtitleProfile format="srt" method="External" />
</SoftSubtitleProfiles> </SubtitleProfiles>
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -106,8 +106,7 @@
<Conditions /> <Conditions />
</ResponseProfile> </ResponseProfile>
</ResponseProfiles> </ResponseProfiles>
<SoftSubtitleProfiles> <SubtitleProfiles>
<SubtitleProfile format="smi" /> <SubtitleProfile format="smi" method="External" />
</SoftSubtitleProfiles> </SubtitleProfiles>
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -71,6 +71,4 @@
</CodecProfile> </CodecProfile>
</CodecProfiles> </CodecProfiles>
<ResponseProfiles /> <ResponseProfiles />
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -99,6 +99,4 @@
<Conditions /> <Conditions />
</ResponseProfile> </ResponseProfile>
</ResponseProfiles> </ResponseProfiles>
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -107,6 +107,4 @@
<Conditions /> <Conditions />
</ResponseProfile> </ResponseProfile>
</ResponseProfiles> </ResponseProfiles>
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -110,6 +110,4 @@
<Conditions /> <Conditions />
</ResponseProfile> </ResponseProfile>
</ResponseProfiles> </ResponseProfiles>
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -93,6 +93,4 @@
<Conditions /> <Conditions />
</ResponseProfile> </ResponseProfile>
</ResponseProfiles> </ResponseProfiles>
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -93,6 +93,4 @@
<Conditions /> <Conditions />
</ResponseProfile> </ResponseProfile>
</ResponseProfiles> </ResponseProfiles>
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -93,6 +93,4 @@
<Conditions /> <Conditions />
</ResponseProfile> </ResponseProfile>
</ResponseProfiles> </ResponseProfiles>
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -78,6 +78,4 @@
<Conditions /> <Conditions />
</ResponseProfile> </ResponseProfile>
</ResponseProfiles> </ResponseProfiles>
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -62,8 +62,7 @@
</CodecProfile> </CodecProfile>
</CodecProfiles> </CodecProfiles>
<ResponseProfiles /> <ResponseProfiles />
<SoftSubtitleProfiles /> <SubtitleProfiles>
<ExternalSubtitleProfiles> <SubtitleProfile format="ttml" method="External" />
<SubtitleProfile format="ttml" /> </SubtitleProfiles>
</ExternalSubtitleProfiles>
</Profile> </Profile>

View File

@ -76,6 +76,4 @@
</CodecProfile> </CodecProfile>
</CodecProfiles> </CodecProfiles>
<ResponseProfiles /> <ResponseProfiles />
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -100,6 +100,4 @@
<Conditions /> <Conditions />
</ResponseProfile> </ResponseProfile>
</ResponseProfiles> </ResponseProfiles>
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -90,6 +90,4 @@
<Conditions /> <Conditions />
</ResponseProfile> </ResponseProfile>
</ResponseProfiles> </ResponseProfiles>
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -45,6 +45,4 @@
<ContainerProfiles /> <ContainerProfiles />
<CodecProfiles /> <CodecProfiles />
<ResponseProfiles /> <ResponseProfiles />
<SoftSubtitleProfiles />
<ExternalSubtitleProfiles />
</Profile> </Profile>

View File

@ -90,8 +90,7 @@ namespace MediaBrowser.Model.Dlna
public CodecProfile[] CodecProfiles { get; set; } public CodecProfile[] CodecProfiles { get; set; }
public ResponseProfile[] ResponseProfiles { get; set; } public ResponseProfile[] ResponseProfiles { get; set; }
public SubtitleProfile[] SoftSubtitleProfiles { get; set; } public SubtitleProfile[] SubtitleProfiles { get; set; }
public SubtitleProfile[] ExternalSubtitleProfiles { get; set; }
public DeviceProfile() public DeviceProfile()
{ {
@ -100,9 +99,6 @@ namespace MediaBrowser.Model.Dlna
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = new ResponseProfile[] { };
CodecProfiles = new CodecProfile[] { }; CodecProfiles = new CodecProfile[] { };
ContainerProfiles = new ContainerProfile[] { }; ContainerProfiles = new ContainerProfile[] { };
SoftSubtitleProfiles = new SubtitleProfile[] { };
ExternalSubtitleProfiles = new SubtitleProfile[] { };
XmlRootAttributes = new XmlAttribute[] { }; XmlRootAttributes = new XmlAttribute[] { };

View File

@ -518,7 +518,7 @@ namespace MediaBrowser.Model.Dlna
{ {
// See if the device can retrieve the subtitles externally // See if the device can retrieve the subtitles externally
bool supportsSubsExternally = options.Context == EncodingContext.Streaming && bool supportsSubsExternally = options.Context == EncodingContext.Streaming &&
ContainsSubtitleFormat(options.Profile.ExternalSubtitleProfiles, _serverTextSubtitleOutputs); ContainsSubtitleFormat(options.Profile.SubtitleProfiles, SubtitleDeliveryMethod.External, _serverTextSubtitleOutputs);
if (supportsSubsExternally) if (supportsSubsExternally)
{ {
@ -526,7 +526,7 @@ namespace MediaBrowser.Model.Dlna
} }
// See if the device can retrieve the subtitles externally // See if the device can retrieve the subtitles externally
bool supportsEmbedded = ContainsSubtitleFormat(options.Profile.SoftSubtitleProfiles, _serverTextSubtitleOutputs); bool supportsEmbedded = ContainsSubtitleFormat(options.Profile.SubtitleProfiles, SubtitleDeliveryMethod.Embed, _serverTextSubtitleOutputs);
if (supportsEmbedded) if (supportsEmbedded)
{ {
@ -547,11 +547,11 @@ namespace MediaBrowser.Model.Dlna
return codec; return codec;
} }
private bool ContainsSubtitleFormat(SubtitleProfile[] profiles, string[] formats) private bool ContainsSubtitleFormat(SubtitleProfile[] profiles, SubtitleDeliveryMethod method, string[] formats)
{ {
foreach (SubtitleProfile profile in profiles) foreach (SubtitleProfile profile in profiles)
{ {
if (ListHelper.ContainsIgnoreCase(formats, profile.Format)) if (method == profile.Method && ListHelper.ContainsIgnoreCase(formats, profile.Format))
{ {
return true; return true;
} }

View File

@ -476,6 +476,10 @@ namespace MediaBrowser.Model.Dlna
/// <summary> /// <summary>
/// The external /// The external
/// </summary> /// </summary>
External = 2 External = 2,
/// <summary>
/// The HLS
/// </summary>
Hls = 3
} }
} }

View File

@ -9,5 +9,8 @@ namespace MediaBrowser.Model.Dlna
[XmlAttribute("protocol")] [XmlAttribute("protocol")]
public string Protocol { get; set; } public string Protocol { get; set; }
[XmlAttribute("method")]
public SubtitleDeliveryMethod Method { get; set; }
} }
} }

View File

@ -550,24 +550,28 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}; };
} }
if (!string.IsNullOrEmpty(info.Path))
{
item.Path = info.Path;
}
else if (!string.IsNullOrEmpty(info.Url))
{
item.Path = info.Url;
}
isNew = true; isNew = true;
} }
item.RecordingInfo = info; item.RecordingInfo = info;
item.ServiceName = serviceName; item.ServiceName = serviceName;
var originalPath = item.Path;
if (!string.IsNullOrEmpty(info.Path))
{
item.Path = info.Path;
}
else if (!string.IsNullOrEmpty(info.Url))
{
item.Path = info.Url;
}
var pathChanged = !string.Equals(originalPath, item.Path);
await item.RefreshMetadata(new MetadataRefreshOptions await item.RefreshMetadata(new MetadataRefreshOptions
{ {
ForceSave = isNew ForceSave = isNew || pathChanged
}, cancellationToken); }, cancellationToken);