1+ '''
2+ Created on 28 feb. 2020
3+
4+ @author: ingov
5+ '''
6+ from selenium import webdriver
7+ import imageio
8+ import pyautogui
9+ import time
10+ import threading
11+ from datetime import datetime
12+ import os
13+
14+ class DesktopBrowserRecorder :
15+ """
16+ This is a class that allows you to record your desktop or a webdriver-driven browser for
17+ automatic testing.
18+
19+ Attributes:
20+ driver (wedriver): A webdriver to get screenshots in case we want to record a selenium.
21+ webdriver-driven automatic test. By default is None and if we don't provide it in __init__
22+ we will the desktop.
23+ __folder (str): The __folder where we want the videos to be saved.
24+ __encoding (str): The __encoding of the video, .mp4 recommended and tested.
25+ __keeprecording (bool): Flag that indicated the algorithm if it has to keep recording or stop.
26+ frame (obj): Current frame taken from the desktop or the browser that will be added to the video.
27+ __thread (theard): As this process will be all performed in another threard, this is where
28+ we will store that __thread so we can start it or stop it and that way start or stop the recording
29+ session.
30+ """
31+ def __init__ (self ,folder ,encoding ,driver = None ):
32+ """
33+ The constructor for DesktopBrowserRecorder class.
34+
35+ Parameters:
36+ __folder (str): The __folder where we want the videos to be saved.
37+ __encoding (str): The __encoding of the video, .mp4 recommended and tested.
38+ driver (wedriver): A webdriver to get screenshots in case we want to record a selenium.
39+ By default is None, in that case we will record the desktop and not the browser opened
40+ by this webdriver
41+ """
42+ self .driver = driver
43+ self .__folder = folder
44+ self .__sessionPath = ""
45+ self .__encoding = encoding
46+ #To start __keeprecording will be False for starters
47+ self .__keeprecording = False
48+ #To start our frame will be None until we capture an image to use for the video
49+ self .frame = None
50+ #Until we start the recording session this will be None
51+ self .__thread = None
52+
53+ def __threatTakeScreenshot (self ):
54+ """
55+ This function takes the screenshot that will be added to the video in a different __thread
56+ than the main __thread. The screenshot will be of the webdriver or the desktop depending
57+ on what we set in the constructor.
58+
59+ Parameters:
60+ None
61+
62+ Returns:
63+ None
64+ """
65+ #This parameter contains the path to the file where we will save the frame
66+ frameFileName = self .__sessionPath + "/frame.png"
67+ #We will keep iterating until the recording session is stopped
68+ while self .__keeprecording :
69+ #If self.driver is not None, means we are recording a webdriver browser
70+ if self .driver is not None :
71+ #We save our browser screenshot in the file for the frame
72+ self .driver .save_screenshot (frameFileName )
73+ else :
74+ #Otherwise, we are recording the desktop and take the screenshot from
75+ #the desktop and save it too to the frame file
76+ pyautogui .screenshot (frameFileName )
77+ #We use imageio to open that file and assign it to the attribute frame
78+ #that we will use to add to the video in the main threat
79+ self .frame = imageio .imread (frameFileName )
80+ #As we are using a 20fps configuration, we sleep for 1/25 seconds
81+ time .sleep (1 / 25 )
82+
83+ def __buildWriter (self ):
84+ """
85+ This function builders the writer for the class imageio that we will use
86+ to builder the video. We will use this to build the video writer for our
87+ current video.
88+
89+ Parameters:
90+ folderPath (str): Folder where we want to save the videos for our current
91+ recording session.
92+
93+ Returns:
94+ writer (obj): The object we will use to write our current video.
95+ """
96+ #We get the current date and time
97+ now = datetime .now ()
98+ #We use the current date and time and the class attribute __encoding to create the
99+ #video file name
100+ fileName = now .strftime ("%d-%m-%Y_%H-%M-%S" ) + self .__encoding
101+ #We use the recording session __folder and file name to builder the imageio video
102+ #writer that we will use to create our video
103+ writer = imageio .get_writer (self .__sessionPath + "/" + fileName , fps = 20 )
104+ #We return this object
105+ return writer
106+
107+ def __mainThreadRecordingSession (self ):
108+ """
109+ As the recording process should be done in a parallel __thread to the execution of
110+ the program where we want to start a recording, we have to define this function
111+ that contains the main parallel __thread to the program that will be started
112+ when we start the recording session (using the class method start recording
113+ session).
114+
115+ Parameters:
116+ None
117+
118+ Returns:
119+ None
120+ """
121+ #We get the current instant to the second to build the __folder which
122+ #will contain the videos resulting of our recording session
123+ now = datetime .now ()
124+ #We build that path to contain the videos resulting from our recording session
125+ self .__sessionPath = self .__folder + "\\ " + now .strftime ("%d-%m-%Y_%H-%M-%S" )
126+ try :
127+ #We create that __folder
128+ os .mkdir (self .__sessionPath )
129+ except Exception as e :
130+ print (e )
131+ #We create the secondary thread that will start taking the screenshots that will
132+ #build our video
133+ x = threading .Thread (target = self .__threatTakeScreenshot , args = ())
134+ #We use the method __buildWriter to get the video writer that will create and build
135+ #our current video
136+ writer = self .__buildWriter ()
137+ #We start the secondary __thread that will start taking the screenshots
138+ x .start ()
139+ #While the flag __keeprecording is True, we will keep adding frames to our video
140+ #or create a new video if case something happens to our current video
141+ while self .__keeprecording :
142+ try :
143+ #If the frame is not None, if we already have a frame, we add it to the video
144+ if self .frame is not None :
145+ writer .append_data (self .frame )
146+ except Exception :
147+ #In case there is an exception when trying to add the frame to the video
148+ #we close the writer
149+ writer .close ()
150+ #We create a new one
151+ writer = self .__buildWriter ()
152+ if self .frame is not None :
153+ #Again we try to add the frame to the new video
154+ writer .append_data (self .frame )
155+ #NOTE: This is the best way to prevent problems when the size of a browser changes
156+ #in that case, an exception occurs because all frame in a video must have the same size
157+ #so we create a new video in the same __folder so the session continues and no information
158+ #is lost. It will be just distributed in several videos in case the window changes sizes
159+ #several times
160+ #As our video has the property 20fps, we have to sleep our __thread 1/25. It is not 1/20
161+ #because processing some of these instructions take time so we need this __thread to keep
162+ #running faster
163+ time .sleep (1 / 25 )
164+ #Once the flag __keeprecording is False we stop our recording session, so we close our video writer
165+ writer .close ()
166+ #And stop the secondary __thread and takes the screenshots
167+ x .join ()
168+
169+ def startRecordingSession (self ):
170+ """
171+ This method starts the recording session. It starts a new __thread that runs the method
172+ __mainThreadRecordingSession
173+
174+ Parameters:
175+ None
176+
177+ Returns:
178+ None
179+ """
180+ #If __keeprecording is already True is because we already have a recording session running
181+ if self .__keeprecording == True :
182+ #In that case, we have to raise a custom made Exception
183+ raise SessionStartedException ("There is an existing running recording session for this object" )
184+ print ("Started our recording session" )
185+ #Otherwise, we don't have a recording session going on, so we change the flag
186+ #__keeprecording to True
187+ self .__keeprecording = True
188+ #We create the main __thread that runs the algorithm and assign it to attribute __thread
189+ #so we can start and stop it
190+ self .__thread = threading .Thread (target = self .__mainThreadRecordingSession , args = ())
191+ #We start that __thread
192+ self .__thread .start ()
193+
194+ def stopRecordingSession (self ):
195+ """
196+ This method stops the recording session. It stops the threads that create the video
197+
198+ Parameters:
199+ None
200+
201+ Returns:
202+ None
203+ """
204+ #If the flag __keeprecording is already False is because we don't have a session running
205+ if self .__keeprecording == False :
206+ #In that case, we raise a custom made Exception
207+ raise NoSessionStartedException ("There is no current recording session" )
208+ #We set the flag keeprecoring to false, to stop the recording process
209+ self .__keeprecording = False
210+ #We stop the main __thread
211+ self .__thread .join ()
212+ #We set the attribute __thread to None, because the __thread is finished and the
213+ #recording session is done
214+ self .__thread = None
215+ try :
216+ os .remove (self .__sessionPath + "/frame.png" )
217+ except Exception :
218+ print ("There was an issue when trying to delete the file where we stored our frames" )
219+ print ("Our recording session ended" )
220+
221+ class SessionStartedException (Exception ):
222+ """
223+ This is a class that inherits from Exception to create a custom made Exception to be raised
224+ in the case that we try to start a new recording session when there is a current session
225+ running.
226+
227+ Attributes:
228+ None
229+ """
230+ pass
231+
232+ class NoSessionStartedException (Exception ):
233+ """
234+ This is a class that inherits from Exception to create a custom made Exception to be raised
235+ in the case that we try to stop a recording session when we have not started a recording
236+ session.
237+
238+ Attributes:
239+ None
240+ """
241+ pass
0 commit comments