Get(string newsid, CancellationToken cancellationToken)
{
try
{
- if (newsid != null && long.TryParse(newsid, out long n))
+ if (!_mediaStreamService.IsValidNewsId(newsid))
{
- MediaStream streamline;
- if (cache.Contains("MediaStream-" + newsid))
- streamline = (MediaStream)cache.Get("MediaStream-" + newsid);
- else
- {
- streamline = new MediaStream(newsid);
- cache.Set("MediaStream-" + newsid, streamline, DateTime.Now.AddSeconds(30.0));
- }
- if (streamline.FileSize == 0)
- throw new HttpResponseException(HttpStatusCode.NotFound);
-
- RangeHeaderValue rangeHeader = Request.Headers.Range;
- HttpResponseMessage response = new HttpResponseMessage();
-
- response.Headers.AcceptRanges.Add("bytes");
-
- // The request will be treated as normal request if there is no Range header.
- if (rangeHeader == null || !rangeHeader.Ranges.Any())
- {
- response.StatusCode = HttpStatusCode.OK;
- response.Content = new PushStreamContent(async (outputStream, httpContent, transpContext)
- =>
- {
- using (outputStream)
- await streamline.CreatePartialContent(outputStream, 0, streamline.FileSize, newsid);
- }
- , GetMimeNameFromExt(streamline.FileExt));
-
- response.Content.Headers.ContentLength = streamline.FileSize;
- response.Content.Headers.ContentType = GetMimeNameFromExt(streamline.FileExt);
-
- return response;
- }
- long start = 0, end = 0;
- // 1. If the unit is not 'bytes'.
- // 2. If there are multiple ranges in header value.
- // 3. If start or end position is greater than file length.
- if (rangeHeader.Unit != "bytes" || rangeHeader.Ranges.Count > 1 ||
- !TryReadRangeItem(rangeHeader.Ranges.First(), streamline.FileSize, out start, out end))
- {
- response.StatusCode = HttpStatusCode.RequestedRangeNotSatisfiable;
- response.Content = new StreamContent(Stream.Null); // No content for this status.
- response.Content.Headers.ContentRange = new ContentRangeHeaderValue(streamline.FileSize);
- response.Content.Headers.ContentType = GetMimeNameFromExt(streamline.FileExt);
-
- return response;
- }
-
- var contentRange = new ContentRangeHeaderValue(start, end, streamline.FileSize);
-
- // We are now ready to produce partial content.
- response.StatusCode = HttpStatusCode.PartialContent;
-
- response.Content = new PushStreamContent(async (outputStream, httpContent, transpContext)
- =>
- {
- using (outputStream) // Copy the file to output stream in indicated range.
- await streamline.CreatePartialContent(outputStream, start, end, newsid);
-
- }, GetMimeNameFromExt(streamline.FileExt));
- response.Content.Headers.ContentRange = contentRange;
+ return NotFound();
+ }
- return response;
+ var mediaInfo = await _mediaStreamService.GetMediaStreamInfoAsync(newsid);
+ if (mediaInfo == null || mediaInfo.FileSize == 0)
+ {
+ return NotFound();
+ }
+ var rangeHeader = Request.GetTypedHeaders().Range;
+
+ // Set accept ranges header
+ Response.Headers.AcceptRanges = "bytes";
+ // The request will be treated as normal request if there is no Range header.
+ if (rangeHeader == null || !rangeHeader.Ranges.Any())
+ {
+ var fullContentStream = await _mediaStreamService.CreatePartialContentAsync(newsid, 0, mediaInfo.FileSize - 1);
+ return File(fullContentStream, GetMimeNameFromExt(mediaInfo.FileExt), enableRangeProcessing: true);
}
- else
+
+ long start = 0, end = 0;
+ // 1. If the unit is not 'bytes'.
+ // 2. If there are multiple ranges in header value.
+ // 3. If start or end position is greater than file length.
+ if (rangeHeader.Unit != "bytes" || rangeHeader.Ranges.Count > 1 ||
+ !TryReadRangeItem(rangeHeader.Ranges.First(), mediaInfo.FileSize, out start, out end))
{
- throw new HttpResponseException(HttpStatusCode.NotFound);
+ Response.Headers.ContentRange = $"bytes */{mediaInfo.FileSize}";
+ return StatusCode(416); // Range Not Satisfiable
}
+
+ // Create partial content stream
+ var partialContentStream = await _mediaStreamService.CreatePartialContentAsync(newsid, start, end);
+
+ // Set content range header
+ Response.Headers.ContentRange = $"bytes {start}-{end}/{mediaInfo.FileSize}";
+
+ // Return 206 Partial Content
+ Response.StatusCode = 206;
+ return new FileStreamResult(partialContentStream, GetMimeNameFromExt(mediaInfo.FileExt))
+ {
+ EnableRangeProcessing = false
+ };
}
catch (Exception ex)
{
-
- throw ex;
+ _logger.LogError(ex, "Error streaming media for newsId {NewsId}", newsid);
+ return StatusCode(500, "Internal server error");
}
- }
- #endregion
+ }
- #region Others
- private static MediaTypeHeaderValue GetMimeNameFromExt(string ext)
+ [HttpOptions("{newsid}")]
+ public IActionResult Options(string newsid)
{
- string value;
+ Response.Headers["Allow"] = "GET, HEAD, OPTIONS";
+ Response.Headers["Access-Control-Allow-Origin"] = "*";
+ Response.Headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS";
+ Response.Headers["Access-Control-Allow-Headers"] = "Range, Content-Type";
+ return Ok();
+ }
- if (MimeNames.TryGetValue(ext.ToLowerInvariant(), out value))
- return new MediaTypeHeaderValue(value);
+ private static string GetMimeNameFromExt(string ext)
+ {
+ if (MimeNames.TryGetValue(ext.ToLowerInvariant(), out string? value))
+ return value;
else
- return new MediaTypeHeaderValue(MediaTypeNames.Application.Octet);
+ return MediaTypeNames.Application.Octet;
}
- private static bool TryReadRangeItem(RangeItemHeaderValue range, long contentLength,
+ private static bool TryReadRangeItem(Microsoft.Net.Http.Headers.RangeItemHeaderValue range, long contentLength,
out long start, out long end)
{
if (range.From != null)
@@ -160,8 +134,8 @@ private static bool TryReadRangeItem(RangeItemHeaderValue range, long contentLe
else
start = 0;
}
- return (start < contentLength && end < contentLength);
+
+ return (start >= 0 && start < contentLength && end >= start && end <= contentLength - 1);
}
- #endregion
}
}
diff --git a/src/Default.aspx b/src/Default.aspx
deleted file mode 100644
index 9a6b301..0000000
--- a/src/Default.aspx
+++ /dev/null
@@ -1,16 +0,0 @@
-<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="md.akharinkhabar.ir.Default" %>
-
-
-
-
-
- md.akharinkhabar.ir
-
-
-
-
-
diff --git a/src/Default.aspx.cs b/src/Default.aspx.cs
deleted file mode 100644
index 558be66..0000000
--- a/src/Default.aspx.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Web;
-using System.Web.UI;
-using System.Web.UI.WebControls;
-
-namespace md.akharinkhabar.ir
-{
- public partial class Default : System.Web.UI.Page
- {
- protected void Page_Load(object sender, EventArgs e)
- {
-
- }
- }
-}
\ No newline at end of file
diff --git a/src/Default.aspx.designer.cs b/src/Default.aspx.designer.cs
deleted file mode 100644
index 7068c92..0000000
--- a/src/Default.aspx.designer.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-
-namespace md.akharinkhabar.ir
-{
-
-
- public partial class Default
- {
-
- ///
- /// form1 control.
- ///
- ///
- /// Auto-generated field.
- /// To modify move field declaration from designer file to code-behind file.
- ///
- protected global::System.Web.UI.HtmlControls.HtmlForm form1;
- }
-}
diff --git a/src/Global.asax b/src/Global.asax
deleted file mode 100644
index aa52a8a..0000000
--- a/src/Global.asax
+++ /dev/null
@@ -1 +0,0 @@
-<%@ Application Codebehind="Global.asax.cs" Inherits="md.akharinkhabar.ir.MvcApplication" Language="C#" %>
diff --git a/src/Global.asax.cs b/src/Global.asax.cs
deleted file mode 100644
index dc105e6..0000000
--- a/src/Global.asax.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Enyim.Caching;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Web;
-using System.Web.Http;
-using System.Web.Mvc;
-using System.Web.Routing;
-
-namespace md.akharinkhabar.ir
-{
- // Note: For instructions on enabling IIS6 or IIS7 classic mode,
- // visit http://go.microsoft.com/?LinkId=9394801
- public class MvcApplication : System.Web.HttpApplication
- {
-
- protected void Application_Start()
- {
- AreaRegistration.RegisterAllAreas();
-
- WebApiConfig.Register(GlobalConfiguration.Configuration);
- FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
- RouteConfig.RegisterRoutes(RouteTable.Routes);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Handler/StreamMessageHandler.cs b/src/Handler/StreamMessageHandler.cs
deleted file mode 100644
index 35325ac..0000000
--- a/src/Handler/StreamMessageHandler.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Web;
-
-namespace md.akharinkhabar.ir.Handler
-{
- public class StreamMessageHandler : DelegatingHandler
- {
- protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- try
- {
- var response = await base.SendAsync(request, cancellationToken);
- return response;
- }
- catch (Exception ex)
- {
- return request.CreateErrorResponse(System.Net.HttpStatusCode.InternalServerError, ex);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Models/MediaStream.cs b/src/Models/MediaStream.cs
deleted file mode 100644
index 4755a1a..0000000
--- a/src/Models/MediaStream.cs
+++ /dev/null
@@ -1,228 +0,0 @@
-using Enyim.Caching;
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Data;
-using System.Data.SqlClient;
-using System.Data.SqlTypes;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Runtime.Caching;
-using System.Threading.Tasks;
-using System.Transactions;
-using System.Web;
-using System.Web.Configuration;
-using System.Web.Mvc;
-
-namespace md.akharinkhabar.ir.Models
-{
- public class MediaStream : Controller
- {
- //
- // GET: /MediaStream/
-
- public ActionResult Index()
- {
- return View();
- }
-
- #region Var
- public string ID { get; set; }
- public long FileSize { get; set; }
- public string FileType { get; set; }
- public string FileExt { get; set; }
- public string ServerPath { get; set; }
- public byte[] ServerTxn { get; set; }
- private readonly string conStr;
- // Chunk file size in byte
- public const int ReadStreamBufferSize = 256 * 1024;
- private readonly bool inBuffer = false;
- readonly string bufferPath;
- #endregion
-
- #region Constructor
- public MediaStream(string _id)
- {
- ID = _id;
- conStr = WebConfigurationManager.ConnectionStrings["Media"].ConnectionString;
-
- //-------------Get Metadata of the file that previously save in the database--------------------------------------------------------
- SqlDataAdapter da = new SqlDataAdapter("select FileSize,FileType,FileExt from MediaStream where newsid = " + ID, conStr);
- DataTable dt = new DataTable();
- da.Fill(dt);
- if (dt.Rows.Count > 0)
- {
- FileSize = (long)dt.Rows[0]["FileSize"];
- FileType = dt.Rows[0]["FileType"].ToString();
- FileExt = dt.Rows[0]["FileExt"].ToString();
- }
-
- //-------------(It's optional) If found the media file in somewhere in the buffer it would set the flag = true--------------------------------------------------------
- bufferPath = "B:\\" + ID + FileExt;
- if (System.IO.File.Exists(bufferPath))
- {
- FileStream fis = null;
- try
- {
- fis = System.IO.File.Open(bufferPath, FileMode.Open, FileAccess.Read, FileShare.Read);
- inBuffer = true;
- }
- catch (Exception)
- {
- inBuffer = false;
- }
- try
- {
- if (fis != null)
- fis.Close();
- }
- catch (Exception)
- {
- }
-
- }
- else
- {
- bufferPath = "F:\\" + ID + FileExt;
- if (System.IO.File.Exists(bufferPath))
- {
- FileStream fis = null;
- try
- {
- fis = System.IO.File.Open(bufferPath, FileMode.Open, FileAccess.Read, FileShare.Read);
- inBuffer = true;
- }
- catch (Exception)
- {
- inBuffer = false;
- }
- try
- {
- if (fis != null)
- fis.Close();
- }
- catch (Exception)
- {
- }
-
- }
- }
- }
- #endregion
-
- ///
- /// Load Media From SQL Server FileStream in the form of chunked parts
- ///
- /// If exists in local buffer (ram disk || fast tempropy disk) it won't go to fetch data from SQL Server
- /// Start range byte of media to play
- /// End range byte of media to play
- /// Id of media to find
- ///
- public async Task CreatePartialContent(Stream outputStream, long start, long end, string id)
- {
- int count = 0;
- long remainingBytes = end - start + 1;
- long position = start;
- byte[] buffer = new byte[ReadStreamBufferSize];
-
- if (inBuffer) //---------------------------(It's optional) LOAD FROM BUFFER----------------------------------------------
- {
- try
- {
- using (FileStream sfs = new FileStream(bufferPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, ReadStreamBufferSize, true))
- {
- sfs.Position = start;
- do
- {
- try
- {
- count = await sfs.ReadAsync(buffer, 0, Math.Min((int)remainingBytes, ReadStreamBufferSize));
- if (count <= 0) break;
- await outputStream.WriteAsync(buffer, 0, count);
-
- }
- catch (Exception)
- {
- return;
- }
- position = sfs.Position;
- remainingBytes = end - position + 1;
- } while (position <= end);
- }
- }
- catch (Exception)
- {
- }
- finally
- {
- await outputStream.FlushAsync();
- outputStream.Close();
- }
-
- }
- else //---------------------------LOAD FROM SQL SERVER----------------------------------------------
- {
- try
- {
- using (TransactionScope ts = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
- {
- try
- {
- using (SqlDataAdapter da = new SqlDataAdapter("select FileData.PathName(),GET_FILESTREAM_TRANSACTION_CONTEXT() from MediaStream where newsid = " + ID, conStr))
- {
- DataTable dt = new DataTable();
- da.Fill(dt);
- ServerPath = dt.Rows[0][0].ToString();
- ServerTxn = (byte[])dt.Rows[0][1];
- dt.Dispose();
- }
- }
- catch (Exception)
- {
- ts.Complete();
- return;
- }
-
- using (SqlFileStream sfs = new SqlFileStream(ServerPath, ServerTxn, FileAccess.Read, FileOptions.Asynchronous, 0))
- {
- sfs.Position = start;
- do
- {
- try
- {
- count = await sfs.ReadAsync(buffer, 0, Math.Min((int)remainingBytes, ReadStreamBufferSize));
- if (count <= 0) break;
- await outputStream.WriteAsync(buffer, 0, count);
- }
- catch (Exception)
- {
- break;
- }
- position = sfs.Position;
- remainingBytes = end - position + 1;
- } while (position <= end);
-
- }
-
-
- ts.Complete();
- }
- }
- catch (Exception)
- {
-
- return;
- }
- finally
- {
- await outputStream.FlushAsync();
- outputStream.Close();
- }
-
- }
- }
- }
-}
diff --git a/src/Models/MediaStreamInfo.cs b/src/Models/MediaStreamInfo.cs
new file mode 100644
index 0000000..a307209
--- /dev/null
+++ b/src/Models/MediaStreamInfo.cs
@@ -0,0 +1,11 @@
+namespace VideoStream.Models;
+
+public class MediaStreamInfo
+{
+ public string Id { get; set; } = string.Empty;
+ public long FileSize { get; set; }
+ public string FileType { get; set; } = string.Empty;
+ public string FileExt { get; set; } = string.Empty;
+ public string? BufferPath { get; set; }
+ public bool InBuffer { get; set; }
+}
\ No newline at end of file
diff --git a/src/Models/PartialStream.cs b/src/Models/PartialStream.cs
new file mode 100644
index 0000000..a234cfc
--- /dev/null
+++ b/src/Models/PartialStream.cs
@@ -0,0 +1,123 @@
+namespace VideoStream.Models;
+
+public class PartialStream : Stream
+{
+ private readonly Stream _baseStream;
+ private readonly long _start;
+ private readonly long _end;
+ private long _position;
+ private bool _disposed = false;
+
+ public PartialStream(Stream baseStream, long start, long end)
+ {
+ _baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
+ _start = start;
+ _end = end;
+ _position = start;
+
+ if (start > end)
+ throw new ArgumentException("Start position cannot be greater than end position");
+
+ // Set the base stream position to the start position
+ if (_baseStream.CanSeek)
+ _baseStream.Position = _start;
+ }
+
+ public override bool CanRead => _baseStream.CanRead;
+ public override bool CanSeek => _baseStream.CanSeek;
+ public override bool CanWrite => false;
+ public override long Length => _end - _start + 1;
+
+ public override long Position
+ {
+ get => _position - _start;
+ set
+ {
+ if (value < 0 || value > Length)
+ throw new ArgumentOutOfRangeException(nameof(value));
+
+ _position = _start + value;
+ if (_baseStream.CanSeek)
+ _baseStream.Position = _position;
+ }
+ }
+
+ public override void Flush()
+ {
+ _baseStream.Flush();
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ var remainingBytes = _end - _position + 1;
+ var bytesToRead = Math.Min(count, (int)remainingBytes);
+
+ if (bytesToRead <= 0)
+ return 0;
+
+ // Ensure base stream is positioned correctly
+ if (_baseStream.CanSeek && _baseStream.Position != _position)
+ _baseStream.Position = _position;
+
+ var bytesRead = _baseStream.Read(buffer, offset, bytesToRead);
+ _position += bytesRead;
+ return bytesRead;
+ }
+
+ public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ var remainingBytes = _end - _position + 1;
+ var bytesToRead = Math.Min(count, (int)remainingBytes);
+
+ if (bytesToRead <= 0)
+ return 0;
+
+ // Ensure base stream is positioned correctly
+ if (_baseStream.CanSeek && _baseStream.Position != _position)
+ _baseStream.Position = _position;
+
+ var bytesRead = await _baseStream.ReadAsync(buffer, offset, bytesToRead, cancellationToken);
+ _position += bytesRead;
+ return bytesRead;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ long newPosition = origin switch
+ {
+ SeekOrigin.Begin => _start + offset,
+ SeekOrigin.Current => _position + offset,
+ SeekOrigin.End => _end + 1 + offset, // End is inclusive, so we add 1
+ _ => throw new ArgumentException("Invalid seek origin", nameof(origin))
+ };
+
+ if (newPosition < _start || newPosition > _end)
+ throw new ArgumentOutOfRangeException(nameof(offset));
+
+ _position = newPosition;
+ if (_baseStream.CanSeek)
+ _baseStream.Position = _position;
+
+ return _position - _start;
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException("Cannot set length on a partial stream");
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException("Cannot write to a partial stream");
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed && disposing)
+ {
+ _baseStream?.Dispose();
+ _disposed = true;
+ }
+ base.Dispose(disposing);
+ }
+}
\ No newline at end of file
diff --git a/src/Program.cs b/src/Program.cs
new file mode 100644
index 0000000..c6b98d9
--- /dev/null
+++ b/src/Program.cs
@@ -0,0 +1,45 @@
+using Microsoft.Extensions.Caching.Memory;
+using VideoStream.Services;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+builder.Services.AddControllers();
+
+// Configure memory cache
+builder.Services.AddMemoryCache(options =>
+{
+ options.SizeLimit = builder.Configuration.GetValue("Caching:SizeLimit", 1024);
+ options.CompactionPercentage = builder.Configuration.GetValue("Caching:CompactionPercentage", 0.25);
+});
+
+// Add custom services
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+// Configure CORS
+builder.Services.AddCors(options =>
+{
+ options.AddPolicy("AllowAll", policy =>
+ {
+ policy.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+ });
+});
+
+// Configure HTTP client
+builder.Services.AddHttpClient();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+app.UseHttpsRedirection();
+app.UseCors("AllowAll");
+app.UseAuthorization();
+app.MapControllers();
+
+app.Run();
+
+// Make Program class available for integration testing
+public partial class Program { }
\ No newline at end of file
diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs
deleted file mode 100644
index 3378b17..0000000
--- a/src/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("md.akharinkhabar.ir")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("md.akharinkhabar.ir")]
-[assembly: AssemblyCopyright("Copyright © 2015")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("145dedd7-13af-46f4-a909-32226ffae501")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Revision and Build Numbers
-// by using the '*' as shown below:
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/src/Properties/launchSettings.json b/src/Properties/launchSettings.json
new file mode 100644
index 0000000..b885020
--- /dev/null
+++ b/src/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "VideoStream": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:64487;http://localhost:64488"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Services/FileSystemService.cs b/src/Services/FileSystemService.cs
new file mode 100644
index 0000000..0a76ded
--- /dev/null
+++ b/src/Services/FileSystemService.cs
@@ -0,0 +1,58 @@
+namespace VideoStream.Services;
+
+///
+/// Implementation of file system operations
+///
+public class FileSystemService : IFileSystemService
+{
+ private readonly ILogger _logger;
+
+ public FileSystemService(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ public bool FileExists(string path)
+ {
+ try
+ {
+ return File.Exists(path);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Error checking if file exists: {FilePath}", path);
+ return false;
+ }
+ }
+
+ ///
+ public FileStream OpenRead(string path, int bufferSize)
+ {
+ try
+ {
+ return new FileStream(path, FileMode.Open, FileAccess.Read,
+ FileShare.ReadWrite | FileShare.Delete, bufferSize, FileOptions.Asynchronous);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error opening file for reading: {FilePath}", path);
+ throw;
+ }
+ }
+
+ ///
+ public bool CanOpenFile(string path)
+ {
+ try
+ {
+ using var testStream = File.OpenRead(path);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Cannot open file: {FilePath}", path);
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Services/IFileSystemService.cs b/src/Services/IFileSystemService.cs
new file mode 100644
index 0000000..e9c37a2
--- /dev/null
+++ b/src/Services/IFileSystemService.cs
@@ -0,0 +1,29 @@
+namespace VideoStream.Services;
+
+///
+/// Interface for file system operations to improve testability
+///
+public interface IFileSystemService
+{
+ ///
+ /// Checks if a file exists at the specified path
+ ///
+ /// The file path to check
+ /// True if the file exists, false otherwise
+ bool FileExists(string path);
+
+ ///
+ /// Opens a file for reading
+ ///
+ /// The file path to open
+ /// The buffer size for the file stream
+ /// A file stream for reading
+ FileStream OpenRead(string path, int bufferSize);
+
+ ///
+ /// Tests if a file can be opened for reading
+ ///
+ /// The file path to test
+ /// True if the file can be opened, false otherwise
+ bool CanOpenFile(string path);
+}
\ No newline at end of file
diff --git a/src/Services/IMediaStreamService.cs b/src/Services/IMediaStreamService.cs
new file mode 100644
index 0000000..9dae691
--- /dev/null
+++ b/src/Services/IMediaStreamService.cs
@@ -0,0 +1,10 @@
+using VideoStream.Models;
+
+namespace VideoStream.Services;
+
+public interface IMediaStreamService
+{
+ Task GetMediaStreamInfoAsync(string newsId);
+ Task CreatePartialContentAsync(string newsId, long start, long end);
+ bool IsValidNewsId(string newsId);
+}
\ No newline at end of file
diff --git a/src/Services/MediaStreamService.cs b/src/Services/MediaStreamService.cs
new file mode 100644
index 0000000..62e72cc
--- /dev/null
+++ b/src/Services/MediaStreamService.cs
@@ -0,0 +1,186 @@
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.Caching.Memory;
+using System.Data;
+using System.Transactions;
+using VideoStream.Models;
+
+namespace VideoStream.Services;
+
+public class MediaStreamService : IMediaStreamService
+{
+ private readonly IConfiguration _configuration;
+ private readonly IMemoryCache _cache;
+ private readonly ILogger _logger;
+ private readonly IFileSystemService _fileSystemService;
+ private readonly string _connectionString;
+ private readonly string[] _bufferPaths;
+ private readonly int _readStreamBufferSize;
+ private readonly int _cacheExpirationSeconds;
+
+ public MediaStreamService(IConfiguration configuration, IMemoryCache cache, ILogger logger, IFileSystemService fileSystemService)
+ {
+ _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _fileSystemService = fileSystemService ?? throw new ArgumentNullException(nameof(fileSystemService));
+ _connectionString = configuration.GetConnectionString("Media") ?? throw new InvalidOperationException("Media connection string not found");
+ _bufferPaths = configuration.GetSection("VideoStream:BufferPaths").Get() ?? new[] { "B:\\", "F:\\" };
+ _readStreamBufferSize = configuration.GetValue("VideoStream:ReadStreamBufferSize", 262144);
+ _cacheExpirationSeconds = configuration.GetValue("VideoStream:CacheExpirationSeconds", 30);
+ }
+
+ public bool IsValidNewsId(string newsId)
+ {
+ if (string.IsNullOrWhiteSpace(newsId) || newsId != newsId.Trim())
+ {
+ return false;
+ }
+
+ if (long.TryParse(newsId, out long parsedId))
+ {
+ return parsedId >= 0;
+ }
+
+ return false;
+ }
+
+ public async Task GetMediaStreamInfoAsync(string newsId)
+ {
+ if (!IsValidNewsId(newsId))
+ return null;
+
+ var cacheKey = $"MediaStream-{newsId}";
+
+ if (_cache.TryGetValue(cacheKey, out MediaStreamInfo? cachedInfo))
+ {
+ return cachedInfo;
+ }
+
+ try
+ {
+ using var connection = new SqlConnection(_connectionString);
+ await connection.OpenAsync();
+
+ using var command = new SqlCommand("SELECT FileSize, FileType, FileExt FROM MediaStream WHERE newsid = @newsId", connection);
+ command.Parameters.AddWithValue("@newsId", newsId);
+
+ using var reader = await command.ExecuteReaderAsync();
+
+ if (!await reader.ReadAsync())
+ return null;
+
+ var mediaInfo = new MediaStreamInfo
+ {
+ Id = newsId,
+ FileSize = reader.GetInt64("FileSize"),
+ FileType = reader.GetString("FileType"),
+ FileExt = reader.GetString("FileExt")
+ };
+
+ // Check if file exists in buffer paths
+ foreach (var bufferPath in _bufferPaths)
+ {
+ var filePath = Path.Combine(bufferPath, newsId + mediaInfo.FileExt);
+ if (_fileSystemService.FileExists(filePath))
+ {
+ if (_fileSystemService.CanOpenFile(filePath))
+ {
+ mediaInfo.BufferPath = filePath;
+ mediaInfo.InBuffer = true;
+ break;
+ }
+ }
+ }
+
+ // Cache the result
+ _cache.Set(cacheKey, mediaInfo, TimeSpan.FromSeconds(_cacheExpirationSeconds));
+
+ return mediaInfo;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error retrieving media stream info for newsId {NewsId}", newsId);
+ return null;
+ }
+ }
+
+ public async Task CreatePartialContentAsync(string newsId, long start, long end)
+ {
+ var mediaInfo = await GetMediaStreamInfoAsync(newsId);
+ if (mediaInfo == null)
+ throw new FileNotFoundException($"Media stream not found for newsId: {newsId}");
+
+ if (mediaInfo.InBuffer && !string.IsNullOrEmpty(mediaInfo.BufferPath))
+ {
+ return await CreatePartialContentFromBufferAsync(mediaInfo.BufferPath, start, end);
+ }
+ else
+ {
+ return await CreatePartialContentFromSqlServerAsync(newsId, start, end);
+ }
+ }
+
+ private Task CreatePartialContentFromBufferAsync(string bufferPath, long start, long end)
+ {
+ try
+ {
+ var fileStream = _fileSystemService.OpenRead(bufferPath, _readStreamBufferSize);
+ // Don't set position here - let PartialStream handle it
+
+ return Task.FromResult(new PartialStream(fileStream, start, end));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating partial content from buffer {BufferPath}", bufferPath);
+ throw;
+ }
+ }
+
+ private async Task CreatePartialContentFromSqlServerAsync(string newsId, long start, long end)
+ {
+ try
+ {
+ // Since SqlFileStream is deprecated, we'll use a different approach
+ // We'll read the data in chunks from the database
+ using var connection = new SqlConnection(_connectionString);
+ await connection.OpenAsync();
+
+ // Create a memory stream to hold the data
+ var memoryStream = new MemoryStream();
+
+ // Read the file data in chunks
+ var chunkSize = Math.Min(_readStreamBufferSize, (int)(end - start + 1));
+ var buffer = new byte[chunkSize];
+ var currentPosition = start;
+
+ while (currentPosition <= end)
+ {
+ var bytesToRead = Math.Min(chunkSize, (int)(end - currentPosition + 1));
+
+ // Use SUBSTRING to read a portion of the FILESTREAM data
+ // Note: This approach may not be as efficient as SqlFileStream for very large files
+ using var command = new SqlCommand(
+ "SELECT SUBSTRING(FileData, @offset, @length) FROM MediaStream WHERE newsid = @newsId",
+ connection);
+ command.Parameters.AddWithValue("@newsId", newsId);
+ command.Parameters.AddWithValue("@offset", currentPosition + 1); // SQL Server uses 1-based indexing
+ command.Parameters.AddWithValue("@length", bytesToRead);
+
+ var data = await command.ExecuteScalarAsync() as byte[];
+ if (data == null || data.Length == 0)
+ break;
+
+ await memoryStream.WriteAsync(data, 0, data.Length);
+ currentPosition += data.Length;
+ }
+
+ memoryStream.Position = 0;
+ return memoryStream;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating partial content from SQL Server for newsId {NewsId}", newsId);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/VideoStream.csproj b/src/VideoStream.csproj
index 07972d7..03ab2ca 100644
--- a/src/VideoStream.csproj
+++ b/src/VideoStream.csproj
@@ -1,225 +1,28 @@
-
-
-
+
+
- Debug
- AnyCPU
-
-
- 2.0
- {228A03E1-3C06-4603-8576-DB4DF833374A}
- {E3E379DF-F4C6-4180-9B81-6769533ABE47};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}
- Library
- Properties
- md.akharinkhabar.ir
- md.akharinkhabar.ir
- v4.5.1
- false
- true
-
-
-
-
-
- true
-
-
-
-
- 12.0
- SAK
- SAK
- SAK
- SAK
-
-
- true
- full
- false
- bin\
- DEBUG;TRACE
- prompt
- 4
-
-
- pdbonly
- true
- bin\
- TRACE
- prompt
- 4
+ net9.0
+ enable
+ enable
+ VideoStream
+ VideoStream
+
-
- C:\Users\m.arsanjani\Desktop\memcache\Enyim.Caching.2.12\Enyim.Caching.dll
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- True
- ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll
-
-
- True
- ..\packages\Microsoft.AspNet.Mvc.FixedDisplayModes.1.0.0\lib\net40\Microsoft.Web.Mvc.FixedDisplayModes.dll
-
-
- ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll
-
-
-
-
- ..\packages\Microsoft.AspNet.WebApi.Client.4.0.30506.0\lib\net40\System.Net.Http.Formatting.dll
-
-
-
-
- True
- ..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.Helpers.dll
-
-
- ..\packages\Microsoft.AspNet.WebApi.Core.4.0.30506.0\lib\net40\System.Web.Http.dll
-
-
- ..\packages\Microsoft.AspNet.WebApi.WebHost.4.0.30506.0\lib\net40\System.Web.Http.WebHost.dll
-
-
- True
- ..\packages\Microsoft.AspNet.Mvc.4.0.30506.0\lib\net40\System.Web.Mvc.dll
-
-
- True
- ..\packages\Microsoft.AspNet.Razor.2.0.30506.0\lib\net40\System.Web.Razor.dll
-
-
- True
- ..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.WebPages.dll
-
-
- True
- ..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.WebPages.Deployment.dll
-
-
- True
- ..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.WebPages.Razor.dll
-
-
+
+
+
+
+
+
-
-
-
-
- Default.aspx
- ASPXCodeBehind
-
-
- Default.aspx
-
-
- Global.asax
-
-
-
-
-
-
-
-
-
-
-
-
- Designer
-
-
- Web.config
+
+ Always
-
- Web.config
+
+ Always
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10.0
- $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
-
-
- true
- bin\
- DEBUG;TRACE
- full
- x64
- prompt
- MinimumRecommendedRules.ruleset
-
-
- bin\
- TRACE
- true
- pdbonly
- x64
- prompt
- MinimumRecommendedRules.ruleset
-
-
-
-
-
-
-
-
-
-
-
- True
- True
- 51079
- /
- http://localhost:51079/
- False
- False
-
-
- False
-
-
-
-
-
+
\ No newline at end of file
diff --git a/src/Web.Debug.config b/src/Web.Debug.config
deleted file mode 100644
index 3e2a97c..0000000
--- a/src/Web.Debug.config
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Web.Release.config b/src/Web.Release.config
deleted file mode 100644
index 9fd481f..0000000
--- a/src/Web.Release.config
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Web.config b/src/Web.config
deleted file mode 100644
index 226af0a..0000000
--- a/src/Web.config
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/appsettings.Development.json b/src/appsettings.Development.json
new file mode 100644
index 0000000..bb8d344
--- /dev/null
+++ b/src/appsettings.Development.json
@@ -0,0 +1,11 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft.AspNetCore": "Information"
+ }
+ },
+ "ConnectionStrings": {
+ "Media": "integrated security=true;Persist Security Info=true;Initial Catalog=dbMedia;Data Source=localhost;TrustServerCertificate=true;"
+ }
+}
\ No newline at end of file
diff --git a/src/appsettings.json b/src/appsettings.json
new file mode 100644
index 0000000..adaf245
--- /dev/null
+++ b/src/appsettings.json
@@ -0,0 +1,25 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "Media": "integrated security=true;Persist Security Info=true;Initial Catalog=dbMedia;Data Source=localhost;TrustServerCertificate=true;"
+ },
+ "VideoStream": {
+ "BufferPaths": [
+ "B:\\",
+ "F:\\"
+ ],
+ "ReadStreamBufferSize": 262144,
+ "CacheExpirationSeconds": 30,
+ "ExecutionTimeoutSeconds": 600
+ },
+ "Caching": {
+ "SizeLimit": 1024,
+ "CompactionPercentage": 0.25
+ }
+}
\ No newline at end of file
diff --git a/src/packages.config b/src/packages.config
deleted file mode 100644
index ccf061d..0000000
--- a/src/packages.config
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file