目录
概述
SimpleAudioPlayer 不要求输入必须是本地文件。通过「流句柄」(Stream Handle) 抽象层,你可以从任何数据源提供音频数据——包括 HTTP URL、内存流、加密流、数据库 BLOB 等。
播放器加载数据时,只需要传入一个实现了 IStreamHandle 的句柄对象:
// 所有流句柄的基类
public abstract class StreamHandle : IStreamHandle { ... }
// 具体类型
var handle = new FileStreamHandler(@"C:\music.mp3");
player.Load(handle);流句柄类型对比
类型 | 数据来源 | 缓存方式 | 是否支持 Seek | 适用场景 |
|---|---|---|---|---|
| 本地文件 / URI | 无 | 是 | 本地文件播放 |
| 任意 .NET | 无 | 取决于源流 | 内存流、加密流等 |
| HTTP URL | 无 | 是(通过 Range) | 直接网络播放 |
| 包装其他句柄 | 全内存缓存 | 是 | 需要完整缓存的网络流 |
| 包装其他句柄 | 磁盘缓存 | 是 | 大文件磁盘缓存 |
| HTTP URL | 渐进式下载 | 是 | 边下边播 + 跳转 |
| 自定义委托 | 自定义 | 自定义 | 特殊数据源 |
FileStreamHandler:本地文件播放
最简单的用法。支持本地文件路径和 file:// URI。
using SimpleAudioPlayer;
using var player = new AudioPlayer();
// 方式一:直接使用文件路径
var handle1 = new FileStreamHandler(@"C:\Music\song.mp3");
player.Load(handle1);
player.Play();
// 方式二:使用 URI
var handle2 = new FileStreamHandler("C:\Music\song.mp3");
player.Load(handle2);
// 方式三:URI 也可以指向网络路径
var handle3 = new FileStreamHandler("\\NAS\Music\song.flac");
player.Load(handle3);支持的文件格式: MP3, AAC, FLAC, Ogg Vorbis, Opus, WAV, APE, WavPack, ALAC, AC3, AIFF, WV, MPC
StreamHandle:任意 .NET 流
包装任何 System.IO.Stream 对象。这意味着你可以从任何来源提供数据:
using SimpleAudioPlayer;
// 从 MemoryStream 播放
byte[] audioBytes = await File.ReadAllBytesAsync(@"C:\Music\example.mp3");
using var memoryStream = new MemoryStream(audioBytes);
var handle = new StreamHandle(memoryStream);
using var player = new AudioPlayer();
player.Load(handle);
player.Play();从网络流播放:
using SimpleAudioPlayer;
using var httpClient = new HttpClient();
using var networkStream = await httpClient.GetStreamAsync("https://example.com/audio.mp3");
var handle = new StreamHandle(networkStream);
using var player = new AudioPlayer();
player.Load(handle);
player.Play();从数据库 BLOB 播放:
using SimpleAudioPlayer;
using System.Data.SqlClient;
// 从数据库读取音频 BLOB
byte[] audioData;
using (var conn = new SqlConnection(connectionString))
using (var cmd = new SqlCommand("SELECT AudioData FROM Songs WHERE Id = @id", conn))
{
cmd.Parameters.AddWithValue("@id", 42);
conn.Open();
audioData = (byte[])await cmd.ExecuteScalarAsync();
}
using var stream = new MemoryStream(audioData);
var handle = new StreamHandle(stream);
using var player = new AudioPlayer();
player.Load(handle);
player.Play();从加密流播放:
using SimpleAudioPlayer;
using System.Security.Cryptography;
byte[] encryptedData = await File.ReadAllBytesAsync(@"C:\Music\encrypted.dat");
byte[] key = Convert.FromHexString("...");
byte[] iv = Convert.FromHexString("...");
using var encryptedStream = new MemoryStream(encryptedData);
using var aes = Aes.Create();
using var decryptor = aes.CreateDecryptor(key, iv);
using var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read);
var handle = new StreamHandle(cryptoStream);
using var player = new AudioPlayer();
player.Load(handle);
player.Play();重要:
StreamHandle不会自动释放源流。你需要确保流在播放期间保持存活,播放完成后释放。如果流在播放中被释放,将导致播放失败。
HttpStreamHandle:HTTP 网络流
通过 HTTP/HTTPS 直接播放远程音频文件。支持 HTTP Range 请求,因此可以 Seek。
using SimpleAudioPlayer;
var handle = await HttpStreamHandle.CreateAsync("https://example.com/music.mp3");
using var player = new AudioPlayer();
// 监听进度
handle.ProgressChanged += (downloaded, total) =>
Console.Write($"\r下载: {downloaded}/{total}");
};
// 监听状态
player.PlaybackFailed += (s, e) =>
{
Console.WriteLine($"\n播放失败: {e.Exception?.Message}");
};
player.Load(handle);
player.Play();
Console.WriteLine("正在播放网络音频... 按 Enter 退出");
Console.ReadLine();Range 请求行为
当调用
Seek()时,HttpStreamHandle发送 HTTPRange头服务器必须支持
Accept-Ranges: bytes才能正确 Seek如果不支持 Range,Seek 会重置流并重新下载
自动重试
网络中断时自动重试(最多 3 次)
重试间隔:1s, 2s, 4s(指数退避)
可监听
PlaybackFailed获取最终失败通知
CachedStreamHandle:全内存缓存
包装另一个流句柄,在内存中缓存整个音频文件。适合需要完整缓存才能流畅 Seek 的场景。
using SimpleAudioPlayer;
// 包装 HTTP 流,完整下载到内存后再播放
var httpHandle = await HttpStreamHandle.CreateAsync("https://example.com/podcast.mp3");
var cachedHandle = new CachedStreamHandle(httpStream);
using var player = new AudioPlayer();
handle.ProgressChanged += (downloaded, total) =>
Console.Write($"\r下载: {downloaded}/{total}");
};
// Load 会等待完整下载完成
player.Load(cachedHandle);
Console.WriteLine("\n缓存完成,开始播放");
player.Play();
Console.ReadLine();关键特性:
在
Load期间将整个文件下载到内存下载完成后开始播放,Seek 无需网络请求
适用于播客、有声书等需要完整文件的场景
大文件会消耗较多内存
DiskCachedStreamHandle:磁盘缓存
将音频数据缓存到磁盘文件,适合大文件或需要持久化缓存的场景。
using SimpleAudioPlayer;
var httpHandle = await HttpStreamHandle.CreateAsync("https://example.com/large_audio.flac");
var diskHandle = new DiskCachedStreamHandle(
innerHandle: httpHandle,
cacheDirectory: @"C:\AudioCache",
cacheFileName: "large_audio.flac",
autoCommitOnComplete: true
);
using var player = new AudioPlayer();
handle.ProgressChanged += (downloaded, total) =>
Console.Write($"\r下载: {downloaded}/{total}");
};
// 首次播放:下载并缓存
player.Load(diskHandle);
player.Play();
Console.WriteLine("\n播放中... 按 Enter 停止并提交缓存");
Console.ReadLine();
// 停止播放后提交缓存(如果在选项中启用自动提交)
player.Stop();参数说明
参数 | 说明 |
|---|---|
| 被包装的源句柄 |
| 缓存文件存放目录 |
| 缓存文件名 |
| 下载完成后自动提交缓存 |
缓存生命周期
// 第二次播放:直接使用缓存文件,无需网络请求
var cachedFileHandle = new FileStreamHandler(@"C:\AudioCache\large_audio.flac");
using var player2 = new AudioPlayer();
player2.Load(cachedFileHandle);
player2.Play();
// 清理缓存
if (File.Exists(@"C:\AudioCache\large_audio.flac"))
{
File.Delete(@"C:\AudioCache\large_audio.flac");
}ProgressiveHttpStreamHandle:渐进式 HTTP 流
最强大的网络流方案——支持边下载边播放、Seek 到已下载部分、断点续传。
using SimpleAudioPlayer;
// 创建渐进式流句柄
var handle = await ProgressiveHttpStreamHandle.CreateAsync("https://example.com/long_audio.mp3", "output.mp3");
using var player = new AudioPlayer();
// 下载进度
handle.ProgressChanged += (downloaded, total) =>
Console.Write($"\r下载: {downloaded}/{total}");
};
// 异步加载(不会等待下载完成)
player.Load(handle);
Console.WriteLine("\n开始边下边播...");
player.Play();
// 播放中跳转到已缓存的部分
await Task.Delay(5000);
if (player.Duration > TimeSpan.FromSeconds(30))
{
Console.WriteLine("跳转到 30 秒...");
player.Seek(TimeSpan.FromSeconds(30));
}
Console.ReadLine();异步创建
// 使用工厂方法异步创建
var handle = await ProgressiveHttpStreamHandle.CreateAsync(
"https://example.com/audio.mp3",
progressCallback: p => Console.WriteLine($"创建进度: {p:P1}")
);
using var player = new AudioPlayer();
player.Load(handle);
player.Play();断点续传
var handle = await ProgressiveHttpStreamHandle.CreateAsync(
"https://example.com/podcast.mp3", "output.mp3",
resume: true // 启用断点续传
);进度事件数据
属性 | 说明 |
|---|---|
| 已下载字节数 |
| 总字节数(可能未知时为 -1) |
| 下载进度,0.0 ~ 1.0 |
CustomHandle:自定义数据源
当你需要完全控制数据提供方式时,使用 CustomHandle。它通过委托函数提供数据。
using SimpleAudioPlayer;
// 通过委托读取数据
var handle = new CustomHandle(
readFunc: async (buffer, offset, count, cancellationToken) =>
{
// 从自定义数据源读取 count 字节到 buffer 的 offset 位置
// 返回实际读取的字节数(0 表示读取完毕)
int bytesRead = await MyDataSource.ReadAsync(buffer, offset, count);
return bytesRead;
},
canSeek: false // 是否支持随机访问
);
using var player = new AudioPlayer();
player.Load(handle);
player.Play();支持 Seek 的自定义句柄
var seekableHandle = new CustomHandle(
readFunc: async (buffer, offset, count, ct) =>
{
return await myStream.ReadAsync(buffer, offset, count, ct);
},
canSeek: true,
seekFunc: (position, origin) =>
{
myStream.Seek(position, (SeekOrigin)origin);
}
);从加密数据库播放
var handle = new CustomHandle(
readFunc: async (buffer, offset, count, ct) =>
{
// 从数据库分段读取
byte[] chunk = await dbContext.AudioFiles
.Where(a => a.Id == 42)
.Select(a => a.Data.Skip(currentPos).Take(count).ToArray())
.FirstAsync();
Array.Copy(chunk, 0, buffer, offset, chunk.Length);
currentPos += chunk.Length;
return chunk.Length;
},
canSeek: true,
seekFunc: (pos, origin) =>
{
currentPos = origin switch
{
SeekOrigin.Begin => (int)pos,
SeekOrigin.Current => currentPos + (int)pos,
SeekOrigin.End => audioRecord.Length + (int)pos,
_ => currentPos
};
}
);
using var player = new AudioPlayer();
player.Load(handle);
player.Play();实战:网络电台播放器
以下示例实现了完整的网络电台播放器,支持播放、暂停、停止、音量调节、缓冲进度显示。
using SimpleAudioPlayer;
Console.WriteLine("SimpleAudioPlayer 网络电台");
Console.WriteLine("===========================");
Console.WriteLine();
// 电台列表
var stations = new Dictionary<string, string>
{
["古典音乐"] = "https://example.com/stream/classical.mp3",
["爵士频道"] = "https://example.com/stream/jazz.mp3",
["新闻直播"] = "https://example.com/stream/news.mp3",
["本地测试"] = "http://localhost:8000/test.mp3"
};
Console.WriteLine("可选电台:");
foreach (var (name, url) in stations)
{
Console.WriteLine($" {name}: {url}");
}
Console.WriteLine();
Console.Write("请输入电台名称: ");
string? stationName = Console.ReadLine();
if (stationName == null || !stations.TryGetValue(stationName, out string? stationUrl))
{
Console.WriteLine("未找到该电台,使用默认测试 URL。");
stationUrl = "https://example.com/stream/default.mp3";
}
Console.WriteLine($"正在连接: {stationUrl}");
// 使用渐进式 HTTP 流(适合直播和长音频)
var handle = await ProgressiveHttpStreamHandle.CreateAsync(stationUrl, "radio.mp3");
using var player = new AudioPlayer();
// 下载状态
handle.ProgressChanged += (downloaded, total) =>
Console.Write($"\r下载: {downloaded}/{total}");
};
// 状态变化
player.PlaybackStateChanged += (s, e) =>
{
Console.WriteLine($"\n[状态] {e.OldState} -> {e.NewState}");
};
// 播放失败
player.PlaybackFailed += (s, e) =>
{
Console.WriteLine($"\n[错误] 播放失败: {e.Exception?.Message} (代码: {e.ErrorCode})");
Console.WriteLine("尝试重连...");
};
// 加载并播放
try
{
player.Load(handle);
Console.WriteLine("\n已连接,开始播放!");
player.Play();
}
catch (Exception ex)
{
Console.WriteLine($"连接失败: {ex.Message}");
return;
}
// 控制循环
bool exit = false;
while (!exit)
{
Console.WriteLine();
Console.WriteLine("[P]暂停/恢复 [S]停止 [+/-]音量 [→]快进10s [←]后退10s [Q]退出");
var key = Console.ReadKey(true);
switch (key.KeyChar)
{
case 'p':
case 'P':
if (player.State == PlaybackState.Playing)
{
player.Pause();
Console.WriteLine("已暂停");
}
else
{
player.Play();
Console.WriteLine("已恢复");
}
break;
case 's':
case 'S':
player.Stop();
Console.WriteLine("已停止");
break;
case '+':
player.Volume = Math.Min(1.0, player.Volume + 0.1);
Console.WriteLine($"音量: {player.Volume:P0}");
break;
case '-':
player.Volume = Math.Max(0.0, player.Volume - 0.1);
Console.WriteLine($"音量: {player.Volume:P0}");
break;
case '\u001b': // 方向键
if (key.Key == ConsoleKey.RightArrow)
{
player.Seek(player.Time + TimeSpan.FromSeconds(10));
Console.WriteLine($"快进到: {player.Time:mm\\:ss}");
}
else if (key.Key == ConsoleKey.LeftArrow)
{
player.Seek(player.Time - TimeSpan.FromSeconds(10));
Console.WriteLine($"后退到: {player.Time:mm\\:ss}");
}
break;
case 'q':
case 'Q':
exit = true;
break;
}
}
Console.WriteLine("电台播放器已关闭。");
return;
static string FormatSize(long bytes)
{
string[] units = { "B", "KB", "MB", "GB" };
int unitIndex = 0;
double size = bytes;
while (size >= 1024 && unitIndex < units.Length - 1)
{
size /= 1024;
unitIndex++;
}
return $"{size:F1} {units[unitIndex]}";
}错误处理与最佳实践
通用错误处理模式
public async Task PlaySafelyAsync(IStreamHandle handle)
{
var player = new AudioPlayer();
player.PlaybackFailed += OnPlaybackFailed;
try
{
player.Load(handle);
player.Play();
}
catch (ArgumentException ex)
{
Console.WriteLine($"不支持的格式或无效句柄: {ex.Message}");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"播放器状态错误: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"未预期的错误: {ex.Message}");
}
}流句柄使用原则
选择合适的句柄类型:本地文件用
FileStreamHandler,网络小文件用HttpStreamHandle,大文件直播用ProgressiveHttpStreamHandle不要忘记释放:
StreamHandle包装的Stream需要手动释放,建议在PlayCompleted或PlaybackFailed中释放网络流 + 错误处理:网络不稳定时,使用
CachedStreamHandle或DiskCachedStreamHandle提供更好的体验内存管理:
CachedStreamHandle会占用大量内存,大文件建议使用DiskCachedStreamHandleSeek 能力:只有支持随机访问的句柄才能正确响应
Seek(),StreamHandle包装非可查找流时 Seek 会失败
流句柄生命周期
创建句柄 → Load → Play/Pause/Stop/Seek → Dispose(播放器自动释放句柄)播放器 Dispose() 时会自动释放关联的流句柄资源。
通过本文的学习,你已经掌握了 SimpleAudioPlayer 全部 7 种流数据源的使用方法,能够应对本地文件、网络流、内存流、加密数据、数据库等多种数据来源场景。下一章将介绍音频录制功能。
发表评论