|
|
|
@ -6,6 +6,7 @@ using ErsatzTV.Core.Images; |
|
|
|
using ErsatzTV.Core.Interfaces.FFmpeg; |
|
|
|
using ErsatzTV.Core.Interfaces.FFmpeg; |
|
|
|
using ErsatzTV.Core.Interfaces.Images; |
|
|
|
using ErsatzTV.Core.Interfaces.Images; |
|
|
|
using Microsoft.Extensions.Logging; |
|
|
|
using Microsoft.Extensions.Logging; |
|
|
|
|
|
|
|
using Microsoft.Extensions.Logging.Abstractions; |
|
|
|
|
|
|
|
|
|
|
|
namespace ErsatzTV.Core.FFmpeg; |
|
|
|
namespace ErsatzTV.Core.FFmpeg; |
|
|
|
|
|
|
|
|
|
|
|
@ -20,9 +21,12 @@ public class WatermarkSelector( |
|
|
|
Option<ChannelWatermark> globalWatermark, |
|
|
|
Option<ChannelWatermark> globalWatermark, |
|
|
|
Channel channel, |
|
|
|
Channel channel, |
|
|
|
PlayoutItem playoutItem, |
|
|
|
PlayoutItem playoutItem, |
|
|
|
DateTimeOffset now) |
|
|
|
DateTimeOffset now, |
|
|
|
|
|
|
|
bool shouldLogMessages) |
|
|
|
{ |
|
|
|
{ |
|
|
|
logger.LogDebug("Checking for watermark at {Now}", now); |
|
|
|
ILogger<WatermarkSelector> log = shouldLogMessages ? logger : NullLogger<WatermarkSelector>.Instance; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log.LogDebug("Checking for watermark at {Now}", now); |
|
|
|
|
|
|
|
|
|
|
|
var result = new List<WatermarkOptions>(); |
|
|
|
var result = new List<WatermarkOptions>(); |
|
|
|
|
|
|
|
|
|
|
|
@ -33,7 +37,7 @@ public class WatermarkSelector( |
|
|
|
|
|
|
|
|
|
|
|
if (playoutItem.DisableWatermarks) |
|
|
|
if (playoutItem.DisableWatermarks) |
|
|
|
{ |
|
|
|
{ |
|
|
|
logger.LogDebug("Watermark is disabled by playout item"); |
|
|
|
log.LogDebug("Watermark is disabled by playout item"); |
|
|
|
return result; |
|
|
|
return result; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -49,36 +53,36 @@ public class WatermarkSelector( |
|
|
|
case DecoMode.Merge: |
|
|
|
case DecoMode.Merge: |
|
|
|
if (playoutItem.FillerKind is FillerKind.None || templateDeco.UseWatermarkDuringFiller) |
|
|
|
if (playoutItem.FillerKind is FillerKind.None || templateDeco.UseWatermarkDuringFiller) |
|
|
|
{ |
|
|
|
{ |
|
|
|
logger.LogDebug("Watermark will come from template deco (merge)"); |
|
|
|
log.LogDebug("Watermark will come from template deco (merge)"); |
|
|
|
result.AddRange( |
|
|
|
result.AddRange( |
|
|
|
OptionsForWatermarks(channel, templateDeco.DecoWatermarks.Map(dwm => dwm.Watermark))); |
|
|
|
OptionsForWatermarks(channel, templateDeco.DecoWatermarks.Map(dwm => dwm.Watermark), log)); |
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogDebug("Watermark is disabled by template deco during filler"); |
|
|
|
log.LogDebug("Watermark is disabled by template deco during filler"); |
|
|
|
result.Clear(); |
|
|
|
result.Clear(); |
|
|
|
done = true; |
|
|
|
done = true; |
|
|
|
break; |
|
|
|
break; |
|
|
|
case DecoMode.Override: |
|
|
|
case DecoMode.Override: |
|
|
|
if (playoutItem.FillerKind is FillerKind.None || templateDeco.UseWatermarkDuringFiller) |
|
|
|
if (playoutItem.FillerKind is FillerKind.None || templateDeco.UseWatermarkDuringFiller) |
|
|
|
{ |
|
|
|
{ |
|
|
|
logger.LogDebug("Watermark will come from template deco (replace)"); |
|
|
|
log.LogDebug("Watermark will come from template deco (replace)"); |
|
|
|
result.AddRange( |
|
|
|
result.AddRange( |
|
|
|
OptionsForWatermarks(channel, templateDeco.DecoWatermarks.Map(dwm => dwm.Watermark))); |
|
|
|
OptionsForWatermarks(channel, templateDeco.DecoWatermarks.Map(dwm => dwm.Watermark), log)); |
|
|
|
done = true; |
|
|
|
done = true; |
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogDebug("Watermark is disabled by template deco during filler"); |
|
|
|
log.LogDebug("Watermark is disabled by template deco during filler"); |
|
|
|
result.Clear(); |
|
|
|
result.Clear(); |
|
|
|
done = true; |
|
|
|
done = true; |
|
|
|
break; |
|
|
|
break; |
|
|
|
case DecoMode.Disable: |
|
|
|
case DecoMode.Disable: |
|
|
|
logger.LogDebug("Watermark is disabled by template deco"); |
|
|
|
log.LogDebug("Watermark is disabled by template deco"); |
|
|
|
done = true; |
|
|
|
done = true; |
|
|
|
break; |
|
|
|
break; |
|
|
|
case DecoMode.Inherit: |
|
|
|
case DecoMode.Inherit: |
|
|
|
logger.LogDebug("Watermark will inherit from playout deco"); |
|
|
|
log.LogDebug("Watermark will inherit from playout deco"); |
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -98,36 +102,36 @@ public class WatermarkSelector( |
|
|
|
case DecoMode.Merge: |
|
|
|
case DecoMode.Merge: |
|
|
|
if (playoutItem.FillerKind is FillerKind.None || playoutDeco.UseWatermarkDuringFiller) |
|
|
|
if (playoutItem.FillerKind is FillerKind.None || playoutDeco.UseWatermarkDuringFiller) |
|
|
|
{ |
|
|
|
{ |
|
|
|
logger.LogDebug("Watermark will come from playout deco (merge)"); |
|
|
|
log.LogDebug("Watermark will come from playout deco (merge)"); |
|
|
|
result.AddRange( |
|
|
|
result.AddRange( |
|
|
|
OptionsForWatermarks(channel, playoutDeco.DecoWatermarks.Map(dwm => dwm.Watermark))); |
|
|
|
OptionsForWatermarks(channel, playoutDeco.DecoWatermarks.Map(dwm => dwm.Watermark), log)); |
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogDebug("Watermark is disabled by playout deco during filler"); |
|
|
|
log.LogDebug("Watermark is disabled by playout deco during filler"); |
|
|
|
result.Clear(); |
|
|
|
result.Clear(); |
|
|
|
done = true; |
|
|
|
done = true; |
|
|
|
break; |
|
|
|
break; |
|
|
|
case DecoMode.Override: |
|
|
|
case DecoMode.Override: |
|
|
|
if (playoutItem.FillerKind is FillerKind.None || playoutDeco.UseWatermarkDuringFiller) |
|
|
|
if (playoutItem.FillerKind is FillerKind.None || playoutDeco.UseWatermarkDuringFiller) |
|
|
|
{ |
|
|
|
{ |
|
|
|
logger.LogDebug("Watermark will come from playout deco (replace)"); |
|
|
|
log.LogDebug("Watermark will come from playout deco (replace)"); |
|
|
|
result.AddRange( |
|
|
|
result.AddRange( |
|
|
|
OptionsForWatermarks(channel, playoutDeco.DecoWatermarks.Map(dwm => dwm.Watermark))); |
|
|
|
OptionsForWatermarks(channel, playoutDeco.DecoWatermarks.Map(dwm => dwm.Watermark), log)); |
|
|
|
done = true; |
|
|
|
done = true; |
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogDebug("Watermark is disabled by playout deco during filler"); |
|
|
|
log.LogDebug("Watermark is disabled by playout deco during filler"); |
|
|
|
result.Clear(); |
|
|
|
result.Clear(); |
|
|
|
done = true; |
|
|
|
done = true; |
|
|
|
break; |
|
|
|
break; |
|
|
|
case DecoMode.Disable: |
|
|
|
case DecoMode.Disable: |
|
|
|
logger.LogDebug("Watermark is disabled by playout deco"); |
|
|
|
log.LogDebug("Watermark is disabled by playout deco"); |
|
|
|
done = true; |
|
|
|
done = true; |
|
|
|
break; |
|
|
|
break; |
|
|
|
case DecoMode.Inherit: |
|
|
|
case DecoMode.Inherit: |
|
|
|
logger.LogDebug("Watermark will inherit from channel and/or global setting"); |
|
|
|
log.LogDebug("Watermark will inherit from channel and/or global setting"); |
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -144,7 +148,8 @@ public class WatermarkSelector( |
|
|
|
Option<WatermarkOptions> options = GetWatermarkOptions( |
|
|
|
Option<WatermarkOptions> options = GetWatermarkOptions( |
|
|
|
channel, |
|
|
|
channel, |
|
|
|
watermark, |
|
|
|
watermark, |
|
|
|
Option<ChannelWatermark>.None); |
|
|
|
Option<ChannelWatermark>.None, |
|
|
|
|
|
|
|
shouldLogMessages); |
|
|
|
result.AddRange(options); |
|
|
|
result.AddRange(options); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -152,7 +157,11 @@ public class WatermarkSelector( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var finalOptions = GetWatermarkOptions(channel, Option<ChannelWatermark>.None, globalWatermark); |
|
|
|
Option<WatermarkOptions> finalOptions = GetWatermarkOptions( |
|
|
|
|
|
|
|
channel, |
|
|
|
|
|
|
|
Option<ChannelWatermark>.None, |
|
|
|
|
|
|
|
globalWatermark, |
|
|
|
|
|
|
|
shouldLogMessages); |
|
|
|
result.AddRange(finalOptions); |
|
|
|
result.AddRange(finalOptions); |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
return result; |
|
|
|
@ -161,8 +170,11 @@ public class WatermarkSelector( |
|
|
|
public Option<WatermarkOptions> GetWatermarkOptions( |
|
|
|
public Option<WatermarkOptions> GetWatermarkOptions( |
|
|
|
Channel channel, |
|
|
|
Channel channel, |
|
|
|
Option<ChannelWatermark> playoutItemWatermark, |
|
|
|
Option<ChannelWatermark> playoutItemWatermark, |
|
|
|
Option<ChannelWatermark> globalWatermark) |
|
|
|
Option<ChannelWatermark> globalWatermark, |
|
|
|
|
|
|
|
bool shouldLogMessages) |
|
|
|
{ |
|
|
|
{ |
|
|
|
|
|
|
|
ILogger<WatermarkSelector> log = shouldLogMessages ? logger : NullLogger<WatermarkSelector>.Instance; |
|
|
|
|
|
|
|
|
|
|
|
if (channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect) |
|
|
|
if (channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return Option<WatermarkOptions>.None; |
|
|
|
return Option<WatermarkOptions>.None; |
|
|
|
@ -183,7 +195,7 @@ public class WatermarkSelector( |
|
|
|
return new WatermarkOptions(watermark, resourcePath, Option<int>.None); |
|
|
|
return new WatermarkOptions(watermark, resourcePath, Option<int>.None); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogWarning( |
|
|
|
log.LogWarning( |
|
|
|
"Watermark resource no longer exists at {Path} and will be ignored", |
|
|
|
"Watermark resource no longer exists at {Path} and will be ignored", |
|
|
|
resourcePath); |
|
|
|
resourcePath); |
|
|
|
return None; |
|
|
|
return None; |
|
|
|
@ -191,13 +203,13 @@ public class WatermarkSelector( |
|
|
|
// bad form validation makes this possible
|
|
|
|
// bad form validation makes this possible
|
|
|
|
if (string.IsNullOrWhiteSpace(watermark.Image)) |
|
|
|
if (string.IsNullOrWhiteSpace(watermark.Image)) |
|
|
|
{ |
|
|
|
{ |
|
|
|
logger.LogWarning( |
|
|
|
log.LogWarning( |
|
|
|
"Watermark {Name} has custom image configured with no image; ignoring", |
|
|
|
"Watermark {Name} has custom image configured with no image; ignoring", |
|
|
|
watermark.Name); |
|
|
|
watermark.Name); |
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogDebug("Watermark will come from playout item (custom)"); |
|
|
|
log.LogDebug("Watermark will come from playout item (custom)"); |
|
|
|
|
|
|
|
|
|
|
|
string customPath = imageCache.GetPathForImage( |
|
|
|
string customPath = imageCache.GetPathForImage( |
|
|
|
watermark.Image, |
|
|
|
watermark.Image, |
|
|
|
@ -209,12 +221,12 @@ public class WatermarkSelector( |
|
|
|
return new WatermarkOptions(watermark, customPath, None); |
|
|
|
return new WatermarkOptions(watermark, customPath, None); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogWarning( |
|
|
|
log.LogWarning( |
|
|
|
"Custom watermark no longer exists at {Path} and will be ignored", |
|
|
|
"Custom watermark no longer exists at {Path} and will be ignored", |
|
|
|
customPath); |
|
|
|
customPath); |
|
|
|
return None; |
|
|
|
return None; |
|
|
|
case ChannelWatermarkImageSource.ChannelLogo: |
|
|
|
case ChannelWatermarkImageSource.ChannelLogo: |
|
|
|
logger.LogDebug("Watermark will come from playout item (channel logo)"); |
|
|
|
log.LogDebug("Watermark will come from playout item (channel logo)"); |
|
|
|
|
|
|
|
|
|
|
|
string channelPath = ChannelLogoGenerator.GenerateChannelLogoUrl(channel); |
|
|
|
string channelPath = ChannelLogoGenerator.GenerateChannelLogoUrl(channel); |
|
|
|
Option<Artwork> maybeLogoArtwork = |
|
|
|
Option<Artwork> maybeLogoArtwork = |
|
|
|
@ -231,7 +243,7 @@ public class WatermarkSelector( |
|
|
|
return new WatermarkOptions(watermark, channelPath, None); |
|
|
|
return new WatermarkOptions(watermark, channelPath, None); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogWarning( |
|
|
|
log.LogWarning( |
|
|
|
"Channel logo no longer exists at {Path} and will be ignored", |
|
|
|
"Channel logo no longer exists at {Path} and will be ignored", |
|
|
|
channelPath); |
|
|
|
channelPath); |
|
|
|
return None; |
|
|
|
return None; |
|
|
|
@ -246,7 +258,7 @@ public class WatermarkSelector( |
|
|
|
switch (channel.Watermark.ImageSource) |
|
|
|
switch (channel.Watermark.ImageSource) |
|
|
|
{ |
|
|
|
{ |
|
|
|
case ChannelWatermarkImageSource.Custom: |
|
|
|
case ChannelWatermarkImageSource.Custom: |
|
|
|
logger.LogDebug("Watermark will come from channel (custom)"); |
|
|
|
log.LogDebug("Watermark will come from channel (custom)"); |
|
|
|
|
|
|
|
|
|
|
|
string customPath = imageCache.GetPathForImage( |
|
|
|
string customPath = imageCache.GetPathForImage( |
|
|
|
channel.Watermark.Image, |
|
|
|
channel.Watermark.Image, |
|
|
|
@ -258,12 +270,12 @@ public class WatermarkSelector( |
|
|
|
return new WatermarkOptions(channel.Watermark, customPath, None); |
|
|
|
return new WatermarkOptions(channel.Watermark, customPath, None); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogWarning( |
|
|
|
log.LogWarning( |
|
|
|
"Custom watermark no longer exists at {Path} and will be ignored", |
|
|
|
"Custom watermark no longer exists at {Path} and will be ignored", |
|
|
|
customPath); |
|
|
|
customPath); |
|
|
|
return None; |
|
|
|
return None; |
|
|
|
case ChannelWatermarkImageSource.ChannelLogo: |
|
|
|
case ChannelWatermarkImageSource.ChannelLogo: |
|
|
|
logger.LogDebug("Watermark will come from channel (channel logo)"); |
|
|
|
log.LogDebug("Watermark will come from channel (channel logo)"); |
|
|
|
|
|
|
|
|
|
|
|
string channelPath = ChannelLogoGenerator.GenerateChannelLogoUrl(channel); |
|
|
|
string channelPath = ChannelLogoGenerator.GenerateChannelLogoUrl(channel); |
|
|
|
Option<Artwork> maybeLogoArtwork = |
|
|
|
Option<Artwork> maybeLogoArtwork = |
|
|
|
@ -280,7 +292,7 @@ public class WatermarkSelector( |
|
|
|
return new WatermarkOptions(channel.Watermark, channelPath, None); |
|
|
|
return new WatermarkOptions(channel.Watermark, channelPath, None); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogWarning( |
|
|
|
log.LogWarning( |
|
|
|
"Channel logo no longer exists at {Path} and will be ignored", |
|
|
|
"Channel logo no longer exists at {Path} and will be ignored", |
|
|
|
channelPath); |
|
|
|
channelPath); |
|
|
|
return None; |
|
|
|
return None; |
|
|
|
@ -295,7 +307,7 @@ public class WatermarkSelector( |
|
|
|
switch (watermark.ImageSource) |
|
|
|
switch (watermark.ImageSource) |
|
|
|
{ |
|
|
|
{ |
|
|
|
case ChannelWatermarkImageSource.Custom: |
|
|
|
case ChannelWatermarkImageSource.Custom: |
|
|
|
logger.LogDebug("Watermark will come from global (custom)"); |
|
|
|
log.LogDebug("Watermark will come from global (custom)"); |
|
|
|
|
|
|
|
|
|
|
|
string customPath = imageCache.GetPathForImage( |
|
|
|
string customPath = imageCache.GetPathForImage( |
|
|
|
watermark.Image, |
|
|
|
watermark.Image, |
|
|
|
@ -307,12 +319,12 @@ public class WatermarkSelector( |
|
|
|
return new WatermarkOptions(watermark, customPath, None); |
|
|
|
return new WatermarkOptions(watermark, customPath, None); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogWarning( |
|
|
|
log.LogWarning( |
|
|
|
"Custom watermark no longer exists at {Path} and will be ignored", |
|
|
|
"Custom watermark no longer exists at {Path} and will be ignored", |
|
|
|
customPath); |
|
|
|
customPath); |
|
|
|
return None; |
|
|
|
return None; |
|
|
|
case ChannelWatermarkImageSource.ChannelLogo: |
|
|
|
case ChannelWatermarkImageSource.ChannelLogo: |
|
|
|
logger.LogDebug("Watermark will come from global (channel logo)"); |
|
|
|
log.LogDebug("Watermark will come from global (channel logo)"); |
|
|
|
|
|
|
|
|
|
|
|
string channelPath = ChannelLogoGenerator.GenerateChannelLogoUrl(channel); |
|
|
|
string channelPath = ChannelLogoGenerator.GenerateChannelLogoUrl(channel); |
|
|
|
Option<Artwork> maybeLogoArtwork = |
|
|
|
Option<Artwork> maybeLogoArtwork = |
|
|
|
@ -329,7 +341,7 @@ public class WatermarkSelector( |
|
|
|
return new WatermarkOptions(watermark, channelPath, None); |
|
|
|
return new WatermarkOptions(watermark, channelPath, None); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.LogWarning( |
|
|
|
log.LogWarning( |
|
|
|
"Channel logo no longer exists at {Path} and will be ignored", |
|
|
|
"Channel logo no longer exists at {Path} and will be ignored", |
|
|
|
channelPath); |
|
|
|
channelPath); |
|
|
|
return None; |
|
|
|
return None; |
|
|
|
@ -341,19 +353,25 @@ public class WatermarkSelector( |
|
|
|
return Option<WatermarkOptions>.None; |
|
|
|
return Option<WatermarkOptions>.None; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private List<WatermarkOptions> OptionsForWatermarks(Channel channel, IEnumerable<ChannelWatermark> watermarks) |
|
|
|
private List<WatermarkOptions> OptionsForWatermarks( |
|
|
|
|
|
|
|
Channel channel, |
|
|
|
|
|
|
|
IEnumerable<ChannelWatermark> watermarks, |
|
|
|
|
|
|
|
ILogger<WatermarkSelector> log) |
|
|
|
{ |
|
|
|
{ |
|
|
|
var result = new List<WatermarkOptions>(); |
|
|
|
var result = new List<WatermarkOptions>(); |
|
|
|
|
|
|
|
|
|
|
|
foreach (var watermark in watermarks) |
|
|
|
foreach (var watermark in watermarks) |
|
|
|
{ |
|
|
|
{ |
|
|
|
result.AddRange(GetWatermarkOptions(channel, watermark)); |
|
|
|
result.AddRange(GetWatermarkOptions(channel, watermark, log)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
return result; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private Option<WatermarkOptions> GetWatermarkOptions(Channel channel, ChannelWatermark watermark) |
|
|
|
private Option<WatermarkOptions> GetWatermarkOptions( |
|
|
|
|
|
|
|
Channel channel, |
|
|
|
|
|
|
|
ChannelWatermark watermark, |
|
|
|
|
|
|
|
ILogger<WatermarkSelector> log) |
|
|
|
{ |
|
|
|
{ |
|
|
|
switch (watermark.ImageSource) |
|
|
|
switch (watermark.ImageSource) |
|
|
|
{ |
|
|
|
{ |
|
|
|
@ -367,7 +385,7 @@ public class WatermarkSelector( |
|
|
|
// bad form validation makes this possible
|
|
|
|
// bad form validation makes this possible
|
|
|
|
if (string.IsNullOrWhiteSpace(watermark.Image)) |
|
|
|
if (string.IsNullOrWhiteSpace(watermark.Image)) |
|
|
|
{ |
|
|
|
{ |
|
|
|
logger.LogWarning( |
|
|
|
log.LogWarning( |
|
|
|
"Watermark {Name} has custom image configured with no image; ignoring", |
|
|
|
"Watermark {Name} has custom image configured with no image; ignoring", |
|
|
|
watermark.Name); |
|
|
|
watermark.Name); |
|
|
|
break; |
|
|
|
break; |
|
|
|
|