Windows Phone 8.1 直播视频流





5.00/5 (3投票s)
一个简单的示例,展示了如何从 Windows Phone 8.1 (Silverlight) 向独立的桌面应用程序实现直播视频流
引言
下面的示例演示了如何将来自摄像头的直播视频流式传输到独立的桌面应用程序。它实现了一个在 Windows Phone 8.1 Silverlight 中运行的简单客户端应用程序,该应用程序提供了一个用户界面来预览和录制摄像头的视频。当用户单击录制按钮时,应用程序将打开与服务的连接并开始流式传输。服务应用程序接收流并将其存储到 MP4 (MPEG-4) 文件中。
此场景的实现解决了以下主题
- 在 Windows Phone 8.1 中捕获视频
- 跨网络流式传输视频。
- 在桌面应用程序中接收视频并将其存储到文件。
运行示例
- 下载 Eneter Messaging Framework 适用于 .NET 平台。
- 下载示例项目.
- 在 Visual Studio 中打开解决方案,并更新以下引用:
Eneter.Messaging.Framework.dll - 适用于 .NET 4.5
Eneter.Messaging.Framework.WindowsPhone.dll - 适用于 Windows Phone 8.1 Silverlight - 在您的网络中找出您计算机的 IP 地址,并在客户端和服务源代码中更新 IP 地址。
要找出您的服务的 IP 地址,您可以运行,例如,以下命令ipconfig -all
- 运行服务应用程序。
- 将客户端应用程序部署到 Windows Phone 设备并运行它。
在 Windows Phone 8.1 中捕获视频
要使用摄像头,应用程序需要以下功能的权限
ID_CAP_ISV_CAMERA
- 提供对摄像头的访问权限ID_CAP_MICROPHONE
- 提供对手机麦克风的访问权限
这些功能可以在 WMAppManifest.xml (位于 Properties 文件夹中) 中启用。
要捕获视频,Windows Phone 8.1 提供了 MediaCapture 类,该类提供了预览和录制视频(包括音频)的功能。在实例化 MediaCapture
对象后,需要使用正确的设置进行初始化 (请参阅 ActivateCameraAsync()
),然后为了启用预览,需要将其与 VideoBrush
关联 (请参阅 StartPreviewAsync()
)。
确保在不需要 MediaCapture
时或在应用程序挂起时正确将其释放也非常重要。否则,将导致其他访问摄像头的应用程序出现问题。
(例如,当我没有释放 MediaCapture
时,我无法多次启动应用程序。以下启动在摄像头初始化期间总是失败,并且需要重启手机。)
跨网络流式传输视频
要录制视频,将调用 StartRecordToStreamAsync(..)
方法。该方法接受两个参数:
MediaEncodingProfile
- 指定视频格式(例如 MP4 或 WMV)- IRandomAccessStream - 指定要写入捕获视频的流
为了在网络上提供流式传输,实现了自定义的 MessageSendingStream
(继承自 IRandomAccessStream
),然后将其用作 StartRecordToStreamAsync(..)
的输入参数。
它不实现整个接口,只实现 MediaCapture
写入 MPEG-4 格式所必需的方法。
当用户开始录制时,将实例化 MessageSendingStream
并打开与服务的连接。然后调用 StartRecordToStreamAsync(..., MessageSendingStream)
,并将捕获的数据发送到服务。
由于 MP4 的写入不是完全顺序的,因此从客户端发送到服务的流消息由两部分组成:
- 4 字节 - 一个整数,指示 MP4 文件中的位置
- n 字节 - 摄像头捕获的视频/音频数据
当用户停止录制时,MediaCapture
完成写入并将与服务的连接关闭。一旦连接关闭,服务将关闭 MP4 文件。
在桌面应用程序中接收视频
接收流非常简单。当服务接收到消息时,它会解码位置(前 4 个字节),并将传入的视频数据写入 MP4 文件中的指定位置。
服务可以连接多个录制客户端。因此,服务为每个连接的(录制)客户端维护一个单独的 MP4 文件。一旦客户端断开连接,文件将被关闭并可供进一步使用(例如,剪切或回放)。
Windows Phone 客户端
Windows Phone 客户端是一个简单的应用程序,显示视频预览并提供开始和停止视频捕获的按钮。
当用户单击“开始录制”时,它将打开与服务的连接并开始发送流消息。
当用户单击“停止录制”或应用程序挂起时,它将完成录制并关闭与服务的连接。
实现包括两个主要部分:
- 处理摄像头的逻辑 - 在 MainPage.xaml.cs 文件中实现
- 向服务发送流消息的逻辑 - 在 MessageSendingStream.cs 文件中实现。
MainPage.xaml.cs 的实现
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Navigation;
using Windows.Devices.Enumeration;
using Windows.Media.Capture;
using Windows.Media.MediaProperties;
using Windows.Phone.Media.Capture;
namespace PhoneCamera
{
public partial class MainPage : PhoneApplicationPage
{
// Stream sending messages to the service.
private MessageSendingStream myMessageSendingStream;
// Video capturing.
private VideoBrush myVideoRecorderBrush;
private MediaCapturePreviewSink myPreviewSink;
private MediaCapture myMediaCapture;
private MediaEncodingProfile myProfile;
private bool myIsRecording;
// Constructor
public MainPage()
{
InitializeComponent();
// Prepare ApplicationBar and buttons.
PhoneAppBar = (ApplicationBar)ApplicationBar;
PhoneAppBar.IsVisible = true;
StartRecordingBtn = ((ApplicationBarIconButton)ApplicationBar.Buttons[0]);
StopRecordingBtn = ((ApplicationBarIconButton)ApplicationBar.Buttons[1]);
}
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Disable both buttons until initialization is completed.
StartRecordingBtn.IsEnabled = false;
StopRecordingBtn.IsEnabled = false;
try
{
await ActivateCameraAsync();
await StartPreviewAsync();
// Enable Start Recording button.
StartRecordingBtn.IsEnabled = true;
txtDebug.Text = "Ready...";
}
catch (Exception err)
{
txtDebug.Text = "ERROR: " + err.Message;
}
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
DeactivateCamera();
base.OnNavigatedFrom(e);
}
private async void OnStartRecordingClick(object sender, EventArgs e)
{
await StartRecordingAsync();
}
// Handle stop requests.
private async void OnStopRecordingClick(object sender, EventArgs e)
{
await StopRecordingAsync("Ready...");
}
private async Task ActivateCameraAsync()
{
// Find the camera device id to use
string aDeviceId = "";
var aDevices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
for (var i = 0; i < aDevices.Count; ++i)
{
aDeviceId = aDevices[i].Id;
}
// Capture settings.
var aSettings = new MediaCaptureInitializationSettings();
aSettings.AudioDeviceId = "";
aSettings.VideoDeviceId = aDeviceId;
aSettings.MediaCategory = MediaCategory.Other;
aSettings.PhotoCaptureSource = PhotoCaptureSource.VideoPreview;
aSettings.StreamingCaptureMode = StreamingCaptureMode.AudioAndVideo;
//Create profile for MPEG-4 container which will have H.264 video.
myProfile = MediaEncodingProfile.CreateMp4(
Windows.Media.MediaProperties.VideoEncodingQuality.Qvga);
// Initialize the media capture with specified settings.
myMediaCapture = new MediaCapture();
await myMediaCapture.InitializeAsync(aSettings);
myIsRecording = false;
}
private void DeactivateCamera()
{
if (myMediaCapture != null)
{
if (myIsRecording)
{
// Note: Camera deactivation needs to run synchronous.
// Otherwise suspending/resuming during recording does not work.
myMediaCapture.StopRecordAsync().AsTask().Wait();
myIsRecording = false;
}
myMediaCapture.StopPreviewAsync().AsTask().Wait();
myMediaCapture.Dispose();
myMediaCapture = null;
}
if (myPreviewSink != null)
{
myPreviewSink.Dispose();
myPreviewSink = null;
}
ViewfinderRectangle.Fill = null;
if (myMessageSendingStream != null)
{
myMessageSendingStream.CloseConnection();
myMessageSendingStream.ConnectionBroken -= OnConnectionBroken;
}
}
private async void OnConnectionBroken(object sender, EventArgs e)
{
await StopRecordingAsync("Disconnected from server.");
}
private async Task StartPreviewAsync()
{
// List of supported video preview formats to be used by the default
// preview format selector.
var aSupportedVideoFormats = new List<string> { "nv12", "rgb32" };
// Find the supported preview format
var anAvailableMediaStreamProperties =
myMediaCapture.VideoDeviceController.GetAvailableMediaStreamProperties(
Windows.Media.Capture.MediaStreamType.VideoPreview)
.OfType<Windows.Media.MediaProperties.VideoEncodingProperties>()
.Where(p => p != null && !String.IsNullOrEmpty(p.Subtype)
&& aSupportedVideoFormats.Contains(p.Subtype.ToLower()))
.ToList();
var aPreviewFormat = anAvailableMediaStreamProperties.FirstOrDefault();
// Start Preview stream
myPreviewSink = new MediaCapturePreviewSink();
await myMediaCapture.VideoDeviceController.SetMediaStreamPropertiesAsync(
Windows.Media.Capture.MediaStreamType.VideoPreview, aPreviewFormat);
await myMediaCapture.StartPreviewToCustomSinkAsync(
new MediaEncodingProfile { Video = aPreviewFormat }, myPreviewSink);
// Create the VideoBrush for the viewfinder.
myVideoRecorderBrush = new VideoBrush();
Microsoft.Devices.CameraVideoBrushExtensions
.SetSource(myVideoRecorderBrush, myPreviewSink);
// Display video preview.
ViewfinderRectangle.Fill = myVideoRecorderBrush;
}
private async Task StartRecordingAsync()
{
// Disable Start Recoridng button.
StartRecordingBtn.IsEnabled = false;
try
{
// Connect the service.
// Note: use IP address of the service within your network.
// To figure out the IP address of you can execute from
// the command prompt: ipconfig -all
myMessageSendingStream =
new MessageSendingStream("tcp://192.168.178.31:8093/");
myMessageSendingStream.ConnectionBroken += OnConnectionBroken;
myMessageSendingStream.OpenConnection();
await myMediaCapture.StartRecordToStreamAsync(myProfile, myMessageSendingStream);
myIsRecording = true;
// Enable Stop Recording button.
StopRecordingBtn.IsEnabled = true;
txtDebug.Text = "Recording...";
}
catch (Exception err)
{
myMessageSendingStream.CloseConnection();
myMessageSendingStream.ConnectionBroken -= OnConnectionBroken;
txtDebug.Text = "ERROR: " + err.Message;
StartRecordingBtn.IsEnabled = true;
}
}
private async Task StopRecordingAsync(string textMessage)
{
// Note: Since this method does not have to be called from UI thread
// ensure UI controls are manipulated from the UI thread.
// Disable Stop Recording button.
ToUiThread(() => StopRecordingBtn.IsEnabled = false);
try
{
if (myIsRecording)
{
await myMediaCapture.StopRecordAsync();
myIsRecording = false;
}
// Enable Start Recording button and display Message.
ToUiThread(() =>
{
StartRecordingBtn.IsEnabled = true;
txtDebug.Text = textMessage;
});
}
catch (Exception err)
{
ToUiThread(() =>
{
txtDebug.Text = "ERROR: " + err.Message;
StopRecordingBtn.IsEnabled = true;
});
}
// Disconnect from the service.
myMessageSendingStream.CloseConnection();
myMessageSendingStream.ConnectionBroken -= OnConnectionBroken;
}
private void ToUiThread(Action x)
{
Dispatcher.BeginInvoke(x);
}
}
}
MessageSendingStream.cs 的实现也非常简单
using Eneter.Messaging.MessagingSystems.ConnectionProtocols;
using Eneter.Messaging.MessagingSystems.MessagingSystemBase;
using Eneter.Messaging.MessagingSystems.TcpMessagingSystem;
using System;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Storage.Streams;
namespace PhoneCamera
{
// Implements IRandomAccessStream which is then used by MediaCapture for storing
// captured video and audi data.
// The implementation of this class sends the captured data across the network
// to the service where it is stored to the file.
internal class MessageSendingStream : IRandomAccessStream
{
private IDuplexOutputChannel myOutputChannel;
private ulong myPosition; // Current position in the stream.
private ulong mySize; // The size of the stream.
// Raised when the connection with the service is broken.
public event EventHandler ConnectionBroken;
public MessageSendingStream(string serviceAddress)
{
// Let's use TCP for the communication with fast encoding of messages.
var aFormatter = new EasyProtocolFormatter();
var aMessaging = new TcpMessagingSystemFactory(aFormatter);
aMessaging.ConnectTimeout = TimeSpan.FromMilliseconds(3000);
myOutputChannel = aMessaging.CreateDuplexOutputChannel(serviceAddress);
}
public void OpenConnection()
{
myOutputChannel.OpenConnection();
}
public void CloseConnection()
{
myOutputChannel.CloseConnection();
}
public bool CanRead { get { return false; } }
public bool CanWrite { get { return true; } }
public IRandomAccessStream CloneStream()
{ throw new NotSupportedException(); }
public IInputStream GetInputStreamAt(ulong position)
{ throw new NotSupportedException(); }
public IOutputStream GetOutputStreamAt(ulong position)
{ throw new NotSupportedException(); }
public ulong Position { get { return myPosition; } }
public void Seek(ulong position)
{
myPosition = position;
if (myPosition >= mySize)
{
mySize = myPosition + 1;
}
}
public ulong Size
{
get { return mySize; }
set { throw new NotSupportedException(); }
}
public void Dispose()
{
myOutputChannel.CloseConnection();
}
public IAsyncOperationWithProgress<IBuffer, uint> ReadAsync(
IBuffer buffer, uint count, InputStreamOptions options)
{
throw new NotSupportedException();
}
public IAsyncOperation<bool> FlushAsync()
{
throw new NotSupportedException();
}
// Implements sending of video/audio data to the service.
// The message is encoded the following way:
// 4 bytes - position in MP4 file where data shall be put.
// n bytes - video/audio data.
public IAsyncOperationWithProgress<uint, uint> WriteAsync(IBuffer buffer)
{
Task<uint> aTask = new Task<uint>(() =>
{
uint aVideoDataLength = buffer.Length;
byte[] aMessage = new byte[aVideoDataLength + 4];
// Put position within MP4 file to the message.
byte[] aPosition = BitConverter.GetBytes((int)myPosition);
Array.Copy(aPosition, aMessage, aPosition.Length);
// Put video/audio data to the message.
buffer.CopyTo(0, aMessage, 4, (int)aVideoDataLength);
uint aTransferedSize = 0;
try
{
// Send the message to the service.
myOutputChannel.SendMessage(aMessage);
aTransferedSize = (uint)aVideoDataLength;
// Calculate new size of the stream.
if (myPosition + aVideoDataLength > mySize)
{
mySize = myPosition + aVideoDataLength;
}
}
catch
{
// If sending fails then the connection is broken.
if (ConnectionBroken != null)
{
ConnectionBroken(this, new EventArgs());
}
}
return aTransferedSize;
});
aTask.RunSynchronously();
Func<CancellationToken, IProgress<uint>, Task<uint>> aTaskProvider =
(token, progress) => aTask;
return AsyncInfo.Run<uint, uint>(aTaskProvider);
}
}
}
桌面服务
桌面服务是一个简单的控制台应用程序,它侦听指定的 IP 地址和端口。
当客户端连接到服务时,它会创建一个 MP4 文件并等待流消息。收到流消息时,它会将数据写入文件。
整个实现非常简单
using Eneter.Messaging.MessagingSystems.ConnectionProtocols;
using Eneter.Messaging.MessagingSystems.MessagingSystemBase;
using Eneter.Messaging.MessagingSystems.TcpMessagingSystem;
using System;
using System.Collections.Generic;
using System.IO;
namespace VideoStorageService
{
class Program
{
// The service can handle multiple clients.
// So this dictionary maintains open files per client.
private static Dictionary<string, FileStream> myActiveVideos =
new Dictionary<string, FileStream>();
static void Main(string[] args)
{
var aFastEncoding = new EasyProtocolFormatter();
var aMessaging = new TcpMessagingSystemFactory(aFastEncoding);
var anInputChannel = aMessaging.CreateDuplexInputChannel("tcp://192.168.178.31:8093/");
// Subscribe to handle incoming data.
anInputChannel.MessageReceived += OnMessageReceived;
// Subscribe to handle client connection/disconnection.
anInputChannel.ResponseReceiverConnected += OnResponseReceiverConnected;
anInputChannel.ResponseReceiverDisconnected += OnClientDisconnected;
// Start listening.
anInputChannel.StartListening();
Console.WriteLine("Videostorage service is running. Press ENTER to stop.");
Console.WriteLine("The service is listening at: " + anInputChannel.ChannelId);
Console.ReadLine();
// Stop listening.
// Note: it releases the listening thread.
anInputChannel.StopListening();
}
private static void OnResponseReceiverConnected(object sender, ResponseReceiverEventArgs e)
{
Console.WriteLine("Connected client: " + e.ResponseReceiverId);
StartStoring(e.ResponseReceiverId);
}
private static void OnClientDisconnected(object sender, ResponseReceiverEventArgs e)
{
Console.WriteLine("Disconnected client: " + e.ResponseReceiverId);
StopStoring(e.ResponseReceiverId);
}
private static void OnMessageReceived(object sender, DuplexChannelMessageEventArgs e)
{
byte[] aVideoData = (byte[])e.Message;
StoreVideoData(e.ResponseReceiverId, aVideoData);
}
private static void StartStoring(string clientId)
{
// Create MP4 file for the client.
string aFileName = "./" + Guid.NewGuid().ToString() + ".mp4";
myActiveVideos[clientId] =
new FileStream(aFileName, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None);
}
private static void StopStoring(string clientId)
{
// Close MP4 file for the client.
FileStream aFileStream;
myActiveVideos.TryGetValue(clientId, out aFileStream);
if (aFileStream != null)
{
aFileStream.Close();
}
myActiveVideos.Remove(clientId);
}
private static void StoreVideoData(string clientId, byte[] videoData)
{
try
{
// Get MP4 file which is open for the client.
FileStream aFileStream;
myActiveVideos.TryGetValue(clientId, out aFileStream);
if (aFileStream != null)
{
// From first 4 bytes decode position in MP4 file
// where to write the video data.
int aPosition = BitConverter.ToInt32(videoData, 0);
// Set the position in the file.
aFileStream.Seek(aPosition, SeekOrigin.Begin);
// Write data to the file.
aFileStream.Write(videoData, 4, videoData.Length - 4);
}
}
catch (Exception err)
{
Console.WriteLine(err);
}
}
}
}