diff --git a/README.md b/README.md index 63a03bc2..65acf582 100644 --- a/README.md +++ b/README.md @@ -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** diff --git a/package.json b/package.json index 38d798e0..fb7bde6e 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/windows/ReactNativeVideo.Net46/Properties/AssemblyInfo.cs b/windows/ReactNativeVideo.Net46/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..ca102d8a --- /dev/null +++ b/windows/ReactNativeVideo.Net46/Properties/AssemblyInfo.cs @@ -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")] diff --git a/windows/ReactNativeVideo.Net46/ReactNativeVideo.Net46.csproj b/windows/ReactNativeVideo.Net46/ReactNativeVideo.Net46.csproj new file mode 100644 index 00000000..df351c95 --- /dev/null +++ b/windows/ReactNativeVideo.Net46/ReactNativeVideo.Net46.csproj @@ -0,0 +1,74 @@ + + + + + Debug + AnyCPU + {2D8406AB-0674-42D3-8FE3-41D251403DF8} + Library + Properties + ReactNativeVideo.Net46 + ReactNativeVideo.Net46 + v4.6 + 512 + + + x64 + bin\x64\Debug\ + + + x64 + bin\x64\Release\ + + + x86 + bin\x86\Debug\ + + + x86 + bin\x86\Release\ + + + + $(SolutionDir)\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + {22CBFF9C-FE36-43E8-A246-266C7635E662} + ReactNative.Net46 + + + + + + + \ No newline at end of file diff --git a/windows/ReactNativeVideo.Net46/ReactVideoEventType.cs b/windows/ReactNativeVideo.Net46/ReactVideoEventType.cs new file mode 100644 index 00000000..dd3f3feb --- /dev/null +++ b/windows/ReactNativeVideo.Net46/ReactVideoEventType.cs @@ -0,0 +1,15 @@ +namespace ReactNativeVideo +{ + enum ReactVideoEventType + { + LoadStart, + Load, + Error, + Progress, + Seek, + End, + Stalled, + Resume, + ReadyForDisplay, + } +} diff --git a/windows/ReactNativeVideo.Net46/ReactVideoEventTypeExtensions.cs b/windows/ReactNativeVideo.Net46/ReactVideoEventTypeExtensions.cs new file mode 100644 index 00000000..5f2b63db --- /dev/null +++ b/windows/ReactNativeVideo.Net46/ReactVideoEventTypeExtensions.cs @@ -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}'.")); + } + } + } +} diff --git a/windows/ReactNativeVideo.Net46/ReactVideoPackage.cs b/windows/ReactNativeVideo.Net46/ReactVideoPackage.cs new file mode 100644 index 00000000..b8cc654b --- /dev/null +++ b/windows/ReactNativeVideo.Net46/ReactVideoPackage.cs @@ -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 CreateJavaScriptModulesConfig() + { + return Array.Empty(); + } + + public IReadOnlyList CreateNativeModules(ReactContext reactContext) + { + return Array.Empty(); + + } + + public IReadOnlyList CreateViewManagers(ReactContext reactContext) + { + return new List + { + new ReactVideoViewManager(), + }; + } + } +} diff --git a/windows/ReactNativeVideo.Net46/ReactVideoView.cs b/windows/ReactNativeVideo.Net46/ReactVideoView.cs new file mode 100644 index 00000000..8a8b1b29 --- /dev/null +++ b/windows/ReactNativeVideo.Net46/ReactVideoView.cs @@ -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() + .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() + .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() + .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() + .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() + .EventDispatcher + .DispatchEvent( + new ReactVideoEvent( + ReactVideoEventType.End.GetEventName(), + this.GetTag(), + null)); + } + } + + private void OnBufferingStarted(object sender, EventArgs args) + { + this.GetReactContext() + .GetNativeModule() + .EventDispatcher + .DispatchEvent( + new ReactVideoEvent( + ReactVideoEventType.Stalled.GetEventName(), + this.GetTag(), + new JObject())); + } + + private void OnBufferingEnded(object sender, EventArgs args) + { + this.GetReactContext() + .GetNativeModule() + .EventDispatcher + .DispatchEvent( + new ReactVideoEvent( + ReactVideoEventType.Resume.GetEventName(), + this.GetTag(), + new JObject())); + } + + private void OnSeekCompleted(object sender, EventArgs args) + { + this.GetReactContext() + .GetNativeModule() + .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); + } + } + } +} diff --git a/windows/ReactNativeVideo.Net46/ReactVideoViewManager.cs b/windows/ReactNativeVideo.Net46/ReactVideoViewManager.cs new file mode 100644 index 00000000..e71f22c1 --- /dev/null +++ b/windows/ReactNativeVideo.Net46/ReactVideoViewManager.cs @@ -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 + { + public override string Name + { + get + { + return "RCTVideo"; + } + } + + public override IReadOnlyDictionary ExportedViewConstants + { + get + { + return new Dictionary + { + { "ScaleNone", ((int)Stretch.None).ToString() }, + { "ScaleToFill", ((int)Stretch.UniformToFill).ToString() }, + { "ScaleAspectFit", ((int)Stretch.Uniform).ToString() }, + { "ScaleAspectFill", ((int)Stretch.Fill).ToString() }, + }; + } + } + + public override IReadOnlyDictionary ExportedCustomDirectEventTypeConstants + { + get + { + var events = new Dictionary(); + var eventTypes = Enum.GetValues(typeof(ReactVideoEventType)).OfType(); + foreach (var eventType in eventTypes) + { + events.Add(eventType.GetEventName(), new Dictionary + { + { "registrationName", eventType.GetEventName() }, + }); + } + + return events; + } + } + + [ReactProp("src")] + public void SetSource(ReactVideoView view, JObject source) + { + view.Source = source.Value("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, + }; + } + } +} diff --git a/windows/ReactNativeVideo.Net46/packages.config b/windows/ReactNativeVideo.Net46/packages.config new file mode 100644 index 00000000..90aba300 --- /dev/null +++ b/windows/ReactNativeVideo.Net46/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/windows/ReactNativeVideo.sln b/windows/ReactNativeVideo.sln new file mode 100644 index 00000000..fe86ec07 --- /dev/null +++ b/windows/ReactNativeVideo.sln @@ -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