目录

  1. 概述

  2. 流句柄类型对比

  3. FileStreamHandler:本地文件播放

  4. StreamHandle:任意 .NET 流

  5. HttpStreamHandle:HTTP 网络流

  6. CachedStreamHandle:内存缓存流

  7. DiskCachedStreamHandle:磁盘缓存流

  8. ProgressiveHttpStreamHandle:渐进式 HTTP 流

  9. CustomHandle:自定义数据源

  10. 实战:网络电台播放器

  11. 错误处理与最佳实践


概述

SimpleAudioPlayer 不要求输入必须是本地文件。通过「流句柄」(Stream Handle) 抽象层,你可以从任何数据源提供音频数据——包括 HTTP URL、内存流、加密流、数据库 BLOB 等。


播放器加载数据时,只需要传入一个实现了 IStreamHandle 的句柄对象:

// 所有流句柄的基类
public abstract class StreamHandle : IStreamHandle { ... }

// 具体类型
var handle = new FileStreamHandler(@"C:\music.mp3");
player.Load(handle);

流句柄类型对比

类型

数据来源

缓存方式

是否支持 Seek

适用场景

FileStreamHandler

本地文件 / URI

本地文件播放

StreamHandle

任意 .NET Stream

取决于源流

内存流、加密流等

HttpStreamHandle

HTTP URL

是(通过 Range)

直接网络播放

CachedStreamHandle

包装其他句柄

全内存缓存

需要完整缓存的网络流

DiskCachedStreamHandle

包装其他句柄

磁盘缓存

大文件磁盘缓存

ProgressiveHttpStreamHandle

HTTP URL

渐进式下载

边下边播 + 跳转

CustomHandle

自定义委托

自定义

自定义

特殊数据源


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 发送 HTTP Range

  • 服务器必须支持 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();

参数说明

参数

说明

innerHandle

被包装的源句柄

cacheDirectory

缓存文件存放目录

cacheFileName

缓存文件名

autoCommitOnComplete

下载完成后自动提交缓存

缓存生命周期

// 第二次播放:直接使用缓存文件,无需网络请求
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 // 启用断点续传
);

进度事件数据

属性

说明

downloaded

已下载字节数

total

总字节数(可能未知时为 -1)

Progress

下载进度,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}");
    }
}

流句柄使用原则

  1. 选择合适的句柄类型:本地文件用 FileStreamHandler,网络小文件用 HttpStreamHandle,大文件直播用 ProgressiveHttpStreamHandle

  2. 不要忘记释放StreamHandle 包装的 Stream 需要手动释放,建议在 PlayCompletedPlaybackFailed 中释放

  3. 网络流 + 错误处理:网络不稳定时,使用 CachedStreamHandleDiskCachedStreamHandle 提供更好的体验

  4. 内存管理CachedStreamHandle 会占用大量内存,大文件建议使用 DiskCachedStreamHandle

  5. Seek 能力:只有支持随机访问的句柄才能正确响应 Seek()StreamHandle 包装非可查找流时 Seek 会失败

流句柄生命周期

创建句柄 → Load → Play/Pause/Stop/Seek → Dispose(播放器自动释放句柄)

播放器 Dispose() 时会自动释放关联的流句柄资源。


通过本文的学习,你已经掌握了 SimpleAudioPlayer 全部 7 种流数据源的使用方法,能够应对本地文件、网络流、内存流、加密数据、数据库等多种数据来源场景。下一章将介绍音频录制功能。