1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-05-09 23:34:06 +02:00

Add WIP video player for MP4s

This commit is contained in:
Daniel Chýlek 2017-08-11 13:27:15 +02:00 committed by GitHub
commit 58cc7ea10d
13 changed files with 537 additions and 11 deletions

View File

@ -114,6 +114,10 @@ public void ScreenshotTweet(string html, int width, int height){
form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width, height));
}
public void PlayVideo(string url){
form.InvokeAsyncSafe(() => form.PlayVideo(url));
}
public void FixClipboard(){
form.InvokeAsyncSafe(WindowsUtils.ClipboardStripHtmlStyles);
}

View File

@ -12,6 +12,7 @@
using TweetDuck.Core.Notification;
using TweetDuck.Core.Notification.Screenshot;
using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Media;
using TweetDuck.Core.Other.Settings;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
@ -58,6 +59,7 @@ public bool IsWaiting{
private TweetScreenshotManager notificationScreenshotManager;
private SoundNotification soundNotification;
private VideoPlayer videoPlayer;
public FormBrowser(PluginManager pluginManager, UpdaterSettings updaterSettings){
InitializeComponent();
@ -506,6 +508,19 @@ public void PlayNotificationSound(){
soundNotification.Play(Config.NotificationSoundPath);
}
public void PlayVideo(string url){
if (videoPlayer == null){
videoPlayer = new VideoPlayer(this);
}
if (!string.IsNullOrEmpty(url)){
videoPlayer.Launch(url);
}
else{
videoPlayer.Close();
}
}
public void OnTweetScreenshotReady(string html, int width, int height){
if (notificationScreenshotManager == null){
notificationScreenshotManager = new TweetScreenshotManager(this, plugins);

View File

@ -0,0 +1,70 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
namespace TweetDuck.Core.Other.Media{
class VideoPlayer{
private readonly string PlayerExe = Path.Combine(Program.ProgramPath, "TweetDuck.Video.exe");
private readonly Form owner;
private Process currentProcess;
public VideoPlayer(Form owner){
this.owner = owner;
}
public void Launch(string url){
Close();
try{
if ((currentProcess = Process.Start(new ProcessStartInfo{
FileName = PlayerExe,
Arguments = $"{owner.Handle} \"{url}\"",
UseShellExecute = false,
RedirectStandardOutput = true
})) != null){
currentProcess.EnableRaisingEvents = true;
currentProcess.Exited += process_Exited;
#if DEBUG
currentProcess.BeginOutputReadLine();
currentProcess.OutputDataReceived += (sender, args) => Debug.WriteLine("VideoPlayer: "+args.Data);
#endif
}
}catch(Exception e){
Program.Reporter.HandleException("Video Playback Error", "Error launching video player.", true, e);
}
}
public void Close(){
if (currentProcess != null){
currentProcess.Exited -= process_Exited;
try{
currentProcess.Kill();
}catch{
// kill me instead then
}
currentProcess.Dispose();
currentProcess = null;
}
}
private void process_Exited(object sender, EventArgs e){
switch(currentProcess.ExitCode){
case 2: // CODE_LAUNCH_FAIL
// TODO
break;
case 3: // CODE_MEDIA_ERROR
// TODO
break;
}
currentProcess.Dispose();
currentProcess = null;
}
}
}

View File

@ -789,20 +789,52 @@
}
//
// Block: Setup unsupported video element hook.
// Block: Setup video player hooks.
//
(function(){
var cancelModal = false;
var playVideo = function(url){
$('<div class="ovl" style="display:block"></div>').click(function(){
$TD.playVideo(null);
$(this).remove();
}).appendTo(app);
$TD.playVideo(url);
};
app.delegate(".js-gif-play", "click", function(e){
let src = $(this).closest(".js-media-gif-container").find("video").attr("src");
if (src){
playVideo(src);
}
else{
let parent = $(e.target).closest(".js-tweet").first();
let link = (parent.hasClass("tweet-detail") ? parent.find("a[rel='url']") : parent.find("time").first().children("a")).first();
$TD.openBrowser(link.attr("href"));
}
e.stopPropagation();
});
if (!ensurePropertyExists(TD, "components", "MediaGallery", "prototype", "_loadTweet") ||
!ensurePropertyExists(TD, "components", "BaseModal", "prototype", "setAndShowContainer") ||
!ensurePropertyExists(TD, "ui", "Column", "prototype", "playGifIfNotManuallyPaused"))return;
var cancelModal = false;
TD.components.MediaGallery.prototype._loadTweet = appendToFunction(TD.components.MediaGallery.prototype._loadTweet, function(){
let media = this.chirp.getMedia().find(media => media.mediaId === this.clickedMediaEntityId);
if (media && media.isVideo && media.service !== "youtube"){
$TD.openBrowser(this.clickedLink);
let data = media.chooseVideoVariant();
if (data.content_type === "video/mp4"){
playVideo(data.url);
}
else{
$TD.openBrowser(this.clickedLink);
}
cancelModal = true;
}
});
@ -816,14 +848,6 @@
TD.ui.Column.prototype.playGifIfNotManuallyPaused = function(){};
TD.mustaches["status/media_thumb.mustache"] = TD.mustaches["status/media_thumb.mustache"].replace("is-gif", "is-gif is-paused");
app.delegate(".js-gif-play", "click", function(e){
let parent = $(e.target).closest(".js-tweet").first();
let link = (parent.hasClass("tweet-detail") ? parent.find("a[rel='url']") : parent.find("time").first().children("a")).first();
$TD.openBrowser(link.attr("href"));
e.stopPropagation();
});
})();
//

View File

@ -142,6 +142,7 @@
<Compile Include="Core\Other\FormPlugins.Designer.cs">
<DependentUpon>FormPlugins.cs</DependentUpon>
</Compile>
<Compile Include="Core\Other\Media\VideoPlayer.cs" />
<Compile Include="Core\Other\Settings\Dialogs\DialogSettingsCSS.cs">
<SubType>Form</SubType>
</Compile>
@ -345,6 +346,10 @@
<Project>{b10b0017-819e-4f71-870f-8256b36a26aa}</Project>
<Name>TweetDuck.Browser</Name>
</ProjectReference>
<ProjectReference Include="video\TweetDuck.Video\TweetDuck.Video.csproj">
<Project>{278b2d11-402d-44b6-b6a1-8fa67db65565}</Project>
<Name>TweetDuck.Video</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>

View File

@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "tests\UnitTest
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Audio", "lib\TweetLib.Audio\TweetLib.Audio.csproj", "{E9E1FD1B-F480-45B7-9970-BE2ECFD309AC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Video", "video\TweetDuck.Video\TweetDuck.Video.csproj", "{278B2D11-402D-44B6-B6A1-8FA67DB65565}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
@ -32,6 +34,10 @@ Global
{E9E1FD1B-F480-45B7-9970-BE2ECFD309AC}.Debug|x86.Build.0 = Debug|x86
{E9E1FD1B-F480-45B7-9970-BE2ECFD309AC}.Release|x86.ActiveCfg = Release|x86
{E9E1FD1B-F480-45B7-9970-BE2ECFD309AC}.Release|x86.Build.0 = Release|x86
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Debug|x86.ActiveCfg = Debug|x86
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Debug|x86.Build.0 = Debug|x86
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Release|x86.ActiveCfg = Release|x86
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -0,0 +1,17 @@
using System.ComponentModel;
using System.Windows.Forms;
using WMPLib;
namespace TweetDuck.Video{
[DesignTimeVisible(true)]
[Clsid("{6bf52a52-394a-11d3-b153-00c04f79faa6}")]
class ControlWMP : AxHost{
public WindowsMediaPlayer Ocx { get; private set; }
public ControlWMP() : base("6bf52a52-394a-11d3-b153-00c04f79faa6"){}
protected override void AttachInterfaces(){
Ocx = (WindowsMediaPlayer)GetOcx();
}
}
}

View File

@ -0,0 +1,96 @@
namespace TweetDuck.Video {
partial class FormPlayer {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
this.components = new System.ComponentModel.Container();
this.player = new TweetDuck.Video.ControlWMP();
this.timer = new System.Windows.Forms.Timer(this.components);
this.trackBarVolume = new System.Windows.Forms.TrackBar();
((System.ComponentModel.ISupportInitialize)(this.player)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.trackBarVolume)).BeginInit();
this.SuspendLayout();
//
// player
//
this.player.Dock = System.Windows.Forms.DockStyle.Fill;
this.player.Enabled = true;
this.player.Location = new System.Drawing.Point(0, 0);
this.player.Name = "player";
this.player.Size = new System.Drawing.Size(236, 120);
this.player.TabIndex = 0;
//
// timer
//
this.timer.Interval = 10;
this.timer.Tick += new System.EventHandler(this.timer_Tick);
//
// trackBarVolume
//
this.trackBarVolume.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.trackBarVolume.AutoSize = false;
this.trackBarVolume.BackColor = System.Drawing.SystemColors.Control;
this.trackBarVolume.Location = new System.Drawing.Point(72, 94);
this.trackBarVolume.Maximum = 100;
this.trackBarVolume.Name = "trackBarVolume";
this.trackBarVolume.Size = new System.Drawing.Size(164, 26);
this.trackBarVolume.SmallChange = 5;
this.trackBarVolume.TabIndex = 1;
this.trackBarVolume.TickFrequency = 10;
this.trackBarVolume.TickStyle = System.Windows.Forms.TickStyle.None;
this.trackBarVolume.Visible = false;
this.trackBarVolume.ValueChanged += new System.EventHandler(this.trackBarVolume_ValueChanged);
this.trackBarVolume.MouseUp += new System.Windows.Forms.MouseEventHandler(this.trackBarVolume_MouseUp);
//
// FormPlayer
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(236, 120);
this.ControlBox = false;
this.Controls.Add(this.trackBarVolume);
this.Controls.Add(this.player);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.Location = new System.Drawing.Point(-32000, -32000);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "FormPlayer";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.Manual;
this.Text = "TweetDuck Video";
this.Load += new System.EventHandler(this.FormPlayer_Load);
((System.ComponentModel.ISupportInitialize)(this.player)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.trackBarVolume)).EndInit();
this.ResumeLayout(false);
}
#endregion
private ControlWMP player;
private System.Windows.Forms.Timer timer;
private System.Windows.Forms.TrackBar trackBarVolume;
}
}

View File

@ -0,0 +1,111 @@
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using WMPLib;
namespace TweetDuck.Video{
partial class FormPlayer : Form{
private readonly IntPtr ownerHandle;
private readonly string videoUrl;
private bool isPaused;
public FormPlayer(IntPtr handle, string url){
InitializeComponent();
this.ownerHandle = handle;
this.videoUrl = url;
player.Ocx.enableContextMenu = false;
player.Ocx.uiMode = "none";
player.Ocx.settings.setMode("loop", true);
player.Ocx.MediaChange += player_MediaChange;
player.Ocx.MediaError += player_MediaError;
trackBarVolume.Value = 25; // changes player volume
Application.AddMessageFilter(new MessageFilter(this));
}
// Events
private void FormPlayer_Load(object sender, EventArgs e){
player.Ocx.URL = videoUrl;
}
private void timer_Tick(object sender, EventArgs e){
if (NativeMethods.GetWindowRect(ownerHandle, out NativeMethods.RECT rect)){
int width = rect.Right-rect.Left+1;
int height = rect.Bottom-rect.Top+1;
IWMPMedia media = player.Ocx.currentMedia;
ClientSize = new Size(Math.Min(media.imageSourceWidth, width*3/4), Math.Min(media.imageSourceHeight, height*3/4));
Location = new Point(rect.Left+(width-ClientSize.Width)/2, rect.Top+(height-ClientSize.Height+SystemInformation.CaptionHeight)/2);
trackBarVolume.Visible = ClientRectangle.Contains(PointToClient(Cursor.Position)) || trackBarVolume.Focused;
}
else{
Environment.Exit(Program.CODE_OWNER_GONE);
}
}
private void player_MediaChange(object item){
timer.Start();
Cursor.Current = Cursors.Default;
NativeMethods.SetWindowOwner(Handle, ownerHandle);
Marshal.ReleaseComObject(item);
}
private void player_MediaError(object pMediaObject){
Marshal.ReleaseComObject(pMediaObject);
Environment.Exit(Program.CODE_MEDIA_ERROR);
}
private void trackBarVolume_ValueChanged(object sender, EventArgs e){
player.Ocx.settings.volume = trackBarVolume.Value;
}
private void trackBarVolume_MouseUp(object sender, MouseEventArgs e){
player.Focus();
}
// Controls & messages
private void TogglePause(){
if (isPaused){
player.Ocx.controls.play();
}
else{
player.Ocx.controls.pause();
}
isPaused = !isPaused;
}
internal class MessageFilter : IMessageFilter{
private readonly FormPlayer form;
public MessageFilter(FormPlayer form){
this.form = form;
}
bool IMessageFilter.PreFilterMessage(ref Message m){
if (m.Msg == 0x0201){ // WM_LBUTTONDOWN
Point cursor = form.PointToClient(Cursor.Position);
if (!(cursor.X >= form.trackBarVolume.Location.X && cursor.Y >= form.trackBarVolume.Location.Y)){
form.TogglePause();
return true;
}
}
else if (m.Msg == 0x0203 || (m.Msg == 0x0100 && m.WParam.ToInt32() == 0x20)){ // WM_LBUTTONDBLCLK, WM_KEYDOWN, VK_SPACE
form.TogglePause();
}
return false;
}
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Runtime.InteropServices;
namespace TweetDuck.Video{
static class NativeMethods{
private const int GWL_HWNDPARENT = -8;
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[StructLayout(LayoutKind.Sequential)]
public struct RECT{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
public static void SetWindowOwner(IntPtr child, IntPtr owner){
SetWindowLong(child, GWL_HWNDPARENT, owner);
/*
* "You must not call SetWindowLong with the GWL_HWNDPARENT index to change the parent of a child window"
*
* ...which I'm not sure they're saying because this is completely unsupported and causes demons to come out of sewers
* ...or because GWL_HWNDPARENT actually changes the OWNER and is therefore NOT changing the parent of a child window
*
* ...so technically, this is following the documentation to the word.
*/
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Globalization;
using System.Windows.Forms;
namespace TweetDuck.Video{
static class Program{
// referenced in VideoPlayer
public const int CODE_INVALID_ARGS = 1;
public const int CODE_LAUNCH_FAIL = 2;
public const int CODE_MEDIA_ERROR = 3;
public const int CODE_OWNER_GONE = 4;
[STAThread]
private static int Main(string[] args){
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
IntPtr ownerHandle;
string videoUrl;
try{
ownerHandle = new IntPtr(int.Parse(args[0], NumberStyles.Integer, CultureInfo.InvariantCulture));
videoUrl = new Uri(args[1], UriKind.Absolute).AbsoluteUri;
}catch{
return CODE_INVALID_ARGS;
}
try{
Application.Run(new FormPlayer(ownerHandle, videoUrl));
}catch(Exception e){
// TODO
Console.Out.WriteLine(e.Message);
return CODE_LAUNCH_FAIL;
}
return 0;
}
}
}

View File

@ -0,0 +1,35 @@
using System.Reflection;
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("TweetDuck Video")]
[assembly: AssemblyDescription("TweetDuck Video")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("TweetDuck.Video")]
[assembly: AssemblyCopyright("")]
[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("278b2d11-402d-44b6-b6a1-8fa67db65565")]
// 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,69 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" 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>{278B2D11-402D-44B6-B6A1-8FA67DB65565}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>TweetDuck.Video</RootNamespace>
<AssemblyName>TweetDuck.Video</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<ResolveComReferenceSilent>True</ResolveComReferenceSilent>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>none</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<GenerateSerializationAssemblies>Auto</GenerateSerializationAssemblies>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<Compile Include="ControlWMP.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="FormPlayer.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="FormPlayer.Designer.cs">
<DependentUpon>FormPlayer.cs</DependentUpon>
</Compile>
<Compile Include="NativeMethods.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<COMReference Include="WMPLib">
<Guid>{6BF52A50-394A-11D3-B153-00C04F79FAA6}</Guid>
<VersionMajor>1</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>