Initial WPF Support (#385)

* initial support for WPF

* update readme with WPF specific instructions

* remove autogenerated .gitattributes file

* reference RNW NPM package instead of hard-coded local reference
This commit is contained in:
Kevin VanGelder 2017-03-31 09:15:26 -07:00 committed by Matt Apperson
parent d48d7efc5d
commit ebc6617ba4
11 changed files with 750 additions and 3 deletions

View File

@ -80,14 +80,18 @@ Add the `ReactNativeVideo` project to your solution.
1. Open the solution in Visual Studio 2015
2. Right-click Solution icon in Solution Explorer > Add > Existing Project...
3. Select `node_modules\react-native-video\windows\ReactNativeVideo\ReactNativeVideo.csproj`
3.
UWP: Select `node_modules\react-native-video\windows\ReactNativeVideo\ReactNativeVideo.csproj`
WPF: Select `node_modules\react-native-video\windows\ReactNativeVideo.Net46\ReactNativeVideo.Net46.csproj`
**windows/myapp/myapp.csproj**
Add a reference to `ReactNativeVideo` to your main application project. From Visual Studio 2015:
1. Right-click main application project > Add > Reference...
2. Check `ReactNativeVideo` from Solution Projects.
2.
UWP: Check `ReactNativeVideo` from Solution Projects.
WPF: Check `ReactNativeVideo.Net46` from Solution Projects.
**MainPage.cs**

View File

@ -27,7 +27,8 @@
"eslint-config-airbnb": "4.0.0"
},
"dependencies": {
"keymirror": "0.1.1"
"keymirror": "0.1.1",
"react-native-windows": "0.41.0-rc.0"
},
"scripts": {
"test": "node_modules/.bin/eslint *.js"

View File

@ -0,0 +1,36 @@
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("ReactNativeVideo.Net46")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ReactNativeVideo.Net46")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[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("2d8406ab-0674-42d3-8fe3-41d251403df8")]
// 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 Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{2D8406AB-0674-42D3-8FE3-41D251403DF8}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ReactNativeVideo.Net46</RootNamespace>
<AssemblyName>ReactNativeVideo.Net46</AssemblyName>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>bin\x64\Debug\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>bin\x64\Release\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Debug\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Release\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xaml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReactVideoEventType.cs" />
<Compile Include="ReactVideoEventTypeExtensions.cs" />
<Compile Include="ReactVideoPackage.cs" />
<Compile Include="ReactVideoView.cs" />
<Compile Include="ReactVideoViewManager.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\..\node_modules\react-native-windows\ReactWindows\ReactNative.Net46\ReactNative.Net46.csproj">
<Project>{22CBFF9C-FE36-43E8-A246-266C7635E662}</Project>
<Name>ReactNative.Net46</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@ -0,0 +1,15 @@
namespace ReactNativeVideo
{
enum ReactVideoEventType
{
LoadStart,
Load,
Error,
Progress,
Seek,
End,
Stalled,
Resume,
ReadyForDisplay,
}
}

View File

@ -0,0 +1,36 @@
using System;
using static System.FormattableString;
namespace ReactNativeVideo
{
static class ReactVideoEventTypeExtensions
{
public static string GetEventName(this ReactVideoEventType eventType)
{
switch (eventType)
{
case ReactVideoEventType.LoadStart:
return "onVideoLoadStart";
case ReactVideoEventType.Load:
return "onVideoLoad";
case ReactVideoEventType.Error:
return "onVideoError";
case ReactVideoEventType.Progress:
return "onVideoProgress";
case ReactVideoEventType.Seek:
return "onVideoSeek";
case ReactVideoEventType.End:
return "onVideoEnd";
case ReactVideoEventType.Stalled:
return "onPlaybackStalled";
case ReactVideoEventType.Resume:
return "onPlaybackResume";
case ReactVideoEventType.ReadyForDisplay:
return "onReadyForDisplay";
default:
throw new NotSupportedException(
Invariant($"No event name added for event type '{eventType}'."));
}
}
}
}

View File

@ -0,0 +1,30 @@
using ReactNative.Bridge;
using ReactNative.Modules.Core;
using ReactNative.UIManager;
using System;
using System.Collections.Generic;
namespace ReactNativeVideo
{
public class ReactVideoPackage : IReactPackage
{
public IReadOnlyList<Type> CreateJavaScriptModulesConfig()
{
return Array.Empty<Type>();
}
public IReadOnlyList<INativeModule> CreateNativeModules(ReactContext reactContext)
{
return Array.Empty<INativeModule>();
}
public IReadOnlyList<IViewManager> CreateViewManagers(ReactContext reactContext)
{
return new List<IViewManager>
{
new ReactVideoViewManager(),
};
}
}
}

View File

@ -0,0 +1,359 @@
using Newtonsoft.Json.Linq;
using ReactNative.Bridge;
using ReactNative.UIManager;
using ReactNative.UIManager.Events;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace ReactNativeVideo
{
class ReactVideoView : Border, IDisposable
{
public const string EVENT_PROP_SEEK_TIME = "seekTime";
private readonly DispatcherTimer _timer;
private bool _isLoopingEnabled;
private bool _isPaused;
private bool _isMuted;
private bool _isCompleted;
private double _volume;
private double _rate;
private MediaPlayer _player;
private VideoDrawing _drawing;
private MediaTimeline _timeline;
private MediaClock _clock;
private DrawingBrush _brush;
public ReactVideoView()
{
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromMilliseconds(250.0);
_timer.Start();
_player = new MediaPlayer();
_drawing = new VideoDrawing();
_drawing.Rect = new Rect(0, 0, 100, 100); // Set the initial viewing area
_drawing.Player = _player;
_brush = new DrawingBrush(_drawing);
this.Background = _brush;
}
public string Source
{
set
{
var uri = new Uri(value);
_player.Open(uri);
this.GetReactContext()
.GetNativeModule<UIManagerModule>()
.EventDispatcher
.DispatchEvent(
new ReactVideoEvent(
ReactVideoEventType.LoadStart.GetEventName(),
this.GetTag(),
new JObject
{
{"src", uri}
}));
ApplyModifiers();
SubscribeEvents();
}
}
public bool IsLoopingEnabled
{
set
{
_isLoopingEnabled = value;
}
}
public bool IsMuted
{
set
{
_isMuted = value;
if (_player != null)
{
_player.IsMuted = _isMuted;
}
}
}
public bool IsPaused
{
set
{
_isPaused = value;
if (_player != null)
{
if (_isPaused)
{
_player.Pause();
}
else
{
_player.Play();
}
}
}
}
public double Volume
{
set
{
_volume = value;
if (_player != null)
{
_player.Volume = _volume;
}
}
}
public double Rate
{
set
{
_rate = value;
if (_player != null)
{
_player.SpeedRatio = _rate;
}
}
}
public double ProgressUpdateInterval
{
set
{
_timer.Interval = TimeSpan.FromSeconds(value);
}
}
public void Seek(double seek)
{
if (_player != null)
{
_player.Position = TimeSpan.FromSeconds(seek);
}
}
public void Dispose()
{
if (_player != null)
{
_timer.Tick -= OnTick;
_player.MediaOpened -= OnMediaOpened;
_player.MediaFailed -= OnMediaFailed;
_player.MediaEnded -= OnMediaEnded;
_player.BufferingStarted -= OnBufferingStarted;
_player.BufferingEnded -= OnBufferingEnded;
// _player.SeekCompleted -= OnSeekCompleted;
}
_timer.Stop();
}
private void ApplyModifiers()
{
IsLoopingEnabled = _isLoopingEnabled;
IsMuted = _isMuted;
IsPaused = _isPaused;
Volume = _volume;
Rate = _rate;
}
private void SubscribeEvents()
{
_timer.Tick += OnTick;
_player.MediaOpened += OnMediaOpened;
_player.MediaFailed += OnMediaFailed;
_player.MediaEnded += OnMediaEnded;
_player.BufferingStarted += OnBufferingStarted;
_player.BufferingEnded += OnBufferingEnded;
//_player.SeekCompleted += OnSeekCompleted;
}
private void OnTick(object sender, object e)
{
if (_player != null && !_isCompleted && !_isPaused)
{
this.GetReactContext()
.GetNativeModule<UIManagerModule>()
.EventDispatcher
.DispatchEvent(
new ReactVideoEvent(
ReactVideoEventType.Progress.GetEventName(),
this.GetTag(),
new JObject
{
{ "currentTime", _player.Position.TotalSeconds },
{ "playableDuration", 0.0 /* TODO */ }
}));
}
}
private void OnMediaOpened(object sender, EventArgs args)
{
RunOnDispatcher(delegate
{
var width = _player.NaturalVideoWidth;
var height = _player.NaturalVideoHeight;
var orientation = (width > height) ? "landscape" : "portrait";
var size = new JObject
{
{ "width", width },
{ "height", height },
{ "orientation", orientation }
};
_drawing.Rect = new Rect(new Size(width, height));
this.GetReactContext()
.GetNativeModule<UIManagerModule>()
.EventDispatcher
.DispatchEvent(
new ReactVideoEvent(
ReactVideoEventType.Load.GetEventName(),
this.GetTag(),
new JObject
{
{ "duration", _player.NaturalDuration.TimeSpan.TotalSeconds },
{ "currentTime", _player.Position.TotalSeconds },
{ "naturalSize", size },
{ "canPlayFastForward", false },
{ "canPlaySlowForward", false },
{ "canPlaySlow", false },
{ "canPlayReverse", false },
{ "canStepBackward", false },
{ "canStepForward", false }
}));
});
}
private void OnMediaFailed(object sender, ExceptionEventArgs args)
{
var errorData = new JObject
{
{ "what", args.ErrorException.HResult.ToString() },
{ "extra", args.ErrorException.Message }
};
this.GetReactContext()
.GetNativeModule<UIManagerModule>()
.EventDispatcher
.DispatchEvent(
new ReactVideoEvent(
ReactVideoEventType.Error.GetEventName(),
this.GetTag(),
new JObject
{
{ "error", errorData }
}));
}
private void OnMediaEnded(object sender, EventArgs args)
{
if (_isLoopingEnabled)
{
_player.Position = TimeSpan.Zero;
_player.Play();
}
else
{
_isCompleted = true;
this.GetReactContext()
.GetNativeModule<UIManagerModule>()
.EventDispatcher
.DispatchEvent(
new ReactVideoEvent(
ReactVideoEventType.End.GetEventName(),
this.GetTag(),
null));
}
}
private void OnBufferingStarted(object sender, EventArgs args)
{
this.GetReactContext()
.GetNativeModule<UIManagerModule>()
.EventDispatcher
.DispatchEvent(
new ReactVideoEvent(
ReactVideoEventType.Stalled.GetEventName(),
this.GetTag(),
new JObject()));
}
private void OnBufferingEnded(object sender, EventArgs args)
{
this.GetReactContext()
.GetNativeModule<UIManagerModule>()
.EventDispatcher
.DispatchEvent(
new ReactVideoEvent(
ReactVideoEventType.Resume.GetEventName(),
this.GetTag(),
new JObject()));
}
private void OnSeekCompleted(object sender, EventArgs args)
{
this.GetReactContext()
.GetNativeModule<UIManagerModule>()
.EventDispatcher.DispatchEvent(
new ReactVideoEvent(
ReactVideoEventType.Seek.GetEventName(),
this.GetTag(),
new JObject()));
}
private static async void RunOnDispatcher(Action action)
{
await Application.Current.Dispatcher.InvokeAsync(action).Task.ConfigureAwait(false);
}
class ReactVideoEvent : Event
{
private readonly string _eventName;
private readonly JObject _eventData;
public ReactVideoEvent(string eventName, int viewTag, JObject eventData)
: base(viewTag, TimeSpan.FromTicks(Environment.TickCount))
{
_eventName = eventName;
_eventData = eventData;
}
public override string EventName
{
get
{
return _eventName;
}
}
public override bool CanCoalesce
{
get
{
return false;
}
}
public override void Dispatch(RCTEventEmitter eventEmitter)
{
eventEmitter.receiveEvent(ViewTag, EventName, _eventData);
}
}
}
}

View File

@ -0,0 +1,139 @@
using Newtonsoft.Json.Linq;
using ReactNative.UIManager;
using ReactNative.UIManager.Annotations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
namespace ReactNativeVideo
{
class ReactVideoViewManager : SimpleViewManager<ReactVideoView>
{
public override string Name
{
get
{
return "RCTVideo";
}
}
public override IReadOnlyDictionary<string, object> ExportedViewConstants
{
get
{
return new Dictionary<string, object>
{
{ "ScaleNone", ((int)Stretch.None).ToString() },
{ "ScaleToFill", ((int)Stretch.UniformToFill).ToString() },
{ "ScaleAspectFit", ((int)Stretch.Uniform).ToString() },
{ "ScaleAspectFill", ((int)Stretch.Fill).ToString() },
};
}
}
public override IReadOnlyDictionary<string, object> ExportedCustomDirectEventTypeConstants
{
get
{
var events = new Dictionary<string, object>();
var eventTypes = Enum.GetValues(typeof(ReactVideoEventType)).OfType<ReactVideoEventType>();
foreach (var eventType in eventTypes)
{
events.Add(eventType.GetEventName(), new Dictionary<string, object>
{
{ "registrationName", eventType.GetEventName() },
});
}
return events;
}
}
[ReactProp("src")]
public void SetSource(ReactVideoView view, JObject source)
{
view.Source = source.Value<string>("uri");
}
[ReactProp("resizeMode")]
public void SetResizeMode(ReactVideoView view, string resizeMode)
{
throw new NotImplementedException("Resize Mode has not been implemented for WPF.");
// view.Stretch = (Stretch)int.Parse(resizeMode);
}
[ReactProp("repeat")]
public void SetRepeat(ReactVideoView view, bool repeat)
{
view.IsLoopingEnabled = repeat;
}
[ReactProp("paused")]
public void SetPaused(ReactVideoView view, bool paused)
{
view.IsPaused = paused;
}
[ReactProp("muted")]
public void SetMuted(ReactVideoView view, bool muted)
{
view.IsMuted = muted;
}
[ReactProp("volume", DefaultDouble = 1.0)]
public void SetVolume(ReactVideoView view, double volume)
{
view.Volume = volume;
}
[ReactProp("seek")]
public void SetSeek(ReactVideoView view, double? seek)
{
if (seek.HasValue)
{
view.Seek(seek.Value);
}
}
[ReactProp("rate", DefaultDouble = 1.0)]
public void SetPlaybackRate(ReactVideoView view, double rate)
{
view.Rate = rate;
}
[ReactProp("playInBackground")]
public void SetPlayInBackground(ReactVideoView view, bool playInBackground)
{
throw new NotImplementedException("Play in background has not been implemented on Windows.");
}
// TODO: Utilize MediaElement when user control enabled and MediaPlayer + VideoDrawing when disabled
[ReactProp("controls")]
public void SetControls(ReactVideoView view, bool controls)
{
throw new NotImplementedException("User controls have not been implemented on WPF.");
}
[ReactProp("progressUpdateInterval")]
public void SetProgressUpdateInterval(ReactVideoView view, double progressUpdateInterval)
{
view.ProgressUpdateInterval = progressUpdateInterval;
}
public override void OnDropViewInstance(ThemedReactContext reactContext, ReactVideoView view)
{
base.OnDropViewInstance(reactContext, view);
view.Dispose();
}
protected override ReactVideoView CreateViewInstance(ThemedReactContext reactContext)
{
return new ReactVideoView
{
HorizontalAlignment = HorizontalAlignment.Stretch,
};
}
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net46" />
</packages>

View File

@ -0,0 +1,49 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactNativeVideo.Net46", "ReactNativeVideo.Net46\ReactNativeVideo.Net46.csproj", "{2D8406AB-0674-42D3-8FE3-41D251403DF8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactNative.Net46", "..\node_modules\react-native-windows\ReactWindows\ReactNative.Net46\ReactNative.Net46.csproj", "{22CBFF9C-FE36-43E8-A246-266C7635E662}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
..\node_modules\react-native-windows\ReactWindows\ReactNative.Shared\ReactNative.Shared.projitems*{22cbff9c-fe36-43e8-a246-266c7635e662}*SharedItemsImports = 4
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM = Debug|ARM
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|ARM = Release|ARM
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2D8406AB-0674-42D3-8FE3-41D251403DF8}.Debug|ARM.ActiveCfg = Debug|x86
{2D8406AB-0674-42D3-8FE3-41D251403DF8}.Debug|x64.ActiveCfg = Debug|x64
{2D8406AB-0674-42D3-8FE3-41D251403DF8}.Debug|x64.Build.0 = Debug|x64
{2D8406AB-0674-42D3-8FE3-41D251403DF8}.Debug|x86.ActiveCfg = Debug|x64
{2D8406AB-0674-42D3-8FE3-41D251403DF8}.Debug|x86.Build.0 = Debug|x64
{2D8406AB-0674-42D3-8FE3-41D251403DF8}.Release|ARM.ActiveCfg = Release|x86
{2D8406AB-0674-42D3-8FE3-41D251403DF8}.Release|x64.ActiveCfg = Release|x64
{2D8406AB-0674-42D3-8FE3-41D251403DF8}.Release|x64.Build.0 = Release|x64
{2D8406AB-0674-42D3-8FE3-41D251403DF8}.Release|x86.ActiveCfg = Release|x86
{2D8406AB-0674-42D3-8FE3-41D251403DF8}.Release|x86.Build.0 = Release|x86
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|ARM.ActiveCfg = Debug|ARM
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|ARM.Build.0 = Debug|ARM
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|x64.ActiveCfg = Debug|x64
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|x64.Build.0 = Debug|x64
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|x86.ActiveCfg = Debug|x86
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|x86.Build.0 = Debug|x86
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|ARM.ActiveCfg = Release|ARM
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|ARM.Build.0 = Release|ARM
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|x64.ActiveCfg = Release|x64
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|x64.Build.0 = Release|x64
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|x86.ActiveCfg = Release|x86
{22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal