最近由于工程需要开始研发基于Windows的自动录屏软件,很多细节很多功能需要处理,毕竟一个完美的录屏软件不是你随随便便就可以写出来的。首先参考了大部分的录屏软件,在研发的过程中遇到了很多的问题;比如-视频加载、麦克风加载、麦克风音量调节、视频播放进度控、视频音量控制、等等很多细节部分都需要好好规划才能开始做。录屏采用的是视频帧的思维逻辑进行编写的。
目前已经基本上成型,基于WPF采用了Model - View框架进行动态加载,每个线程与线程之间采用Async异步执行,并使用线程等待;录屏基本功能包含了(展示历史录屏记录、删除、录屏、视频编码、视频播放及删除、麦克风调用(音量调节-跟随系统)、加载视频(拖拉-旋转)、系统遮罩 等);编码的核心是采用FFMPEG(这个工具真的非常强大);
这边提供几个核心代码仅供参考:
1-难点:系统遮罩核心方法(使用Windows API):
1 /// <summary>
2 /// 视图模型属性改变
3 /// </summary>
4 /// <param name="sender">
5 /// The sender.
6 /// </param>
7 /// <param name="propertyChangedEventArgs">
8 /// 属性改变事件参数
9 /// </param>
10 private void ViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
11 {
12 if (propertyChangedEventArgs.PropertyName == "IsRecording")
13 {
14 this.Locked = this.ViewModel.IsRecording;
15 if (this.ViewModel.IsRecording)
16 {
17 var hwnd = new WindowInteropHelper(this).Handle;
18 NativeWindowHelper.SetWindowExTransparent(hwnd);
19 }
20 }
21
22 if (propertyChangedEventArgs.PropertyName == "IsFullScreen")
23 {
24 this.IsFullScreen = this.ViewModel.IsFullScreen;
25 }
26 }
改变属性的时候触发
1 #region Constants
2
3 /// <summary>
4 /// The gw l_ exstyle.
5 /// </summary>
6 [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:FieldNamesMustNotContainUnderscore",
7 Justification = "Reviewed. Suppression is OK here.")]
8 private const int GWL_EXSTYLE = -20;
9
10 /// <summary>
11 /// The w s_ e x_ transparent.
12 /// </summary>
13 [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:FieldNamesMustNotContainUnderscore",
14 Justification = "Reviewed. Suppression is OK here.")]
15 private const int WS_EX_TRANSPARENT = 0x00000020;
16
17
18
19
20 #endregion
21
22 #region Public Methods and Operators
23
24 /// <summary>
25 /// 窗口前置透明设置命令
26 /// </summary>
27 /// <param name="hwnd">
28 /// The hwnd.
29 /// </param>
30 public static void SetWindowExTransparent(IntPtr hwnd)
31 {
32 var extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
33 SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TRANSPARENT);
34 }
35
36 #endregion
37
38 #region Methods
39
40 /// <summary>
41 /// The get window long.
42 /// </summary>
43 /// <param name="hwnd">
44 /// The hwnd.
45 /// </param>
46 /// <param name="index">
47 /// The index.
48 /// </param>
49 /// <returns>
50 /// The <see cref="int"/>.
51 /// </returns>
52 [DllImport("user32.dll")]
53 private static extern int GetWindowLong(IntPtr hwnd, int index);
54
55 /// <summary>
56 /// The set window long.
57 /// </summary>
58 /// <param name="hwnd">
59 /// The hwnd.
60 /// </param>
61 /// <param name="index">
62 /// The index.
63 /// </param>
64 /// <param name="newStyle">
65 /// The new style.
66 /// </param>
67 /// <returns>
68 /// The <see cref="int"/>.
69 /// </returns>
70 [DllImport("user32.dll")]
71 private static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
72
73 #endregion
API方法
2-难点:麦克风获取及控制
<Slider x:Name="volumeSlider" Grid.Column="7" Grid.ColumnSpan="3" Grid.Row="1" Width="100" Height="20" Minimum="0" Maximum="100" Value="100" VerticalAlignment="Center" />
1 //定义一个获取之前拉动时候的value值,然后跟当前的value对比,选择触发
2 private bool isUserChangeVolume = true;
3 private VolumeControl volumeControl;
4 //private DispatcherTimer volumeControlTimer;
5
6 /// <summary>
7 /// 加载拖动条的事件
8 /// </summary>
9 /// <param name="sender"></param>
10 /// <param name="e"></param>
11 private void volumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
12 {
13 if (isUserChangeVolume)
14 {
15 volumeControl.MasterVolume = volumeSlider.Value;
16 }
17 }
18
19 private void InitializeAudioControl()
20 {
21 volumeControl = VolumeControl.Instance;
22 volumeControl.OnAudioNotification += volumeControl_OnAudioNotification;
23 volumeControl_OnAudioNotification(null, new AudioNotificationEventArgs() { MasterVolume = volumeControl.MasterVolume });
24
25 //volumeControlTimer = new DispatcherTimer();
26 //volumeControlTimer.Interval = TimeSpan.FromTicks(150);
27 //volumeControlTimer.Tick += volumeControlTimer_Tick;
28 }
29
30 void volumeControl_OnAudioNotification(object sender, AudioNotificationEventArgs e)
31 {
32 this.isUserChangeVolume = false;
33 try
34 {
35 this.Dispatcher.Invoke(new Action(() => { volumeSlider.Value = e.MasterVolume; }));
36 }
37 catch { }
38 this.isUserChangeVolume = true;
39 }
40
41 void volumeControlTimer_Tick(object sender, EventArgs e)
42 {
43 //获取系统主声道、左声道、右声道音量值
44 //double[] information = volumeControl.AudioMeterInformation;
45 //mMasterPBar.Value = information[0];
46 //mLeftPBar.Value = information[1];
47 //mRightPBar.Value = information[2];
48 }
3-难点:系统遮罩(其实也不能算难点,这个是API调用的时候颜色控制);
4-难点:视频旋转核心代码(已更新为方法8)
1 /// <summary>
2 /// 旋转视频
3 /// </summary>
4 /// <param name="sender"></param>
5 /// <param name="e"></param>
6 private void RotateCamera_bt(object sender, RoutedEventArgs e)
7 {
8 if (AnAngle > 360 || AnAngle == 0)
9 {
10 AnAngle = 90;
11 }
12 TransformGroup transformGroup = new TransformGroup();
13
14 ScaleTransform scaleTransform = new ScaleTransform();
15 scaleTransform.ScaleX = -1;
16 transformGroup.Children.Add(scaleTransform);
17
18 RotateTransform rotateTransform = new RotateTransform(AnAngle);
19 transformGroup.Children.Add(rotateTransform);
20 videoPlayer.RenderTransform = transformGroup;
21 AnAngle += 90;
22 }
旋转视频代码
5-难点:录屏核心代码(这部分代码视频格式可以自行调整,颜色代码原理已经理解。)--已更新为方法10
1 /// <summary>
2 /// Starts the recording.
3 /// </summary>
4 public void StartRecording()
5 {
6 this.notifyIcon.HideBalloonTip();
7 this.IsRecording = true;
8
9
10 var fileName = string.Format("Recording {0}.mp4", DateTime.Now.ToString("yy-MM-dd HH-mm-ss"));
11 var outputFilePath = Path.Combine(this.settings.StoragePath, fileName);
12 this.fileViewModel = new ScreenGunFileViewModel(outputFilePath, RecordingStage.DoingNothing);
13
14 var opts = new ScreenRecorderOptions(this.RecordingRegion)
15 {
16 DeleteMaterialWhenDone = true,
17 OutputFilePath = outputFilePath,
18 RecordMicrophone = this.UseMicrophone,
19 AudioRecordingDeviceNumber = this.settings.RecordingDeviceNumber
20 };
21
22 var progress = new Progress<RecorderState>(state => this.fileViewModel.RecordingStage = state.Stage);
23 this.recorder.Start(opts, progress);
24 }
录屏代码
6-难点:屏幕画框代码(采集X,Y坐标及遮幕的宽,高)
1 /// <summary>
2 /// 设置初始区域
3 /// </summary>
4 private void SetupInitialRegion()
5 {
6 var cursorPos = System.Windows.Forms.Cursor.Position;
7 foreach (var screen in Screen.AllScreens)
8 {
9 if (screen.Bounds.Contains(cursorPos) == false)
10 {
11 continue;
12 }
13
14 var regionWidth = (double)screen.Bounds.Width / 2;
15 var regionHeight = (double)screen.Bounds.Height / 2;
16 double x = ((double)screen.Bounds.Width / 2) - (regionWidth / 2);
17 double y = ((double)screen.Bounds.Height / 2) - (regionHeight / 2);
18 x -= this.virtualScreen.X - screen.Bounds.X;
19 y -= this.virtualScreen.Y - screen.Bounds.Y;
20
21 this.startPosition = new Point(x, y);
22 this.endPosition = new Point(x + regionWidth, y + regionHeight);
23 this.UpdatePosition();
24 break;
25 }
26 }
7-放大缩小(根据屏幕大小范围随意拉伸缩小) 核心代码如下:
当你有摄像头长跟宽不一样的时候,旋转-缩小-放大然后根据给定的边缘坐标是一个非常头疼的事情,单单这个问题就使我加班到凌晨4点了,不过最终还是解决了;
1 void resizer_Resize(object sender, ControlResizeEventArgs e)
2 {
3 if (!this.RectangleU.IsMouseCaptured) return;
4 if (AnAngle == 180 || AnAngle == 360)
5 {
6 #region --竖直拉伸--
7 double Image_xx = 0;
8 double Image_yy = 0;
9 double point_xx = 0;
10 double point_yy = 0;
11 double point_center = Math.Abs(this.MainGrid.Width / 2 - this.MainGrid.Height / 2);//当前中心点值
12 double actual_center = Math.Abs(this.videoPlayer.MinWidth / 2 - this.videoPlayer.MinHeight / 2);//实际中心点,用于比较初始值
13
14 if (Math.Abs(Image_PointX) == 0)
15 {
16 point_xx = -25;//初始化原点未动
17 }
18 else
19 {
20 //拖动到其他位置时偏移量(必须是固定值)
21 if (Image_PointX < -25)
22 {
23 point_xx = -25;
24 }
25 else
26 {
27 point_xx = Image_PointX;
28 }
29 }
30
31 if (Math.Abs(Image_PointY) == 0)
32 {
33 point_yy = -25;//初始化原点未动
34 }
35 else
36 {
37 //拖动到其他位置时偏移量(必须是固定值)
38 if (Image_PointY < -25)
39 {
40 point_yy = -25;
41 }
42 else
43 {
44 point_yy = Image_PointY;
45 }
46
47 }
48 if (Math.Abs(point_xx) == 25)
49 {
50 Image_xx = videoPlayer.ActualHeight;
51 }
52 else
53 {
54 Image_xx = videoPlayer.ActualHeight + Math.Abs(Image_PointX) - 25;
55 }
56 if (Math.Abs(point_yy) == 25)
57 {
58 Image_yy = videoPlayer.ActualWidth;
59 }
60 else
61 {
62 Image_yy = Math.Abs(Image_PointY) + videoPlayer.ActualWidth - 25;
63 }
64
65 //左右拉伸(只能往右拉伸)
66 if (e.LeftDirection.HasValue)
67 {
68 var value = videoPlayer.Height + e.HorizontalChange;
69 if (value > videoPlayer.MinHeight)
70 {
71 videoPlayer.Height = value;
72 MainGrid.Height = value;
73 if (videoPlayer.ActualHeight < value)
74 {
75 MainGrid.Height = videoPlayer.ActualHeight;
76 }
77 if (Image_xx >= RecordingArea.Width)
78 {
79 MainGrid.Height = videoPlayer.ActualHeight;
80 videoPlayer.Height = videoPlayer.ActualHeight;
81 }
82 }
83 }
84 //上下拉伸(只能往上拉伸)
85 if (e.TopDirection.HasValue)
86 {
87 var value = videoPlayer.Width + e.VerticalChange;
88 if (value > videoPlayer.MinWidth)
89 {
90 videoPlayer.Width = value;
91 MainGrid.Width = value;
92 if (videoPlayer.ActualWidth < value)
93 {
94 MainGrid.Width = videoPlayer.ActualWidth;
95 }
96
97 if (Image_yy >= RecordingArea.Height)
98 {
99 MainGrid.Width = videoPlayer.ActualWidth;
100 videoPlayer.Width = videoPlayer.ActualWidth;
101 }
102
103 }
104 }
105
106 #region 调整位置
107
108 Matrix m = MainGrid.RenderTransform.Value;
109
110
111 //初始值(-25,-25)-->(x,y)
112 if ((Image_xx >= RecordingArea.Width) || Image_yy >= RecordingArea.Height)
113 {
114
115 }
116 else
117 {
118 if (point_center >= actual_center)
119 {
120 // (point_center - actual_center)为x-y轴偏移量
121 //point_xx--point_yy为当前x,y轴坐标
122 m.OffsetX = point_xx - (point_center - actual_center);
123 m.OffsetY = point_yy - (point_center - actual_center);
124
125 }
126 }
127 MainGrid.RenderTransform = new MatrixTransform(m);//重新定位
128
129 #endregion
130
131
132
133 #endregion
134 }
135 else
136 {
137 #region --正常拉伸--
138 //左右拉伸(只能往右拉伸)
139 if (e.LeftDirection.HasValue)
140 {
141 var value = videoPlayer.Width + e.HorizontalChange;
142 if (value > videoPlayer.MinWidth)
143 {
144 videoPlayer.Width = value;
145 MainGrid.Width = value;
146 if (videoPlayer.ActualWidth < value)
147 {
148 MainGrid.Width = videoPlayer.ActualWidth;
149 }
150 if (Image_PointX + videoPlayer.ActualWidth >= RecordingArea.Width)
151 {
152 MainGrid.Width = videoPlayer.ActualWidth;
153 videoPlayer.Width = videoPlayer.ActualWidth;
154 }
155 }
156 }
157 //上下拉伸(只能往上拉伸)
158 if (e.TopDirection.HasValue)
159 {
160 var value = videoPlayer.Height + e.VerticalChange;
161 if (value > videoPlayer.MinHeight)
162 {
163 videoPlayer.Height = value;
164 MainGrid.Height = value;
165 if (videoPlayer.ActualHeight < value)
166 {
167 MainGrid.Height = videoPlayer.ActualHeight;
168 }
169
170 if (Math.Abs(Image_PointY) + videoPlayer.ActualHeight >= RecordingArea.Height)
171 {
172 MainGrid.Height = videoPlayer.ActualHeight;
173 videoPlayer.Height = videoPlayer.ActualHeight;
174 }
175
176 }
177 }
178 #endregion
179 }
180 }
放大缩小-分长宽不一致情况
8-旋转,核心代码如下:
1 private void RotateCamera_bt(object sender, RoutedEventArgs e)
2 {
3 if (MainGrid.ActualWidth > SystemParameters.PrimaryScreenHeight)
4 {
5 return;
6 }
7 if (AnAngle > 360 || AnAngle == 0)
8 {
9 AnAngle = 90;
10 }
11
12 TransformGroup transformGroup = new TransformGroup();
13 RotateTransform rotateTransform = new RotateTransform(AnAngle);
14 transformGroup.Children.Add(rotateTransform);
15 MainGrid.RenderTransform = transformGroup;
16 #region 特殊四个角反转需要变换长跟宽
17 //重新调整坐标坐标位置
18 Matrix m = MainGrid.RenderTransform.Value;
19 //求出中心点坐标
20 double point_xx = (this.MainGrid.ActualWidth) / 2 - (this.MainGrid.ActualHeight) / 2;
21 // Image_PointX,Image_Point为当前坐标
22 if (AnAngle == 90 || AnAngle == 270)
23 {
24 if (AnAngle == 270)
25 {
26 RectangleU.VerticalAlignment = VerticalAlignment.Bottom;
27 RectangleU.HorizontalAlignment = System.Windows.HorizontalAlignment.Right;
28 RectangleU.BorderThickness = new Thickness(0, 0, 8, 8);
29 RectangleU.CornerRadius = new CornerRadius(0, 0, 1, 0);
30
31 }
32 else
33 {
34 RectangleU.VerticalAlignment = VerticalAlignment.Top;
35 RectangleU.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
36 RectangleU.BorderThickness = new Thickness(8, 8, 0, 0);
37 RectangleU.CornerRadius = new CornerRadius(1, 0, 0, 0);
38
39 }
40 if (!IschangeAngle)
41 {
42 if (Image_PointX <= -point_xx)
43 {
44 m.OffsetX = -point_xx;
45 }
46 else
47 {
48 m.OffsetX = Image_PointX - point_xx;
49 }
50 if (Image_PointY >= -point_xx)
51 {
52 m.OffsetY = -point_xx;
53 }
54 else
55 {
56 m.OffsetY = Image_PointY + point_xx;
57 }
58
59 if (m.OffsetX >= this.RecordingArea.Width - this.videoPlayer.Width - point_xx)
60 {
61 m.OffsetX = m.OffsetX + point_xx * 2;
62
63 }
64 if (m.OffsetY >= -point_xx)
65 {
66 m.OffsetY = -point_xx;
67 }
68 }
69 else
70 {
71 //旋转为竖直拉到某个坐标时触发
72 if (Image_PointX <= -point_xx)
73 {
74 m.OffsetX = -point_xx;
75 }
76 else
77 {
78 m.OffsetX = Image_PointX;
79 }
80 if (Image_PointY >= point_xx)
81 {
82 m.OffsetY = -point_xx;
83 }
84 else
85 {
86 m.OffsetY = Image_PointY;
87 }
88 }
89
90 if (this.MainGrid.Width >= this.RecordingArea.Height)
91 {
92 //触发
93 //相对于屏幕的x,y轴不变
94 m.OffsetX = -point_xx;
95 m.OffsetY = -point_xx;
96
97
98 }
99
100 }
101 else
102 {
103 if (AnAngle == 180)
104 {
105 RectangleU.VerticalAlignment = VerticalAlignment.Bottom;
106 RectangleU.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
107 RectangleU.BorderThickness = new Thickness(8, 0, 0, 8);
108 RectangleU.CornerRadius = new CornerRadius(0, 0, 0, 1);
109
110 }
111 else
112 {
113 RectangleU.VerticalAlignment = VerticalAlignment.Top;
114 RectangleU.HorizontalAlignment = System.Windows.HorizontalAlignment.Right;
115 RectangleU.BorderThickness = new Thickness(0, 8, 8, 0);
116 RectangleU.CornerRadius = new CornerRadius(0, 1, 0, 0);
117
118 }
119 if (IschangeAngle)
120 {
121 if (this.MainGrid.Width >= this.RecordingArea.Height)
122 {
123 Image_PointX = 0;
124 }
125 else
126 {
127 if (Image_PointX + this.videoPlayer.Width > this.RecordingArea.Width)
128 {
129 m.OffsetX = Image_PointX - point_xx;
130 }
131 else
132 {
133 m.OffsetX = Image_PointX + point_xx;
134 }
135 }
136 //旋转为竖直拉到某个坐标时触发
137 if (Image_PointY >= -point_xx)
138 {
139 m.OffsetY = 0;
140 }
141 else
142 {
143 m.OffsetY = Image_PointY - point_xx;
144 }
145
146 }
147 else
148 {
149
150 if (this.MainGrid.Width >= this.RecordingArea.Width)
151 {
152 m.OffsetX = 0;
153 m.OffsetY = 0;
154 }
155 else
156 {
157 //正常情况
158 if (Image_PointX <= 0)
159 {
160 m.OffsetX = 0;
161 }
162 else
163 {
164 m.OffsetX = Image_PointX;
165 }
166 if (Image_PointY >= 0)
167 {
168 m.OffsetY = 0;
169 }
170 else
171 {
172 m.OffsetY = Image_PointY;
173 }
174 }
175 }
176 }
177 //IschangeAngle = false;
178 //更换坐标位置
179 MainGrid.RenderTransform = new MatrixTransform(m);
180
181
182 var x = Math.Min(this.startPosition.X, this.endPosition.X);
183 var y = Math.Min(this.startPosition.Y, this.endPosition.Y);
184 if (AnAngle == 90 || AnAngle == 270)
185 {
186 if (this.MainGrid.Width >= this.RecordingArea.Height)
187 {
188 this.relativeRecordingArea = new Rect(x, y, this.MainGrid.Height, this.MainGrid.Width);
189 this.UpdateUI();
190 }
191 }
192 else
193 {
194 if (this.MainGrid.Width >= this.RecordingArea.Width)
195 {
196 this.relativeRecordingArea = new Rect(x, y, this.MainGrid.Width, this.MainGrid.Height);
197 this.UpdateUI();
198 }
199 }
200 //UpdatePosition();
201
202 AnAngle += 90;
203 #endregion
204 }
旋转代码-分长宽不一致情况
9-不同屏幕百分比自适应边框-采用DPIX
这个稍微简单点:只要获取出每个屏幕差值即可。
dpiX = graphics.DpiX / 96;//当前屏幕的DPI然后除以正常值96得出的值即为扩展百分比
10-录屏核心代码:(不采用之前的位图编译,直接通过引用第三方插件)
通过AForge对FFMPEG进行录屏封装,我们可以轻松的录制想要录制的内容,关于录屏时间上则采用的是异步执行Timer。
private void video_NewFrame(object sender, NewFrameEventArgs e)
{
//if (this.IScreenRecording)
//{
this.videoWriter.WriteVideoFrame(e.Frame);
//异步执行时间
this.stopWatchLabel.Dispatcher.Invoke(new Action
(() => this.stopWatchLabel.Text = string.Format
(@"{0}", this.stopWatch.Elapsed.ToString("hh\\:mm\\:ss"))));
//}
//else
//{
// stopWatch.Reset();
// videoStreamer.Stop();
// videoWriter.Close();
//}
}
11-比较重要的一步:任何商用的录屏软件都需要实现播放、暂停、继续功能,这款软件也不例外:
/// <summary>
/// 点击之后更换图标并判断是否需要停止or启用
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PauseOrRecording_Checked(object sender, RoutedEventArgs e)
{
//暂停计时
//暂停保存图片
//暂停保存麦克风
if (IScreenRecording)
{
this.stopWatch.Stop();//时间表
//this.videoStreamer.Stop();
this.videoStreamer.SignalToStop();
if (PassMediaMessage.IsrecordingSound)
{
//暂停
PassMediaMessage.IsrecordingSound = false;
PassMediaMessage.Is_Recording = false;
}
IScreenRecording = false;
}
else
{
if (this.IsMicrophone.IsChecked == true)
{
PassMediaMessage.IsrecordingSound = true;
PassMediaMessage.Is_Recording = true;
}
//启用(只是暂停并没有真正的释放)
this.stopWatch.Start();//时间表
this.videoStreamer.Start();
IScreenRecording = true;
}
}
12-由于我们软件是面向世界的,所以必须有增加世界12国语言支持,这边就不再详细贴出代码了。
13-整体效果展示: