This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Grim Reaper's profile picture

Simple Recorder

Started by Grim Reaper, 23 January 2013 - 07:05 PM
Grim Reaper #1
Posted 23 January 2013 - 08:05 PM
I've noticed a couple of screen capturing programs floating around recently, so I wanted to give it a shot.

I looked over the code that was written by GravityScore for LightShot, and it seemed really and long and fairly complex.
This script is a lot shorter and a bit less complex. It is lightweight and the saving and loading speeds are much faster than that of ScreenCapture by Ardera. However, it doesn't save nearly as fast as GravityScore's LightShot because, although I'm not entirely sure if this is true, I believe it writes the recording data to a file as its recording rather than afterwards.

I wrote this script in about half an hour, so it is likely to have a couple of bugs, but hopefully not too many.
* Fixed choppiness in playing back recordings!
* Fixed a bug where the program would crash if the recording was too long.
* Added a little prompt to tell you how far along in the saving the program is.
* Made saving and loading recordings more than ten times faster!

Code:
Spoiler


local tArgs = { ... } -- Capture arguments.

--[[
    Recorder        PaymentOption
                    22 January 2013

    This is a simple implementation of
    screen recording software that seems
    to be so popular now-a-days.
]]--

-- Variables --
local NATIVE_TERM     = {} -- A copy of the native terminal.
local recordingTerm   = {} -- The terminal which will house edited functions for recording.
local recordingTable  = {} -- The table that will record all calls by the terminal.

local RECORDING_PATH = nil      -- The path that the user wants to save a new recording to.
local END_RECORDING = "Recording finished." -- The string that is returned by the shell thread when it has exited.
-- Below is the thread which houses the shell which will be used to record the terminal output of the computer.
-- This thread returns once the user has exited the shell by running the 'exit' program on their machine.
local shellThread = coroutine.create(function()

    while true do
        local eventData   = { os.pullEvent() }

        local hasFinished = coroutine.resume(os.run({["shell"] = shell}, "rom/programs/shell"), unpack(eventData))

        -- In the case that this shell is exited because the 'exit' program was run, then we need to mimic the shell
        -- and almost shutdown the computer, except we'll just return from this recording.
        if hasFinished then
            return END_RECORDING
        end
    end

end)
-- Variables --

-- Terminal Functions --
-- Creates and returns a copy of the current terminal table.
function createTerminalCopy()

    local copy = {}
    for itemName, item in pairs(term.native) do
        copy[itemName] = item
    end

    return copy

end

-- Takes a terminal table and recording table and modifies the functions of
-- the terminal table to record the screen.
-- In a recording table, entries are made in the format: t[n] = {functionName = (string), parameters = (variable number of arguemts ( ... ))}
function modifyTerminalForRecording(terminalTable, recordingTable)

    -- Returns a modified function that acts as an old terminal function by
    -- adding the call of the function to the recording table and calling the old
    -- function from the terminalTable.
    local function modifyFunction(functionName, recordingTable)

        return function ( ... )

            recordingTable[#recordingTable + 1] = {functionName = functionName, parameters = { ... }, currentTime = os.clock()}
            return NATIVE_TERM[functionName]( ... )

        end

    end

    for itemName, item in pairs(terminalTable) do
        terminalTable[itemName] = modifyFunction(itemName, recordingTable)
    end

    return terminalTable

end
-- Terminal Functions --

-- File Functions --
-- Writes a recording to a file. Takes a recording table.
function writeRecordingToFile(recordingTable, filePath)

    local fileHandle = fs.open(filePath, 'w')    
    print("Saving recording... ")

    fileHandle.writeLine(textutils.serialize(recordingTable))
    fileHandle.close()

end

-- Reads a recording from a file. Returns the recording table.
function readRecordingFromFile(filePath)

    local fileHandle     = fs.open(filePath, 'r')
    local recordingTable = textutils.unserialize(fileHandle.readLine())
    fileHandle.close()

    return recordingTable

end
-- File Functions --

-- Playback Functions --
-- Plays back a recording table.
function playRecording(recordingTable)

    NATIVE_TERM.clear()
    NATIVE_TERM.setCursorPos(1, 1)

    print(#recordingTable)
    -- Sleep the difference between each call, assuming this isn't the first call.
    for itemIndex, item in pairs(recordingTable) do
        -- Make sure that the difference for sleeping is greater than 0.3. If this isn't the case, then don't sleep at all.
        if itemIndex > 1 and item.currentTime - recordingTable[itemIndex - 1].currentTime > 0 then
            sleep(item.currentTime - recordingTable[itemIndex - 1].currentTime)
        end

        NATIVE_TERM[item.functionName](unpack(item.parameters))
    end

    NATIVE_TERM.clear()
    NATIVE_TERM.setCursorPos(1, 1)

end
-- Playback Functions --



-- Initialization --
NATIVE_TERM   = createTerminalCopy()

recordingTerm = createTerminalCopy()
recordingTerm = modifyTerminalForRecording(recordingTerm, recordingTable)
-- Initialization --


-- Handle arguments.
if #tArgs > 0 then
    -- If the user wants to play a recording, then attempt to load it.
    if fs.exists(tArgs[1]) and not fs.isDir(tArgs[1]) then
        playRecording(readRecordingFromFile(tArgs[1]))
        print("End of recording at " .. tArgs[1] .. '.')
        return

    -- If the user wants to record a new recording, then do that.
    -- Don't return here.
    elseif tArgs[1] == "record" and not fs.exists(tArgs[2]) then
        RECORDING_PATH = tArgs[2]
        term.clear()
        term.setCursorPos(1, 1)
        print("Run the 'exit' program in the shell to stop recording and save.")
        sleep(1)
    else
        -- If none of the argument setups match, then print the usage.
        print("Usage: " .. shell.getRunningProgram() .. " <recording path>")
        print("       " .. shell.getRunningProgram() .. " record <new recording path>")
        return
    end
else
    -- If none of the argument setups match, then print the usage.
    print("Usage: " .. shell.getRunningProgram() .. " <recording path>")
    print("       " .. shell.getRunningProgram() .. " record <new recording path>")
    return
end
-- Handle arguments.


-- Main entry point --
-- Redirect terminal output to the modified terminal table.
term.redirect(recordingTerm)
term.clear()
term.setCursorPos(1, 1)

-- Queue a couple of events to get the shell going.
os.queueEvent("char", '')
os.queueEvent("char", '')
while coroutine.status(shellThread) ~= "dead" do
    local eventData = { os.pullEvent() }

    coroutine.resume(shellThread, unpack(eventData))
end

-- Once the thread has died, redirect the terminal output back to the standard terminal
-- and replay the recording.
term.redirect(NATIVE_TERM)
NATIVE_TERM.clear()
NATIVE_TERM.setCursorPos(1, 1)
writeRecordingToFile(recordingTable, RECORDING_PATH)
print("Finished recording. Saved as " .. RECORDING_PATH)

-- Main entry point --

Pastebin link: http://pastebin.com/K8aagw9K

There probably won't be any screenshots considering how easy this program is to use. The usage is displayed by the program so I won't bother explaining it here.

- Payment
theoriginalbit #2
Posted 24 January 2013 - 06:57 PM
Hello :)/>

I have a bug report for you :)/>
SpoilerAs I was saving a simple recording I got this:
SimpleRecorder:87: attempt to index ? (a nil value)
Grim Reaper #3
Posted 24 January 2013 - 07:39 PM
Hello :)/>

I have a bug report for you :)/>
SpoilerAs I was saving a simple recording I got this:
SimpleRecorder:87: attempt to index ? (a nil value)

That's strange. I updated the version currently on pastebin, so try and use that. Usually that kind of problem is because of a file handle which isn't valid, but I'm not sure. Restarting the CC computer might work.

I recorded myself editing a text file with the latest version and it saved and played back just fine, so I'm not sure what the problem was. Try the current version on pastebin and see if it doesn't solve the problem. Also, try restarting your CC computer.


I'm going to bed seeing as I have finals tomorrow, so I'll try and be of more assistance when I get up if I have any time.
theoriginalbit #4
Posted 24 January 2013 - 07:45 PM
nope still getting it, line 86 now tho…

altho I have just figured it out… it doesn't seem to like it when its writing to a file, in a path that doesn't exist (for example if the folder sr doesn't exist and i try to get it to put it in sr it wont do it)

also if the file exists you show usage as though they have entered it in the calling of the program wrong… :)/>

good luck with your finals
Grim Reaper #5
Posted 25 January 2013 - 12:08 PM
nope still getting it, line 86 now tho…

altho I have just figured it out… it doesn't seem to like it when its writing to a file, in a path that doesn't exist (for example if the folder sr doesn't exist and i try to get it to put it in sr it wont do it)

also if the file exists you show usage as though they have entered it in the calling of the program wrong… :)/>

good luck with your finals

In all honesty, this program wasn't designed to be released as something that was used by the public. Let me elaborate on this: I wrote it mainly because I wanted to. I wanted to see if I could accomplish the same thing in a simpler way that a few others who were achieving great popularity for it.
GravityScore #6
Posted 25 January 2013 - 12:41 PM
Wow I only just saw this. Looks pretty cool :D/>

No, Lightshot does not save to a file as it goes. Try rebooting the computer while recording - no file is there! It does save afterwards. I actually have no idea why other frame-based screen recorders take so long to save…
NeverCast #7
Posted 25 January 2013 - 01:03 PM
Just wait until you see CCT's upcoming format ;)/>
It's SWEEEEEEEET!
theoriginalbit #8
Posted 25 January 2013 - 01:10 PM
In all honesty, this program wasn't designed to be released as something that was used by the public. Let me elaborate on this: I wrote it mainly because I wanted to. I wanted to see if I could accomplish the same thing in a simpler way that a few others who were achieving great popularity for it.
Hey thats fine, I have no objection to you decisions, however NeverCast and I wish to add support for your video format into CCTube, and well we needed a video file to see what it was like (yes I know we could have read your code to see, but I was just being lazy :P/> )…


Just wait until you see CCT's upcoming format ;)/>
It's SWEEEEEEEET!
Lol I was telling Gravity about our 1 byte reduction on colours early this morning (our time).
NeverCast #9
Posted 25 January 2013 - 01:14 PM
Hey thats fine, I have no objection to you decisions, however NeverCast and I wish to add support for your video format into CCTube, and well we needed a video file to see what it was like (yes I know we could have read your code to see, but I was just being lazy :P/> )…

We would greatly like to implement all screen recorder formats in to our transcoder, for wide support. But can't do so if it doesn't record and save :D/>

Lol I was telling Gravity about our 1 byte reduction on colours early this morning (our time).

What'd he think? (:
theoriginalbit #10
Posted 25 January 2013 - 01:17 PM
What'd he think? (:
Not much :P/>
Grim Reaper #11
Posted 25 January 2013 - 04:14 PM
What'd he think? (:
Not much :P/>

Here's a basic recording of me just typing gibberish into the shell.
However, something to be wary of is that programs which are drawing intensive, like the edit program, will yield much larger file sizes.
It works by saving the function name of the terminal call, the current clock time when the function was called, and the parameters to the terminal function called. This is done by using the built in serialization code as it goes along:
http://pastebin.com/LjXLDfb1

It's all done in one line so that the function which plays recordings can just read the first line from a recording file and unserialize it, then play it back by iterating through the unserialized table of terminal calls.
theoriginalbit #12
Posted 25 January 2013 - 04:19 PM
-snip-
ok thank you :)/> looks like this might be the easiest one to support ;)/> :)/>
Grim Reaper #13
Posted 25 January 2013 - 04:23 PM
-snip-
ok thank you :)/> looks like this might be the easiest one to support ;)/> :)/>

I wrote it to be all about simplicity ;)/> What did you think of my unauthorized call check-thing for lightshot recordings?
theoriginalbit #14
Posted 25 January 2013 - 04:26 PM
-snip-
ok thank you :)/> looks like this might be the easiest one to support ;)/> :)/>

I wrote it to be all about simplicity ;)/> What did you think of my unauthorized call check-thing for lightshot recordings?
Yeh its good :)/> but NeverCast and I are going with a different method now that allows the multi recorder compatibility.
Grim Reaper #15
Posted 25 January 2013 - 04:27 PM
-snip-
ok thank you :)/> looks like this might be the easiest one to support ;)/> :)/>

I wrote it to be all about simplicity ;)/> What did you think of my unauthorized call check-thing for lightshot recordings?
Yeh its good :)/> but NeverCast and I are going with a different method now that allows the multi recorder compatibility.

Cool.
theoriginalbit #16
Posted 25 January 2013 - 04:30 PM
-snip-
Cool.
Yeh we were up fairly late last night putting together the spec documentation and starting the coding. we are trying to get the smallest file size by reducing bytes used and handling duplicate frames and such.
Grim Reaper #17
Posted 25 January 2013 - 04:34 PM
-snip-
Cool.
Yeh we were up fairly late last night putting together the spec documentation and starting the coding. we are trying to get the smallest file size by reducing bytes used and handling duplicate frames and such.

Sounds interesting! I'm still receiving my formal CS education, but I'd like to help in any way I can.
theoriginalbit #18
Posted 25 January 2013 - 04:39 PM
Sounds interesting! I'm still receiving my formal CS education, but I'd like to help in any way I can.
Yeh we will let you know if we need any assistance or if we run into any issues and can't figure it out, took me a while to remember back to my Computer Logics &amp; Essentials unit for some stuff we are doing last night :P/> … Cool have fun with CS :P/> I hated my CS unit! I'm Bachelor of Science (Professional Software Development), NeverCast is a Programming Teacher ( from memory :P/> )