├── .github └── FUNDING.yml ├── modules ├── output.lua ├── log.lua ├── profile_handler.lua ├── utils.lua ├── data_format.lua ├── text_source_handler.lua ├── script_handler.lua ├── bot.lua ├── data.lua ├── json.lua └── ljsocket.lua ├── OBS-Stats-on-Stream.lua ├── LICENSE ├── Text-Formatting-Variables.md ├── Bot-Commands.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: greencomfytea 2 | ko_fi: greencomfytea 3 | patreon: greencomfytea 4 | custom: ["https://streamelements.com/greencomfytea/tip", "https://paypal.me/greencomfytea"] 5 | -------------------------------------------------------------------------------- /modules/output.lua: -------------------------------------------------------------------------------- 1 | local output = {}; 2 | 3 | local json; 4 | local data; 5 | 6 | local obslua = obslua; 7 | local require = require; 8 | local script_path = script_path; 9 | local print = print; 10 | 11 | local file_output_name = "obs-stats.json"; 12 | 13 | function output.to_json() 14 | --print("Output to " .. file_output_name); 15 | 16 | local data_json = json.encode(data.stats, { indent = true }); 17 | 18 | local script_path_ = script_path(); 19 | local output_path = script_path_ .. file_output_name; 20 | 21 | obslua.os_quick_write_utf8_file(output_path, data_json, #data_json, false); 22 | end 23 | 24 | function output.init_module() 25 | json = require("modules.json"); 26 | data = require("modules.data"); 27 | end 28 | 29 | return output; -------------------------------------------------------------------------------- /OBS-Stats-on-Stream.lua: -------------------------------------------------------------------------------- 1 | local require = require; 2 | local print = print; 3 | 4 | local json = require("modules.json"); 5 | local ljsocket = require("modules.ljsocket"); 6 | local bot = require("modules.bot"); 7 | local data_format = require("modules.data_format"); 8 | local data = require("modules.data"); 9 | local output = require("modules.output"); 10 | local log = require("modules.log"); 11 | local profile_handler = require("modules.profile_handler"); 12 | local script_handler = require("modules.script_handler"); 13 | local text_source_handler = require("modules.text_source_handler"); 14 | local utils = require("modules.utils"); 15 | 16 | bot.init_module(); 17 | data_format.init_module(); 18 | data.init_module(); 19 | output.init_module(); 20 | log.init_module(); 21 | profile_handler.init_module(); 22 | script_handler.init_module(); 23 | text_source_handler.init_module(); 24 | utils.init_module(); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 GreenComfyTea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Text-Formatting-Variables.md: -------------------------------------------------------------------------------- 1 | # Text Formatting Variables: 2 | 3 | 1. `$date` 4 | 2. `$time` 5 | 3. `$date_time` 6 | 4. `$encoder` 7 | 5. `$output_mode` 8 | 6. `$canvas_resolution` 9 | 7. `$output_resolution` 10 | 8. `$missed_frames` 11 | 9. `$total_missed_frames`/`$missed_total_frames` 12 | 10. `$missed_percents` 13 | 11. `$skipped_frames` 14 | 12. `$total_skipped_frames`/`$skipped_total_frames` 15 | 13. `$skipped_percents` 16 | 14. `$dropped_frames` 17 | 15. `$total_dropped_frames`/`$dropped_total_frames` 18 | 16. `$dropped_percents` 19 | 17. `$congestion` 20 | 18. `$average_congestion` 21 | 19. `$average_frame_time` 22 | 20. `$fps` 23 | 21. `$target_fps` 24 | 22. `$average_fps` 25 | 23. `$memory_usage` 26 | 24. `$cpu_physical_cores` 27 | 25. `$cpu_logical_cores` 28 | 26. `$cpu_cores` 29 | 27. `$cpu_usage` 30 | 28. `$audio_bitrate` 31 | 29. `$recording_bitrate` 32 | 30. `$bitrate` 33 | 31. `$streaming_total_seconds` 34 | 32. `$streaming_total_minutes` 35 | 33. `$streaming_hours` 36 | 34. `$streaming_minutes` 37 | 35. `$streaming_seconds` 38 | 36. `$streaming_duration` 39 | 37. `$recording_total_seconds` 40 | 38. `$recording_total_minutes` 41 | 39. `$recording_hours` 42 | 40. `$recording_minutes` 43 | 41. `$recording_seconds` 44 | 42. `$recording_duration` 45 | 43. `$streaming_status` 46 | 44. `$recording_status` 47 | -------------------------------------------------------------------------------- /modules/log.lua: -------------------------------------------------------------------------------- 1 | local log = {}; 2 | 3 | local json; 4 | local data; 5 | local text_source_handler; 6 | 7 | local obslua = obslua; 8 | local require = require; 9 | local script_path = script_path; 10 | local print = print; 11 | 12 | local file_output_name = "obs-stats_" .. os.date("%d.%m.%Y") .. ".log"; 13 | 14 | local log_file_formatting = [[ 15 | [$date_time] Streaming duration: $streaming_duration, Recording Duration: $recording_duration, Streaming status: $streaming_status, Recording_status: $recording_status, Encoder: $encoder, Output Mode: $output_mode, Canvas Resolution: $canvas_resolution, Output Resolution: $output_resolution, Missed Frames: $missed_frames/$missed_total_frames ($missed_percents%), Skipped Frames: $skipped_frames/$skipped_total_frames ($skipped_percents%), Dropped Frames: $dropped_frames/$dropped_total_frames ($dropped_percents%), Congestion: $congestion% (avg. $average_congestion%), Memory Usage: $memory_usage MB, CPU Cores: $cpu_cores, CPU Usage: $cpu_usage%, Frame Time: $average_frame_time ms, FPS: $fps/$target_fps (avg. $average_fps), Bitrate: $bitrate kb/s, Audio Bitrate: $audio_bitrate kb/s, Recording bitrate: $recording_bitrate kb/s 16 | ]]; 17 | 18 | function log.to_file() 19 | --print("Log to " .. file_output_name); 20 | 21 | local data_log = text_source_handler.format_text(log_file_formatting); 22 | --local data_log = json.encode(data.stats, { indent = false }) .. "\n"; 23 | 24 | local script_path_ = script_path(); 25 | local output_path = script_path_ .. file_output_name; 26 | 27 | local log_file = io.open(output_path, "a"); 28 | log_file:write(data_log); 29 | log_file:close(); 30 | end 31 | 32 | function log.init_module() 33 | json = require("modules.json"); 34 | data = require("modules.data"); 35 | text_source_handler = require("modules.text_source_handler"); 36 | end 37 | 38 | return log; -------------------------------------------------------------------------------- /modules/profile_handler.lua: -------------------------------------------------------------------------------- 1 | local profile_handler = {}; 2 | 3 | local data; 4 | 5 | local obslua = obslua; 6 | local print = print; 7 | local tonumber = tonumber; 8 | local require = require; 9 | 10 | function profile_handler.read_config() 11 | local profile = obslua.obs_frontend_get_current_profile():gsub("[^%w_ ]", ""):gsub("%s", "_"); 12 | 13 | local profile_relative_path = "obs-studio\\basic\\profiles\\" .. profile .. "\\basic.ini"; 14 | 15 | -- char profile_path[512]; 16 | local profile_path = " "; 17 | obslua.os_get_abs_path("..\\..\\config\\" .. profile_relative_path, profile_path, #profile_path); 18 | 19 | if not obslua.os_file_exists(profile_path) then 20 | obslua.os_get_config_path(profile_path, #profile_path, profile_relative_path); 21 | 22 | if not obslua.os_file_exists(profile_path) then 23 | print("Profile Config File not found."); 24 | return; 25 | end 26 | end 27 | 28 | local config_text = obslua.os_quick_read_utf8_file(profile_path); 29 | 30 | if config_text == nil then 31 | print("Couldn't read Profile Config File."); 32 | return; 33 | end 34 | 35 | print("Profile Config loaded: " .. profile_path); 36 | 37 | local config = profile_handler.parse_ini(config_text); 38 | 39 | if config.Output == nil then 40 | data.update_output_mode(nil); 41 | else 42 | data.update_output_mode(config.Output.Mode); 43 | end 44 | 45 | if config.Video == nil then 46 | data.update_target_fps(nil); 47 | else 48 | data.update_canvas_resolution(config.Video.BaseCX, config.Video.BaseCY); 49 | data.update_output_resolution(config.Video.OutputCX, config.Video.OutputCY); 50 | data.update_target_fps(config.Video.FPSCommon); 51 | end 52 | 53 | if data.stats.output_mode == data.output_modes.simple then 54 | if config.SimpleOutput == nil then 55 | data.update_audio_bitrate(nil); 56 | else 57 | data.update_encoder(config.SimpleOutput.StreamEncoder); 58 | data.update_audio_bitrate(config.SimpleOutput.ABitrate); 59 | end 60 | else 61 | if config.AdvOut == nil then 62 | data.update_audio_bitrate(nil); 63 | else 64 | data.update_encoder(config.AdvOut.Encoder); 65 | data.update_audio_bitrate(config.AdvOut.Track1Bitrate); 66 | end 67 | end 68 | end 69 | 70 | function profile_handler.parse_ini(ini_text) 71 | local data = {}; 72 | local section; 73 | 74 | for line in ini_text:gmatch("[^\r\n]+") do 75 | local tempSection = line:match('^%[([^%[%]]+)%]$'); 76 | 77 | if tempSection then 78 | section = tonumber(tempSection) and tonumber(tempSection) or tempSection; 79 | data[section] = data[section] or {}; 80 | end 81 | 82 | local param, value = line:match('^([%w|_]+)%s-=%s-(.+)$'); 83 | 84 | if param and value ~= nil then 85 | 86 | if tonumber(value) then 87 | value = tonumber(value); 88 | elseif value == 'true' then 89 | value = true; 90 | elseif value == 'false' then 91 | value = false; 92 | end 93 | 94 | if tonumber(param) then 95 | param = tonumber(param); 96 | end 97 | 98 | data[section][param] = value; 99 | end 100 | end 101 | return data; 102 | end 103 | 104 | function profile_handler.init_module() 105 | data = require("modules.data"); 106 | end 107 | 108 | return profile_handler; -------------------------------------------------------------------------------- /Bot-Commands.md: -------------------------------------------------------------------------------- 1 | # Bot Commands: 2 | 1. `!encoder [to_user]` 3 | 4 | ``` 5 | @to_user -> Encoder: $encoder 6 | ``` 7 | 2. `!output_mode [to_user]` (aliases: `!outputmode`) 8 | 9 | ``` 10 | @to_user -> Output Mode: $output_mode 11 | ``` 12 | 3. `!canvas_resolution [to_user]` (aliases: `!canvasresolution`) 13 | 14 | ``` 15 | @to_user -> Canvas Resolution: $canvas_resolution 16 | ``` 17 | 4. `!output_resolution [to_user]` (aliases: `!outputresolution`) 18 | 19 | ``` 20 | @to_user -> Output Resolution: $output_resolution 21 | ``` 22 | 5. `!missed_frames [to_user]` (aliases: `!missedframes` `!missed`) 23 | 24 | ``` 25 | @to_user -> Missed Frames: $missed_frames/$missed_total_frames ($missed_percents%) 26 | ``` 27 | 6. `!skipped_frames [to_user]` (aliases: `!skippedframes` `!skipped`) 28 | 29 | ``` 30 | @to_user -> Skipped Frames: $skipped_frames/$skipped_total_frames ($skipped_percents%) 31 | ``` 32 | 7. `!dropped_frames [to_user]` (aliases: `!droppedframes` `!dropped`) 33 | 34 | ``` 35 | @to_user -> Dropped Frames: $dropped_frames/$dropped_total_frames ($dropped_percents%) 36 | ``` 37 | 8. `!congestion [to_user]` 38 | 39 | ``` 40 | @to_user -> Congestion: $congestion% (Average: $average_congestion%) 41 | ``` 42 | 9. `!frame_time [to_user]` (aliases: `!render_time` `!frametime` `!rendertime`) 43 | 44 | ``` 45 | @to_user -> Average Frame Time: $average_frame_time ms 46 | ``` 47 | 10. `!fps [to_user]` (aliases: `!framerate`) 48 | 49 | ``` 50 | @to_user -> FPS: $fps/%target_fps (Average: %average_fps) 51 | ``` 52 | 11. `!memory_usage [to_user]` (aliases: `!memoryusage` `!memory`) 53 | 54 | ``` 55 | @to_user -> Memory Usage: $memory_usage MB 56 | ``` 57 | 12. `!cpu_cores [to_user]` (aliases: `!cpuccores` `!cores`) 58 | 59 | ``` 60 | @to_user -> CPU Cores: $cpu_cores 61 | ``` 62 | 13. `!cpu_usage [to_user]` (aliases: `!cpuusage`) 63 | 64 | ``` 65 | @to_user -> CPU Usage: $cpu_usage% 66 | ``` 67 | 14. `!audio_bitrate [to_user]` (aliases: `!audiobitrate`) 68 | 69 | ``` 70 | @to_user -> Audio Bitrate: $audio_bitrate kb/s 71 | ``` 72 | 15. `!recording_bitrate [to_user]` (aliases: `!recordingbitrate`) 73 | 74 | ``` 75 | @to_user -> Recording Bitrate: $recording_bitrate kb/s 76 | ``` 77 | 16. `!bitrate [to_user]` 78 | 79 | ``` 80 | @to_user -> Bitrate: $bitrate kb/s 81 | ``` 82 | 17. `!recording_bitrate [to_user]` (aliases: `!recordingbitrate`) 83 | 84 | ``` 85 | @to_user -> Recording Bitrate: $recording_bitrate kb/s 86 | ``` 87 | 18. `!streaming_duration [to_user]` (aliases: `!streamingduration`) 88 | 89 | ``` 90 | @to_user -> Streaming Duration: 03:32:59 91 | ``` 92 | 19. `!recording_duration [to_user]` (aliases: `!recordingduration`) 93 | 94 | ``` 95 | @to_user -> Recording Duration: 03:32:59 96 | ``` 97 | 20. `!streaming_status [to_user]` (aliases: `!streamingstatus`) 98 | 99 | ``` 100 | @to_user -> Streaming Status: Live 101 | ``` 102 | ``` 103 | @to_user -> Streaming Status: Reconnecting 104 | ``` 105 | ``` 106 | @to_user -> Streaming Status: Offline 107 | ``` 108 | 21. `!recording_status [to_user]` (aliases: `!recordingstatus`) 109 | 110 | ``` 111 | @to_user -> Recording Status: On 112 | ``` 113 | ``` 114 | @to_user -> Recording Status: Paused 115 | ``` 116 | ``` 117 | @to_user -> Recording Status: Off 118 | ``` 119 | 22. `!obs_static_stats [to_user]` (aliases: `!obsstaticstats`) 120 | 121 | ``` 122 | @to_user -> Encoder: $encoder, 123 | Output Mode: $output_mode, 124 | Canvas Resolution: $canvas_resolution, 125 | Output Resolution: $output_resolution, 126 | CPU Cores: $cpu_cores, 127 | Audio Bitrate: $audio_bitrate kb/s 128 | ``` 129 | 23. `!obsstats [to_user]` 130 | 131 | ``` 132 | @to_user -> Missed: $missed_frames/$missed_total_frames ($missed_percents%), 133 | Skipped: $skipped_frames/$skipped_total_frames ($skipped_percents%), 134 | Dropped: $dropped_frames/$dropped_total_frames ($dropped_percents%), 135 | Cong.: $congestion% (avg. $average_congestion%), 136 | Frame Fime: $average_frame_time ms, 137 | FPS: $fps/$target_fps (average: $average_fps), 138 | RAM: $memory_usage MB, 139 | CPU: $cpu_usage%, 140 | Bitrate: $bitrate kb/s 141 | ``` 142 | 143 | -------------------------------------------------------------------------------- /modules/utils.lua: -------------------------------------------------------------------------------- 1 | local utils = {}; 2 | 3 | local next = next; 4 | local type = type; 5 | local pairs = pairs; 6 | local string = string; 7 | local table = table; 8 | local tostring = tostring; 9 | local require = require; 10 | local math = math; 11 | 12 | function utils.table_tostring(table_) 13 | if type(table_) == "number" or type(table_) == "boolean" or type(table_) == "string" then 14 | return tostring(table_); 15 | end 16 | 17 | if utils.is_table_empty(table_) then 18 | return "{}"; 19 | end 20 | 21 | local cache = {}; 22 | local stack = {}; 23 | local output = {}; 24 | local depth = 1; 25 | local output_str = "{\n"; 26 | 27 | while true do 28 | local size = 0; 29 | for k,v in pairs(table_) do 30 | size = size + 1; 31 | end 32 | 33 | local cur_index = 1; 34 | for k,v in pairs(table_) do 35 | if cache[table_] == nil or cur_index >= cache[table_] then 36 | 37 | if string.find(output_str, "}", output_str:len()) then 38 | output_str = output_str .. ",\n"; 39 | elseif not string.find(output_str, "\n", output_str:len()) then 40 | output_str = output_str .. "\n"; 41 | end 42 | 43 | -- This is necessary for working with HUGE tables otherwise we run out of memory using concat on huge strings 44 | table.insert(output,output_str); 45 | output_str = ""; 46 | 47 | local key; 48 | if type(k) == "number" or type(k) == "boolean" then 49 | key = "[" .. tostring(k) .. "]"; 50 | else 51 | key = "['" .. tostring(k) .. "']"; 52 | end 53 | 54 | if type(v) == "number" or type(v) == "boolean" then 55 | output_str = output_str .. string.rep('\t', depth) .. key .. " = "..tostring(v); 56 | elseif type(v) == "table" then 57 | output_str = output_str .. string.rep('\t', depth) .. key .. " = {\n"; 58 | table.insert(stack, table_); 59 | table.insert(stack, v); 60 | cache[table_] = cur_index + 1; 61 | break; 62 | else 63 | output_str = output_str .. string.rep('\t', depth) .. key .. " = '" .. tostring(v) .. "'"; 64 | end 65 | 66 | if cur_index == size then 67 | output_str = output_str .. "\n" .. string.rep('\t', depth - 1) .. "}"; 68 | else 69 | output_str = output_str .. ","; 70 | end 71 | else 72 | -- close the table 73 | if cur_index == size then 74 | output_str = output_str .. "\n" .. string.rep('\t', depth - 1) .. "}"; 75 | end 76 | end 77 | 78 | cur_index = cur_index + 1; 79 | end 80 | 81 | if size == 0 then 82 | output_str = output_str .. "\n" .. string.rep('\t', depth - 1) .. "}"; 83 | end 84 | 85 | if #stack > 0 then 86 | table_ = stack[#stack]; 87 | stack[#stack] = nil; 88 | depth = cache[table_] == nil and depth + 1 or depth - 1; 89 | else 90 | break 91 | end 92 | end 93 | 94 | -- This is necessary for working with HUGE tables otherwise we run out of memory using concat on huge strings 95 | table.insert(output, output_str); 96 | output_str = table.concat(output); 97 | 98 | return output_str; 99 | end 100 | 101 | function utils.table_tostringln(table_) 102 | return "\n" .. utils.table_tostring(table_); 103 | end 104 | 105 | function utils.is_table_empty(table_) 106 | return next(table_) == nil; 107 | end 108 | 109 | function utils.is_NaN(value) 110 | return tostring(value) == tostring(0/0); 111 | end 112 | 113 | function utils.round(value) 114 | return math.floor(value + 0.5); 115 | end 116 | 117 | function utils.trim(str) 118 | return str:match("^%s*(.-)%s*$"); 119 | end 120 | 121 | function utils.starts_with(str, pattern) 122 | return str:find("^".. pattern) ~= nil; 123 | end 124 | 125 | function utils.init_module() 126 | end 127 | 128 | return utils; -------------------------------------------------------------------------------- /modules/data_format.lua: -------------------------------------------------------------------------------- 1 | local data_format = {}; 2 | 3 | local data; 4 | 5 | local require = require; 6 | local tostring = tostring; 7 | local string = string; 8 | 9 | data_format.stats = {}; 10 | 11 | function data_format.update() 12 | local stats = data.stats; 13 | 14 | data_format.stats.date_time = tostring(stats.date_time); 15 | data_format.stats.date = tostring(stats.date); 16 | data_format.stats.time = tostring(stats.time); 17 | 18 | data_format.stats.encoder = tostring(stats.encoder); 19 | data_format.stats.output_mode = tostring(stats.output_mode); 20 | 21 | data_format.stats.canvas_width = string.format("%d", stats.canvas_width); 22 | data_format.stats.canvas_height = string.format("%d", stats.canvas_height); 23 | data_format.stats.canvas_resolution = tostring(stats.canvas_resolution); 24 | 25 | data_format.stats.output_width = string.format("%d", stats.output_width); 26 | data_format.stats.output_height = string.format("%d", stats.output_height); 27 | data_format.stats.output_resolution = tostring(stats.output_resolution); 28 | 29 | data_format.stats.missed_frames = string.format("%d", stats.missed_frames); 30 | data_format.stats.total_missed_frames = string.format("%d", stats.total_missed_frames); 31 | data_format.stats.missed_percents = string.format("%.2f", 100 * stats.missed_percents); 32 | 33 | data_format.stats.skipped_frames = string.format("%d", stats.skipped_frames); 34 | data_format.stats.total_skipped_frames = string.format("%d", stats.total_skipped_frames); 35 | data_format.stats.skipped_percents = string.format("%.2f", 100 * stats.skipped_percents); 36 | 37 | data_format.stats.dropped_frames = string.format("%d", stats.dropped_frames); 38 | data_format.stats.total_dropped_frames = string.format("%d", stats.total_dropped_frames); 39 | data_format.stats.dropped_percents = string.format("%.2f", 100 * stats.dropped_percents); 40 | 41 | data_format.stats.congestion = string.format("%.2f", 100 * stats.congestion); 42 | data_format.stats.average_congestion = string.format("%.2f", 100 * stats.average_congestion); 43 | 44 | data_format.stats.average_frame_time = string.format("%.1f", stats.average_frame_time); 45 | data_format.stats.fps = string.format("%.2f", stats.fps); 46 | data_format.stats.target_fps = string.format("%d", stats.target_fps); 47 | data_format.stats.average_fps = string.format("%.2f", stats.average_fps); 48 | 49 | data_format.stats.memory_usage = string.format("%.1f", stats.memory_usage); 50 | data_format.stats.cpu_physical_cores = string.format("%d", stats.cpu_physical_cores); 51 | data_format.stats.cpu_logical_cores = string.format("%d", stats.cpu_logical_cores); 52 | data_format.stats.cpu_cores = tostring(stats.cpu_cores); 53 | data_format.stats.cpu_usage = string.format("%.2f", 100 * stats.cpu_usage); 54 | 55 | data_format.stats.audio_bitrate = string.format("%d", stats.audio_bitrate); 56 | data_format.stats.recording_bitrate = string.format("%d", stats.recording_bitrate); 57 | data_format.stats.bitrate = string.format("%d", stats.bitrate); 58 | 59 | data_format.stats.streaming_total_seconds = string.format("%d", stats.streaming_total_seconds); 60 | data_format.stats.streaming_total_minutes = string.format("%d", stats.streaming_total_minutes); 61 | data_format.stats.streaming_hours = string.format("%d", stats.streaming_hours); 62 | data_format.stats.streaming_minutes = string.format("%d", stats.streaming_minutes); 63 | data_format.stats.streaming_seconds = string.format("%d", stats.streaming_seconds); 64 | data_format.stats.streaming_duration = tostring(stats.streaming_duration); 65 | 66 | data_format.stats.recording_total_seconds = string.format("%d", stats.recording_total_seconds); 67 | data_format.stats.recording_total_minutes = string.format("%d", stats.recording_total_minutes); 68 | data_format.stats.recording_hours = string.format("%d", stats.recording_hours); 69 | data_format.stats.recording_minutes = string.format("%d", stats.recording_minutes); 70 | data_format.stats.recording_seconds = string.format("%d", stats.recording_seconds); 71 | data_format.stats.recording_duration = tostring(stats.recording_duration); 72 | 73 | data_format.stats.streaming_status = tostring(stats.streaming_status); 74 | data_format.stats.recording_status = tostring(stats.recording_status); 75 | end 76 | 77 | function data_format.init_module() 78 | data = require("modules.data"); 79 | end 80 | 81 | return data_format; -------------------------------------------------------------------------------- /modules/text_source_handler.lua: -------------------------------------------------------------------------------- 1 | local text_source_handler = {}; 2 | 3 | local data_format; 4 | local script_handler; 5 | local utils; 6 | 7 | local obslua = obslua; 8 | local print = print; 9 | local os = os; 10 | local tostring = tostring; 11 | local string = string; 12 | local require = require; 13 | 14 | text_source_handler.default_formatting = [[ 15 | Missed Frames: $missed_frames/$missed_total_frames ($missed_percents%) 16 | Skipped Frames: $skipped_frames/$skipped_total_frames ($skipped_percents%) 17 | Dropped Frames: $dropped_frames/$dropped_total_frames ($dropped_percents%) 18 | Congestion: $congestion% (avg. $average_congestion%) 19 | Memory Usage: $memory_usage MB 20 | CPU Usage: $cpu_usage% 21 | Frame Time: $average_frame_time ms 22 | FPS: $fps/$target_fps (avg. $average_fps) 23 | Bitrate: $bitrate kb/s 24 | ]]; 25 | 26 | function text_source_handler.update() 27 | --print("Text Source Update Tick."); 28 | 29 | local text_source_name = script_handler.text_source; 30 | 31 | if text_source_name == nil or text_source_name == "" then 32 | print("Text Source not specified."); 33 | return; 34 | end 35 | 36 | local source = obslua.obs_get_source_by_name(text_source_name); 37 | if source == nil then 38 | print("Text Source not found."); 39 | end 40 | 41 | -- Make a string for display in a text source 42 | local formatted_text = text_source_handler.format_text(script_handler.text_formatting); 43 | 44 | local settings = obslua.obs_data_create(); 45 | obslua.obs_data_set_string(settings, "text", formatted_text); 46 | obslua.obs_source_update(source, settings); 47 | obslua.obs_source_release(source); 48 | obslua.obs_data_release(settings); 49 | end 50 | 51 | function text_source_handler.format_text(text_formatting) 52 | local formatted_text = text_formatting; 53 | local formatted_stats = data_format.stats; 54 | 55 | formatted_text = formatted_text:gsub("$date_time", formatted_stats.date_time); 56 | formatted_text = formatted_text:gsub("$date", formatted_stats.date); 57 | formatted_text = formatted_text:gsub("$time", formatted_stats.time); 58 | 59 | formatted_text = formatted_text:gsub("$encoder", formatted_stats.encoder); 60 | formatted_text = formatted_text:gsub("$output_mode", formatted_stats.output_mode); 61 | 62 | formatted_text = formatted_text:gsub("$canvas_width", formatted_stats.canvas_width); 63 | formatted_text = formatted_text:gsub("$canvas_height", formatted_stats.canvas_height); 64 | formatted_text = formatted_text:gsub("$canvas_resolution", formatted_stats.canvas_resolution); 65 | 66 | formatted_text = formatted_text:gsub("$output_width", formatted_stats.output_width); 67 | formatted_text = formatted_text:gsub("$output_height", formatted_stats.output_height); 68 | formatted_text = formatted_text:gsub("$output_resolution", formatted_stats.output_resolution); 69 | 70 | formatted_text = formatted_text:gsub("$missed_frames", formatted_stats.missed_frames); 71 | formatted_text = formatted_text:gsub("$missed_total_frames", formatted_stats.total_missed_frames); 72 | formatted_text = formatted_text:gsub("$total_missed_frames", formatted_stats.total_missed_frames); 73 | formatted_text = formatted_text:gsub("$missed_percents", formatted_stats.missed_percents); 74 | 75 | formatted_text = formatted_text:gsub("$skipped_frames", formatted_stats.skipped_frames); 76 | formatted_text = formatted_text:gsub("$skipped_total_frames", formatted_stats.total_skipped_frames); 77 | formatted_text = formatted_text:gsub("$total_skipped_frames", formatted_stats.total_skipped_frames); 78 | formatted_text = formatted_text:gsub("$skipped_percents", formatted_stats.skipped_percents); 79 | 80 | formatted_text = formatted_text:gsub("$dropped_frames", formatted_stats.dropped_frames); 81 | formatted_text = formatted_text:gsub("$dropped_total_frames", formatted_stats.total_dropped_frames); 82 | formatted_text = formatted_text:gsub("$total_dropped_frames", formatted_stats.total_dropped_frames); 83 | formatted_text = formatted_text:gsub("$dropped_percents", formatted_stats.dropped_percents); 84 | 85 | formatted_text = formatted_text:gsub("$congestion", formatted_stats.congestion); 86 | formatted_text = formatted_text:gsub("$average_congestion", formatted_stats.average_congestion); 87 | 88 | formatted_text = formatted_text:gsub("$average_frame_time", formatted_stats.average_frame_time); 89 | formatted_text = formatted_text:gsub("$fps", formatted_stats.fps); 90 | formatted_text = formatted_text:gsub("$target_fps", formatted_stats.target_fps); 91 | formatted_text = formatted_text:gsub("$average_fps", formatted_stats.average_fps); 92 | 93 | formatted_text = formatted_text:gsub("$memory_usage", formatted_stats.memory_usage); 94 | formatted_text = formatted_text:gsub("$cpu_physical_cores", formatted_stats.cpu_physical_cores); 95 | formatted_text = formatted_text:gsub("$cpu_logical_cores", formatted_stats.cpu_logical_cores); 96 | formatted_text = formatted_text:gsub("$cpu_cores", formatted_stats.cpu_cores); 97 | formatted_text = formatted_text:gsub("$cpu_usage", formatted_stats.cpu_usage); 98 | 99 | formatted_text = formatted_text:gsub("$audio_bitrate", formatted_stats.audio_bitrate); 100 | formatted_text = formatted_text:gsub("$recording_bitrate", formatted_stats.recording_bitrate); 101 | formatted_text = formatted_text:gsub("$bitrate", formatted_stats.bitrate); 102 | 103 | formatted_text = formatted_text:gsub("$streaming_total_seconds", formatted_stats.streaming_total_seconds); 104 | formatted_text = formatted_text:gsub("$streaming_total_minutes", formatted_stats.streaming_total_minutes); 105 | formatted_text = formatted_text:gsub("$streaming_hours", formatted_stats.streaming_hours); 106 | formatted_text = formatted_text:gsub("$streaming_minutes", formatted_stats.streaming_minutes); 107 | formatted_text = formatted_text:gsub("$streaming_seconds", formatted_stats.streaming_seconds); 108 | formatted_text = formatted_text:gsub("$streaming_duration", formatted_stats.streaming_duration); 109 | 110 | formatted_text = formatted_text:gsub("$recording_total_seconds", formatted_stats.recording_total_seconds); 111 | formatted_text = formatted_text:gsub("$recording_total_minutes", formatted_stats.recording_total_minutes); 112 | formatted_text = formatted_text:gsub("$recording_hours", formatted_stats.recording_hours); 113 | formatted_text = formatted_text:gsub("$recording_minutes", formatted_stats.recording_minutes); 114 | formatted_text = formatted_text:gsub("$recording_seconds", formatted_stats.recording_seconds); 115 | formatted_text = formatted_text:gsub("$recording_duration", formatted_stats.recording_duration); 116 | 117 | formatted_text = formatted_text:gsub("$streaming_status", formatted_stats.streaming_status); 118 | formatted_text = formatted_text:gsub("$recording_status", formatted_stats.recording_status); 119 | 120 | return formatted_text; 121 | end 122 | 123 | function text_source_handler.init_module() 124 | data_format = require("modules.data_format"); 125 | script_handler = require("modules.script_handler"); 126 | utils = require("modules.utils"); 127 | end 128 | 129 | return text_source_handler; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

OBS Stats on Stream

3 |

Shows obs stats on stream and/or in Twitch chat. Supported data: encoder, output mode, canvas resolution, output resolution, missed frames, skipped frames, dropped frames, congestion, average frame time, fps, memory usage, cpu core count, cpu usage, audio bitrate, video bitrate, streaming duration, recording duration, streaming status and recording status.

4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Text Formatting VariablesBot Commands
18 | 19 |

20 | 21 | Contributors 22 | 23 | 24 | Issues 25 | 26 | 27 | Issues Closed 28 | 29 | 30 | Pull Requests 31 | 32 | 33 | Pull Requests Closed 34 | 35 |
36 | 37 | License 38 | 39 | 40 | Release Version 41 | 42 | 43 | Release Date 44 | 45 | 46 | Platform 47 | 48 | 49 | Maintenance 50 | 51 |
52 |
53 | 54 | Commits 55 | 56 | 57 | Last Commit 58 | 59 | 60 | Commits Since 61 | 62 |
63 | 64 | Commit Activity (Year) 65 | 66 | 67 | Commit Activity (Month) 68 | 69 | 70 | Commit Activity (Week) 71 | 72 |
73 |
74 | 75 | Repo Size 76 | 77 | 78 | Line Count 79 | 80 | 81 | Goto Counter 82 | 83 |
84 | 85 | Sponsors 86 | 87 | 88 | Watchers 89 | 90 | 91 | Forks 92 | 93 | 94 | Stars 95 | 96 | 97 | Hits 98 | 99 |
100 |
101 | 102 | Website 103 | 104 | 105 | Followers 106 | 107 | 108 | Twitter 109 | 110 | 111 | Twitch 112 | 113 |
114 | 115 | Author 116 | 117 | 118 | Open Source 119 | 120 | 121 | Written in 122 | 123 |

124 | 125 |

126 | 127 | 128 | 129 |

130 |

131 | 132 | 133 | 134 |

135 |

136 | 137 | 138 | 139 |

140 | 141 | # How to use 142 | 1. Download the script from the release page. Unzip the archive. 143 | 2. Add a text source to your scene. This source will be used to display the data. 144 | 3. Open Tools -> Scripts. Add the `OBS-Stats-on-Stream.lua` script. 145 | 4. Configure the script. 146 | * If you don't need Twitch Bot functionality, uncheck `Enable Bot` mark. 147 | * `Update Delay` determines how often the data will be updated. 1000 ms means once a second. 100 ms means 10 times a second. 148 | * `Bot Delay` determines how often the bot will read chat and write to it. 149 | 150 | * Enter bot's (or your own) nickname in `Bot Nickname` field. 151 | * Enter `Bot OAuth Password` for the bot's (or your own) twitch account. You can get it here: [click](https://twitchapps.com/tmi). 152 | * Enter `Channel Nickname` your bot gonna join (it gonna accept commands from this chat and print there). PLEASE, ONLY JOIN YOUR OWN CHANNEL. DO NOT TRY TO JOIN OTHER CHANNELS. 153 | * Link your created text source. 154 | * Modify `Text Formatting` if needed. all $name are variables and are replaced with actual values. 155 | 5. You are ready to go! 156 | 157 | >**:pushpin: NOTE:** If you don't need Text Source functionality, you don't need to add a text source and link it in the script. 158 | 159 | >**:pushpin: NOTE:** If you don't need Twitch Bot functionality, you can use `Enable Bot` checkbox to disable it. You also don't need to type `Bot Nickname`, `Bot OAuth Password` and `Channel Nickname` in that case. 160 | 161 | >**:pushpin: NOTE:** Bot only works on Twitch. I have no knowledge nor intentions to make it work on YT or any other platform. 162 | 163 | >**:pushpin: NOTE:** If you use source code instead of the release page, the script will not come precompiled into one file, but instead will be composed of many files. `OBS-Stats-on-Stream.lua` file and a bunch of files in `modules` folder. The file and the folder must be located in the same place and in OBS you only need to add `OBS-Stats-on-Stream.lua` as a script. 164 | 165 | # Contribution 166 | 167 | Big thanks to [jammehcow](https://github.com/jammehcow) for helping me with figuring out Socket functionality in Lua! 168 | 169 | OBS Docs are very confusing. If you want to contribute feel free to message me, make a pull request or open an issue! 170 | 171 | # Donate 172 | 173 | Another way to support me is donating! Thank you for using this script! 174 | 175 | 176 | Qries 177 | 178 | 179 | Qries 180 | 181 | -------------------------------------------------------------------------------- /modules/script_handler.lua: -------------------------------------------------------------------------------- 1 | local script_handler = {}; 2 | 3 | local data; 4 | local data_format; 5 | local bot; 6 | local output; 7 | local log; 8 | local profile_handler; 9 | local text_source_handler; 10 | 11 | local obslua = obslua; 12 | local ipairs = ipairs; 13 | local print = print; 14 | local require = require; 15 | 16 | local description = [[ 17 |

OBS Stats on Stream v2.1

18 |
Made by GreenComfyTea | 2024
19 |
Socials | Buy me a tea | Donate
20 |

Shows OBS Stats (like Bitrate, Dropped Frames and more) on Stream and/or in Twitch Chat.

21 |
Twitch Chat OAuth Password Generator
22 |
Text Formatting Variables | Bot commands
23 |
24 | ]]; 25 | 26 | local my_settings = nil; 27 | 28 | script_handler.is_script_enabled = true; 29 | script_handler.timer_delay = 1000; 30 | script_handler.text_source = ""; 31 | script_handler.text_formatting = ""; 32 | 33 | script_handler.is_output_to_file_enabled = false; 34 | script_handler.is_logging_enabled = false; 35 | 36 | script_handler.is_bot_enabled = true; 37 | script_handler.bot_delay = 2000; 38 | 39 | script_handler.bot_password = ""; 40 | script_handler.bot_nickname = "justinfan4269"; 41 | 42 | script_handler.channel_nickname = ""; 43 | 44 | script_handler.is_timer_on = false; 45 | 46 | function script_handler.tick() 47 | --print("Tick"); 48 | 49 | if not script_handler.is_script_enabled then 50 | return; 51 | end 52 | 53 | data.update(); 54 | data_format.update(); 55 | text_source_handler.update(); 56 | 57 | if script_handler.is_output_to_file_enabled then 58 | output.to_json(); 59 | end 60 | 61 | if script_handler.is_logging_enabled then 62 | log.to_file(); 63 | end 64 | end 65 | 66 | function script_handler.reset_formatting(properties, property) 67 | script_handler.text_formatting = text_source_handler.default_formatting; 68 | 69 | obslua.obs_data_set_string(my_settings, "text_formatting", text_source_handler.default_formatting); 70 | obslua.obs_properties_apply_settings(properties, my_settings); 71 | 72 | return true; 73 | end 74 | 75 | function script_handler.on_event(event) 76 | if event == obslua.OBS_FRONTEND_EVENT_STREAMING_STARTED then 77 | data.update_streaming_status(true); 78 | elseif event == obslua.OBS_FRONTEND_EVENT_STREAMING_STOPPED then 79 | data.update_streaming_status(false); 80 | elseif event == obslua.OBS_FRONTEND_EVENT_RECORDING_STARTED then 81 | data.update_recording_status(true, false); 82 | elseif event == obslua.OBS_FRONTEND_EVENT_RECORDING_STOPPED then 83 | data.update_recording_status(false, false); 84 | elseif event == obslua.OBS_FRONTEND_EVENT_RECORDING_PAUSED then 85 | data.update_recording_status(true, true); 86 | elseif event == obslua.OBS_FRONTEND_EVENT_RECORDING_UNPAUSED then 87 | data.update_recording_status(true, false); 88 | elseif event == obslua.OBS_FRONTEND_EVENT_FINISHED_LOADING 89 | or event == obslua.OBS_FRONTEND_EVENT_PROFILE_CHANGED 90 | or event == obslua.OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED 91 | or event == obslua.OBS_FRONTEND_EVENT_STREAMING_STARTING 92 | or event == obslua.OBS_FRONTEND_EVENT_STREAMING_STOPPING 93 | or event == obslua.OBS_FRONTEND_EVENT_RECORDING_STARTING 94 | or event == obslua.OBS_FRONTEND_EVENT_RECORDING_STOPPING then 95 | profile_handler.read_config(); 96 | end 97 | end 98 | 99 | function script_properties() 100 | local properties = obslua.obs_properties_create(); 101 | 102 | local enable_script_property = obslua.obs_properties_add_bool(properties, "is_script_enabled", "Enable Script"); 103 | local enable_bot_property = obslua.obs_properties_add_bool(properties, "is_bot_enabled", "Enable Bot"); 104 | 105 | local timer_delay_property = obslua.obs_properties_add_int(properties, "timer_delay", "Update Delay (ms)", 100, 10000, 100); 106 | obslua.obs_property_set_long_description(timer_delay_property, "Determines how often the data will update."); 107 | 108 | local bot_delay_property = obslua.obs_properties_add_int(properties, "bot_delay", "Bot Delay (ms)", 500, 5000, 100); 109 | obslua.obs_property_set_long_description(bot_delay_property, "Determines how often the bot will read chat and write to it."); 110 | 111 | local bot_nickname_property = obslua.obs_properties_add_text(properties, "bot_nickname", "Bot Nickname", obslua.OBS_TEXT_DEFAULT); 112 | obslua.obs_property_set_long_description(bot_nickname_property, "Nickname of your bot."); 113 | 114 | local bot_oauth_property = obslua.obs_properties_add_text(properties, "bot_password", "Bot OAuth Password", obslua.OBS_TEXT_PASSWORD); 115 | obslua.obs_property_set_long_description(bot_oauth_property, "Format: oauth:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. Visit https://twitchapps.com/tmi/ to get the AOuth Password for the bot (you must login to twitch.tv accordingly."); 116 | 117 | local channel_nickname_property = obslua.obs_properties_add_text(properties, "channel_nickname", "Channel Nickname", obslua.OBS_TEXT_DEFAULT); 118 | obslua.obs_property_set_long_description(channel_nickname_property, "Nickname of your channel for bot to join. If empty bot will join his own chat."); 119 | 120 | obslua.obs_properties_add_button(properties, "recconect_button", "Reconnect...", bot.reconnect); 121 | 122 | local text_source_property = obslua.obs_properties_add_list(properties, "text_source", "Text Source", obslua.OBS_COMBO_TYPE_EDITABLE, obslua.OBS_COMBO_FORMAT_STRING); 123 | 124 | local sources = obslua.obs_enum_sources(); 125 | if sources ~= nil then 126 | for _, source in ipairs(sources) do 127 | local source_id = obslua.obs_source_get_id(source); 128 | if source_id == "text_gdiplus_v2" or source_id == "text_ft2_source_v2" then 129 | local name = obslua.obs_source_get_name(source); 130 | obslua.obs_property_list_add_string(text_source_property, name, name); 131 | end 132 | end 133 | end 134 | obslua.source_list_release(sources); 135 | 136 | obslua.obs_property_set_long_description(text_source_property, "Text source that will be used to display the data."); 137 | 138 | obslua.obs_properties_add_text(properties, "text_formatting", "Text Formatting", obslua.OBS_TEXT_MULTILINE); 139 | obslua.obs_properties_add_button(properties, "reset_formatting_button", "Reset Formatting", script_handler.reset_formatting); 140 | 141 | obslua.obs_properties_apply_settings(properties, my_settings); 142 | 143 | local enable_output_to_file_property = obslua.obs_properties_add_bool(properties, "is_output_to_file_enabled", "Output to File"); 144 | local enable_logging_property = obslua.obs_properties_add_bool(properties, "is_logging_enabled", "Enable Logging"); 145 | 146 | return properties; 147 | end 148 | 149 | function script_defaults(settings) 150 | my_settings = settings; 151 | 152 | obslua.obs_data_set_default_bool(settings, "is_script_enabled", true); 153 | obslua.obs_data_set_default_bool(settings, "is_bot_enabled", true); 154 | 155 | obslua.obs_data_set_default_int(settings, "timer_delay", 1000); 156 | obslua.obs_data_set_default_int(settings, "bot_delay", 2000); 157 | 158 | obslua.obs_data_set_default_string(settings, "bot_nickname", ""); 159 | obslua.obs_data_set_default_string(settings, "bot_password", ""); 160 | obslua.obs_data_set_default_string(settings, "channel_nickname", ""); 161 | 162 | obslua.obs_data_set_default_string(settings, "text_source", ""); 163 | obslua.obs_data_set_default_string(settings, "text_formatting", text_source_handler.default_formatting); 164 | 165 | obslua.obs_data_set_default_bool(settings, "is_output_to_file_enabled", false); 166 | obslua.obs_data_set_default_bool(settings, "is_logging_enabled", false); 167 | 168 | print("Settings were reset."); 169 | end 170 | 171 | function script_update(settings) 172 | my_settings = settings; 173 | 174 | local previous_is_script_enabled = script_handler.is_script_enabled; 175 | local previous_is_bot_enabled = script_handler.is_bot_enabled; 176 | local previous_bot_delay = script_handler.bot_delay; 177 | local previous_bot_nickname = script_handler.bot_nickname; 178 | local previous_bot_password = script_handler.bot_password; 179 | local previous_channel_nickname = script_handler.channel_nickname; 180 | 181 | script_handler.is_script_enabled = obslua.obs_data_get_bool(settings, "is_script_enabled"); 182 | script_handler.is_bot_enabled = obslua.obs_data_get_bool(settings, "is_bot_enabled"); 183 | 184 | script_handler.timer_delay = obslua.obs_data_get_int(settings, "timer_delay"); 185 | script_handler.bot_delay = obslua.obs_data_get_int(settings, "bot_delay"); 186 | 187 | script_handler.bot_nickname = obslua.obs_data_get_string(settings, "bot_nickname"); 188 | script_handler.bot_password = obslua.obs_data_get_string(settings, "bot_password"); 189 | script_handler.channel_nickname = obslua.obs_data_get_string(settings, "channel_nickname"); 190 | 191 | script_handler.text_source = obslua.obs_data_get_string(settings, "text_source"); 192 | script_handler.text_formatting = obslua.obs_data_get_string(settings, "text_formatting"); 193 | 194 | script_handler.is_output_to_file_enabled = obslua.obs_data_get_bool(settings, "is_output_to_file_enabled"); 195 | script_handler.is_logging_enabled = obslua.obs_data_get_bool(settings, "is_logging_enabled"); 196 | 197 | local reconnect_bot = script_handler.is_script_enabled ~= previous_is_script_enabled 198 | or script_handler.is_bot_enabled ~= previous_is_bot_enabled 199 | or script_handler.bot_delay ~= previous_bot_delay 200 | or script_handler.bot_nickname ~= previous_bot_nickname 201 | or script_handler.bot_password ~= previous_bot_password 202 | or script_handler.channel_nickname ~= previous_channel_nickname; 203 | 204 | data.start_cpu_usage_info(); 205 | 206 | if script_handler.is_timer_on then 207 | script_handler.is_timer_on = false; 208 | obslua.timer_remove(script_handler.tick); 209 | 210 | print("Timer removed."); 211 | end 212 | 213 | if reconnect_bot then 214 | bot.close_socket(); 215 | end 216 | 217 | print("Settings were updated."); 218 | 219 | profile_handler.read_config(); 220 | data.update_cores_on_script_settings_changed(); 221 | data.update_streaming_status_on_script_settings_changed(); 222 | data.update_recording_status_on_script_settings_changed(); 223 | 224 | if script_handler.is_script_enabled then 225 | script_handler.is_timer_on = true; 226 | obslua.timer_add(script_handler.tick, script_handler.timer_delay); 227 | print("Timer added."); 228 | 229 | if reconnect_bot then 230 | bot.init_socket(); 231 | end 232 | end 233 | end 234 | 235 | function script_description() 236 | return description; 237 | end 238 | 239 | function script_load(settings) 240 | obslua.obs_frontend_add_event_callback(script_handler.on_event); 241 | print("Script is loaded."); 242 | end 243 | 244 | function script_unload() 245 | -- not working??? 246 | 247 | --if is_script_enabled then 248 | -- is_timer_on = false; 249 | -- obslua.timer_remove(data.tick); 250 | -- print("Timer removed."); 251 | --end 252 | 253 | bot.close_socket_on_unload(); 254 | 255 | print("[OBS-Stats-on-Stream.lua] Script unloaded."); 256 | end 257 | 258 | function script_handler.init_module() 259 | data = require("modules.data"); 260 | data_format = require("modules.data_format"); 261 | output = require("modules.output"); 262 | log = require("modules.log"); 263 | profile_handler = require("modules.profile_handler"); 264 | text_source_handler = require("modules.text_source_handler"); 265 | bot = require("modules.bot"); 266 | end 267 | 268 | return script_handler; -------------------------------------------------------------------------------- /modules/bot.lua: -------------------------------------------------------------------------------- 1 | local bot = {}; 2 | 3 | local script_handler; 4 | local data_format; 5 | local ljsocket; 6 | local utils; 7 | 8 | local obslua = obslua; 9 | local require = require; 10 | local assert = assert; 11 | local string = string; 12 | local print = print; 13 | local error = error; 14 | local tostring = tostring; 15 | 16 | local host = "irc.chat.twitch.tv"; 17 | local port = 6667; 18 | local bot_socket = nil; 19 | 20 | local bot_nickname = ""; 21 | local bot_password = ""; 22 | 23 | local auth_success = false; 24 | local auth_requested = false; 25 | 26 | local joined_channel = ""; 27 | 28 | local password_length = 36; 29 | local short_password_length = 30; 30 | 31 | local oauth_string = "oauth:"; 32 | 33 | bot.is_timer_on = false; 34 | 35 | function bot.tick() 36 | --print("Bot Tick"); 37 | 38 | if not script_handler.is_script_enabled then 39 | return; 40 | end 41 | 42 | if bot_socket:is_connected() then 43 | if not auth_success and not auth_requested then 44 | bot.auth(); 45 | end 46 | 47 | local response, err = bot.receive(); 48 | 49 | if response ~= nil then 50 | for line in response:gmatch("[^\n]+") do 51 | if not auth_success then 52 | auth_requested = false; 53 | if line:match(":tmi.twitch.tv 001") then 54 | bot_nickname = bot.get_real_nickname(line); 55 | print("Authentication success: " .. bot_nickname); 56 | auth_success = true; 57 | 58 | bot.join_channel(); 59 | goto continue; 60 | else 61 | print("Authentication to " .. bot_nickname .. " failed! Socket closed! Trying to reconnect..."); 62 | 63 | bot.reconnect(); 64 | return; 65 | end 66 | end 67 | 68 | if line:match("PING") then 69 | bot.send("PONG"); 70 | print("PING PONG"); 71 | goto continue; 72 | end 73 | 74 | if line:match("JOIN") then 75 | print("Joined Channel: " .. joined_channel); 76 | goto continue; 77 | end 78 | 79 | local i = 0; 80 | local to_user = ""; 81 | local command = ""; 82 | 83 | for word in line:gmatch("[^%s]+") do 84 | if i == 0 then 85 | local j = 0; 86 | for token in word:gmatch("[^!]+") do 87 | if j == 0 then 88 | to_user = token:sub(2); 89 | end 90 | j = j + 1; 91 | end 92 | 93 | end 94 | 95 | if i == 1 then 96 | if word ~= "PRIVMSG" then 97 | return; 98 | end 99 | end 100 | 101 | if i == 3 then 102 | command = word:sub(2):lower(); 103 | end 104 | 105 | if i == 4 then 106 | if not word:match("󠀀") then 107 | if word:match("^@") then 108 | to_user = word:sub(2); 109 | else 110 | to_user = word; 111 | end 112 | end 113 | 114 | end 115 | 116 | i = i + 1; 117 | end 118 | 119 | bot.process_commands(to_user, command); 120 | 121 | ::continue:: 122 | end 123 | end 124 | else 125 | bot_socket:poll_connect(); 126 | end 127 | end 128 | 129 | function bot.process_commands(to_user, command) 130 | local formatted_stats = data_format.stats; 131 | 132 | if command:match("^!encoder") then 133 | 134 | bot.send_message(string.format("@%s -> Encoder: %s", 135 | to_user, 136 | formatted_stats.encoder 137 | )); 138 | 139 | elseif command:match("^!output_mode") or command:match("^!outputmode") then 140 | 141 | bot.send_message(string.format("@%s -> Output Mode: %s", 142 | to_user, 143 | formatted_stats.output_mode 144 | )); 145 | 146 | elseif command:match("^!canvas_resolution") or command:match("^!canvasresolution") then 147 | 148 | bot.send_message(string.format("@%s -> Canvas Resolution: %s", 149 | to_user, 150 | formatted_stats.canvas_resolution 151 | )); 152 | 153 | elseif command:match("^!output_resolution") or command:match("^!outputresolution") then 154 | 155 | bot.send_message(string.format("@%s -> Output Resolution: %s", 156 | to_user, 157 | formatted_stats.output_resolution 158 | )); 159 | 160 | elseif command:match("^!missed_frames") or command:match("^!missedframes") or command:match("^!missed") then 161 | 162 | bot.send_message(string.format("@%s -> Missed Frames: %s/%s (%s%%)", 163 | to_user, 164 | formatted_stats.missed_frames, 165 | formatted_stats.total_missed_frames, 166 | formatted_stats.missed_percents 167 | )); 168 | 169 | elseif command:match("^!skipped_frames") or command:match("^!skippedframes") or command:match("^!skipped") then 170 | 171 | bot.send_message(string.format("@%s -> Skipped Frames: %s/%s (%s%%)", 172 | to_user, 173 | formatted_stats.skipped_frames, 174 | formatted_stats.total_skipped_frames, 175 | formatted_stats.skipped_percents 176 | )); 177 | 178 | elseif command:match("^!dropped_frames") or command:match("^!droppedframes") or command:match("^!dropped") then 179 | 180 | bot.send_message(string.format("@%s -> Dropped Frames: %s/%s (%s%%)", 181 | to_user, 182 | formatted_stats.dropped_frames, 183 | formatted_stats.total_dropped_frames, 184 | formatted_stats.dropped_percents 185 | )); 186 | 187 | elseif command:match("^!congestion") then 188 | 189 | bot.send_message(string.format("@%s -> Congestion: %s%% (Average: %s%%)", 190 | to_user, 191 | formatted_stats.congestion, 192 | formatted_stats.average_congestion 193 | )); 194 | 195 | elseif command:match("^!frame_time") or command:match("^!render_time") or command:match("^!frametime") or command:match("^!rendertime") then 196 | 197 | bot.send_message(string.format("@%s -> Average Frame Time: %s ms", 198 | to_user, 199 | formatted_stats.average_frame_time 200 | )); 201 | 202 | elseif command:match("^!fps") or command:match("^!framerate") then 203 | 204 | bot.send_message(string.format("@%s -> FPS: %s/%s (Average: %s)", 205 | to_user, 206 | formatted_stats.fps, 207 | formatted_stats.target_fps, 208 | formatted_stats.average_fps 209 | )); 210 | 211 | elseif command:match("^!memory_usage") or command:match("^!memoryusage") or command:match("^!memory") then 212 | 213 | bot.send_message(string.format("@%s -> Memory Usage: %s MB", 214 | to_user, 215 | formatted_stats.memory_usage 216 | )); 217 | 218 | elseif command:match("^!cpu_cores") or command:match("^!cpucores") or command:match("^!cores") then 219 | 220 | bot.send_message(string.format("@%s -> CPU Cores: %s", 221 | to_user, 222 | formatted_stats.cpu_cores 223 | )); 224 | 225 | elseif command:match("^!cpu_usage") or command:match("^!cpuusage") then 226 | 227 | bot.send_message(string.format("@%s -> CPU Usage: %s%%", 228 | to_user, 229 | formatted_stats.cpu_usage 230 | )); 231 | 232 | elseif command:match("^!audio_bitrate") or command:match("^!audiobitrate") then 233 | 234 | bot.send_message(string.format("@%s -> Audio Bitrate: %s kb/s", 235 | to_user, 236 | formatted_stats.audio_bitrate 237 | )); 238 | 239 | elseif command:match("^!bitrate") then 240 | 241 | bot.send_message(string.format("@%s -> Bitrate: %s kb/s", 242 | to_user, 243 | formatted_stats.bitrate 244 | )); 245 | 246 | elseif command:match("^!recording_bitrate") or command:match("^!recordingbitrate")then 247 | 248 | bot.send_message(string.format("@%s -> Recording Bitrate: %s kb/s", 249 | to_user, 250 | formatted_stats.recording_bitrate 251 | )); 252 | 253 | elseif command:match("^!streaming_duration") or command:match("^!streamingduration") then 254 | 255 | bot.send_message(string.format("@%s -> Streaming Duration: %s", 256 | to_user, 257 | formatted_stats.streaming_duration 258 | )); 259 | 260 | elseif command:match("^!recording_duration") or command:match("^!recordingduration") then 261 | 262 | bot.send_message(string.format("@%s -> Recording Duration: %s", 263 | to_user, 264 | formatted_stats.recording_duration 265 | )); 266 | 267 | elseif command:match("^!streaming_status") or command:match("^!streamingstatus") then 268 | 269 | bot.send_message(string.format("@%s -> Streaming Status: %s", 270 | to_user, 271 | formatted_stats.streaming_status 272 | )); 273 | 274 | elseif command:match("^!recording_status") or command:match("^!recordingstatus") then 275 | 276 | bot.send_message(string.format("@%s -> Recording Status: %s", 277 | to_user, 278 | formatted_stats.recording_status 279 | )); 280 | 281 | elseif command:match("^!obs_static_stats") or command:match("^!obsstaticstats") then 282 | 283 | bot.send_message(string.format("@%s -> Encoder: %s, Output Mode: %s, Canvas Resolution: %s, Output Resolution: %s, CPU Cores: %s, Audio Bitrate: %s kb/s", 284 | to_user, 285 | formatted_stats.encoder, 286 | formatted_stats.output_mode, 287 | formatted_stats.canvas_resolution, 288 | formatted_stats.output_resolution, 289 | formatted_stats.cpu_cores, 290 | formatted_stats.audio_bitrate 291 | )); 292 | 293 | elseif command:match("^!obs_stats") or command:match("^!obsstats") or command:match("^!obs_dynamic_stats") or command:match("^!obsdynamicstats") then 294 | 295 | bot.send_message(string.format("@%s -> Missed: %s/%s (%s%%), Skipped: %s/%s (%s%%), Dropped: %s/%s (%s%%), Cong.: %s%% (average: %s%%), Frame Time: %s ms, FPS: %s/%s (average: %s), RAM: %s MB, CPU: %s%%, Bitrate: %s kb/s", 296 | to_user, 297 | formatted_stats.missed_frames, 298 | formatted_stats.total_missed_frames, 299 | formatted_stats.missed_percents, 300 | formatted_stats.skipped_frames, 301 | formatted_stats.total_skipped_frames, 302 | formatted_stats.skipped_percents, 303 | formatted_stats.dropped_frames, 304 | formatted_stats.total_dropped_frames, 305 | formatted_stats.dropped_percents, 306 | formatted_stats.congestion, 307 | formatted_stats.average_congestion, 308 | formatted_stats.average_frame_time, 309 | formatted_stats.fps, 310 | formatted_stats.target_fps, 311 | formatted_stats.average_fps, 312 | formatted_stats.memory_usage, 313 | formatted_stats.cpu_usage, 314 | formatted_stats.bitrate 315 | )); 316 | end 317 | end 318 | 319 | function bot.auth() 320 | print("Authentication attempt: " .. bot_nickname); 321 | auth_requested = true; 322 | assert(bot_socket:send( 323 | string.format("PASS %s\r\nNICK %s\r\n", bot_password, bot_nickname) 324 | )); 325 | end 326 | 327 | function bot.send(message) 328 | if message ~= "PONG" then 329 | print(string.format("Sending Message: %s", message)); 330 | end 331 | 332 | assert(bot_socket:send( 333 | string.format("%s\r\n", message) 334 | )); 335 | end 336 | 337 | function bot.send_message(message) 338 | print(string.format("Sending Message to %s: %s", joined_channel, message)); 339 | assert(bot_socket:send( 340 | string.format("PRIVMSG #%s :%s\r\n", joined_channel, message) 341 | )); 342 | end 343 | 344 | function bot.receive() 345 | local response, err = bot_socket:receive(); 346 | 347 | if response ~= nil then 348 | return response; 349 | elseif err ~= nil then 350 | if err == "timeout" then 351 | return nil; 352 | --"An established connection was aborted by the software in your host machine." 353 | elseif err:match("An established connection was aborted") then 354 | print(tostring(err)); 355 | bot.reconnect(); 356 | return nil; 357 | else 358 | print(tostring(err)); 359 | return nil; 360 | end 361 | else 362 | print("Unknown Error"); 363 | return nil; 364 | end 365 | end 366 | 367 | function bot.join_channel(channel) 368 | if channel == nil or channel == "" then 369 | channel = script_handler.channel_nickname; 370 | end 371 | 372 | if channel == nil or channel == "" then 373 | channel = bot_nickname; 374 | end 375 | 376 | joined_channel = channel; 377 | bot.send("JOIN #" .. channel); 378 | 379 | end 380 | 381 | function bot.get_real_nickname(line) 382 | local i = 0; 383 | for word in line:gmatch("[^%s]+") do 384 | if i == 2 then 385 | return word; 386 | end 387 | i = i + 1; 388 | end 389 | end 390 | 391 | function bot.reconnect() 392 | print("Reconnecting..."); 393 | 394 | bot.close_socket(); 395 | bot.init_socket(); 396 | end 397 | 398 | function bot.init_socket() 399 | if not script_handler.is_bot_enabled then 400 | return; 401 | end 402 | 403 | local nickname = bot.validate_nickname(script_handler.bot_nickname); 404 | local password = bot.validate_password(script_handler.bot_password); 405 | 406 | if nickname ~= nil then 407 | bot_nickname = nickname; 408 | else 409 | return; 410 | end 411 | 412 | if password ~= nil then 413 | bot_password = password; 414 | else 415 | return; 416 | end 417 | 418 | bot_socket = assert(ljsocket.create("inet", "stream", "tcp")); 419 | assert(bot_socket:set_blocking(false)); 420 | assert(bot_socket:connect(host, port)); 421 | 422 | if not bot.is_timer_on then 423 | bot.is_timer_on = true; 424 | obslua.timer_add(bot.tick, script_handler.bot_delay); 425 | 426 | print("Bot Timer added."); 427 | end 428 | end 429 | 430 | function bot.close_socket() 431 | if bot.is_timer_on then 432 | bot.is_timer_on = false; 433 | obslua.timer_remove(bot.tick); 434 | 435 | print("Bot Timer removed."); 436 | end 437 | 438 | if bot_socket ~= nil then 439 | bot_socket:close(); 440 | bot_socket = nil; 441 | end 442 | 443 | bot.reset_data(); 444 | end 445 | 446 | function bot.close_socket_on_unload() 447 | if bot_socket ~= nil then 448 | bot_socket:close(); 449 | end 450 | end 451 | 452 | function bot.validate_nickname(nickname) 453 | if nickname == nil then 454 | print("No Bot Nickname provided."); 455 | return nil; 456 | end 457 | 458 | nickname = utils.trim(nickname):lower(); 459 | 460 | if nickname == "" then 461 | print("No Bot Nickname provided."); 462 | return nil; 463 | end 464 | 465 | return nickname; 466 | end 467 | 468 | function bot.validate_password(password) 469 | if password == nil then 470 | print("No Bot OAuth Password provided."); 471 | return nil; 472 | end 473 | 474 | password = utils.trim(password):lower(); 475 | 476 | if password == "" then 477 | print("No Bot OAuth Password provided."); 478 | return nil; 479 | end 480 | 481 | if utils.starts_with(password, oauth_string) then 482 | if #password ~= password_length then 483 | print("Incorrect OAuth Password provided."); 484 | return nil; 485 | end 486 | else 487 | if #password ~= short_password_length then 488 | print("Incorrect OAuth Password provided."); 489 | return nil; 490 | end 491 | 492 | password = oauth_string .. password; 493 | end 494 | 495 | return password; 496 | end 497 | 498 | function bot.reset_data() 499 | auth_success = false; 500 | auth_requested = false; 501 | end 502 | 503 | function bot.init_module() 504 | script_handler = require("modules.script_handler"); 505 | data_format = require("modules.data_format"); 506 | ljsocket = require("modules.ljsocket"); 507 | utils = require("modules.utils"); 508 | end 509 | 510 | return bot; -------------------------------------------------------------------------------- /modules/data.lua: -------------------------------------------------------------------------------- 1 | local data = {}; 2 | 3 | local ffi; 4 | local text_source_handler; 5 | local utils; 6 | 7 | local obslua = obslua; 8 | local print = print; 9 | local tostring = tostring; 10 | local math = math; 11 | local require = require; 12 | local string = string; 13 | local os = os; 14 | 15 | local obsffi = nil; 16 | local bytes_to_megabytes = 1024 * 1024; 17 | local ns_to_ms = 1000000; 18 | local ms_to_s = 1000; 19 | local s_to_m = 60; 20 | local s_to_h = 3600; 21 | local cpu_info = nil; 22 | local bitrate_update_delay = 2000; 23 | 24 | local default_encoder = "x264" 25 | local default_canvas_width = 1920; 26 | local default_canvas_resolution = "1920x1080"; 27 | local default_output_resolution = "1280x720"; 28 | local default_target_fps = 30; 29 | 30 | local defaults = { 31 | encoder = "x264?", 32 | output_mode = "Simple", 33 | canvas_width = 1920, 34 | canvas_height = 1080, 35 | canvas_resolution = "1920x1080?", 36 | output_width = 1280, 37 | output_height = 720, 38 | output_resolution = "1280x720", 39 | target_fps = 30, 40 | audio_bitrate = 160 41 | } 42 | 43 | local cumulative_fps = 0; 44 | local cumulative_congestion = 0; 45 | local last_bitrate_update_time = 0; 46 | local last_bytes_sent = 0; 47 | local last_bytes_recorded = 0; 48 | 49 | data.ticks = 0; 50 | 51 | data.streaming_statuses = { 52 | live = "Live", 53 | reconnecting = "Reconnecting", 54 | offline = "Offline" 55 | }; 56 | 57 | data.recording_statuses = { 58 | on = "On", 59 | paused = "Paused", 60 | off = "Off" 61 | }; 62 | 63 | data.output_modes = { 64 | simple = "Simple", 65 | advanced = "Advanced" 66 | }; 67 | 68 | data.stats = { 69 | date = "01.01.1970", 70 | time = "00:00:00", 71 | date_time = "01.01.1970 00:00:00", 72 | encoder = defaults.encoder, 73 | output_mode = defaults.output_mode, 74 | canvas_width = defaults.canvas_width, 75 | canvas_height = defaults.canvas_height, 76 | canvas_resolution = defaults.canvas_resolution, 77 | output_width = defaults.output_width, 78 | output_height = defaults.output_height, 79 | output_resolution = defaults.output_resolution, 80 | missed_frames = 0, 81 | total_missed_frames = 0, 82 | missed_percents = 0, 83 | skipped_frames = 0, 84 | total_skipped_frames = 0, 85 | skipped_percents = 0, 86 | dropped_frames = 0, 87 | total_dropped_frames = 0, 88 | dropped_percents = 0, 89 | congestion = 0, 90 | average_congestion = 0, 91 | average_frame_time = 0, 92 | fps = 0, 93 | target_fps = defaults.target_fps, 94 | average_fps = 0, 95 | memory_usage = 0, 96 | cpu_physical_cores = 0, 97 | cpu_logical_cores = 0, 98 | cpu_cores = "0C/0T", 99 | cpu_usage = 0, 100 | audio_bitrate = defaults.audio_bitrate, 101 | recording_bitrate = 0, 102 | bitrate = 0, 103 | streaming_total_seconds = 0, 104 | streaming_total_minutes = 0, 105 | streaming_hours = 0, 106 | streaming_minutes = 0, 107 | streaming_seconds = 0; 108 | streaming_duration = "00:00:00", 109 | recording_total_seconds = 0, 110 | recording_total_minutes = 0, 111 | recording_hours = 0, 112 | recording_minutes = 0, 113 | recording_seconds = 0; 114 | recording_duration = "00:00:00", 115 | streaming_status = "Offline", 116 | recording_status = "Off" 117 | } 118 | 119 | function data.update() 120 | --print("Data Update Tick."); 121 | 122 | data.ticks = data.ticks + 1; 123 | 124 | -- streaming_output will be nil when not actually streaming 125 | local streaming_output = obslua.obs_frontend_get_streaming_output(); 126 | local recording_output = obslua.obs_frontend_get_recording_output(); 127 | 128 | local bitrate_time_passed = data.update_bitrate_time_passed(); 129 | 130 | data.update_time(); 131 | data.update_cpu_usage(); 132 | data.update_memory_usage(); 133 | data.update_fps(); 134 | data.update_average_frame_time(); 135 | data.update_missed_frames(); 136 | data.update_skipped_frames(); 137 | data.update_dropped_frames(streaming_output); 138 | data.update_congestion(streaming_output); 139 | data.update_reconnection_status(streaming_output); 140 | data.update_streaming_bitrate(streaming_output, bitrate_time_passed); 141 | data.update_streaming_duration(); 142 | data.update_recording_bitrate(recording_output, bitrate_time_passed); 143 | data.update_recording_duration(recording_output); 144 | 145 | if streaming_output ~= nil then 146 | obslua.obs_output_release(streaming_output); 147 | end 148 | 149 | if recording_output ~= nil then 150 | obslua.obs_output_release(recording_output); 151 | end 152 | end 153 | 154 | function data.update_time() 155 | data.stats.date = os.date("%d.%m.%Y"); 156 | data.stats.time = os.date("%X"); 157 | data.stats.date_time = os.date("%d.%m.%Y %X"); 158 | end 159 | 160 | function data.update_bitrate_time_passed() 161 | local current_time = obslua.os_gettime_ns(); 162 | local time_passed = (current_time - last_bitrate_update_time) / ns_to_ms; 163 | 164 | if time_passed >= bitrate_update_delay then 165 | last_bitrate_update_time = current_time; 166 | end 167 | 168 | return time_passed; 169 | end 170 | 171 | function data.start_cpu_usage_info() 172 | if obsffi == nil then 173 | return; 174 | end 175 | 176 | data.destroy_cpu_usage_info(); 177 | 178 | data.cpu_info = obsffi.os_cpu_usage_info_start(); 179 | end 180 | 181 | function data.destroy_cpu_usage_info() 182 | if data.cpu_info == nil or obsffi == nil then 183 | return; 184 | end 185 | 186 | obsffi.os_cpu_usage_info_destroy(data.cpu_info); 187 | data.cpu_info = nil; 188 | end 189 | 190 | function data.update_cpu_usage() 191 | if data.cpu_info == nil or obsffi == nil then 192 | return; 193 | end 194 | 195 | local cpu_usage = obsffi.os_cpu_usage_info_query(data.cpu_info); 196 | 197 | if cpu_usage ~= nil then 198 | data.stats.cpu_usage = cpu_usage / 100; 199 | end 200 | 201 | end 202 | 203 | function data.update_memory_usage() 204 | local memory_usage = obslua.os_get_proc_resident_size() / bytes_to_megabytes; 205 | 206 | if memory_usage ~= nil then 207 | data.stats.memory_usage = memory_usage; 208 | end 209 | end 210 | 211 | function data.update_fps() 212 | local fps = obslua.obs_get_active_fps(); 213 | 214 | if fps ~= nil then 215 | data.stats.fps = fps; 216 | cumulative_fps = cumulative_fps + fps; 217 | data.stats.average_fps = cumulative_fps / data.ticks; 218 | end 219 | end 220 | 221 | function data.update_average_frame_time() 222 | local average_frame_time = obslua.obs_get_average_frame_time_ns() / ns_to_ms; 223 | 224 | if average_frame_time ~= nil then 225 | data.stats.average_frame_time = average_frame_time; 226 | end 227 | end 228 | 229 | function data.update_missed_frames() 230 | local total_missed_frames = obslua.obs_get_total_frames(); -- total rendered frames 231 | local missed_frames = obslua.obs_get_lagged_frames(); -- lagged frames 232 | 233 | if total_missed_frames ~= nil then 234 | data.stats.total_missed_frames = total_missed_frames; 235 | end 236 | 237 | if missed_frames ~= nil then 238 | data.stats.missed_frames = missed_frames; 239 | end 240 | 241 | if data.stats.total_missed_frames == 0 then 242 | data.stats.missed_percents = 0; 243 | else 244 | data.stats.missed_percents = data.stats.missed_frames / data.stats.total_missed_frames; 245 | end 246 | end 247 | 248 | function data.update_skipped_frames() 249 | if obsffi == nil then 250 | return; 251 | end 252 | 253 | local video = obsffi.obs_get_video(); 254 | 255 | if video == nil then 256 | return; 257 | end 258 | 259 | local total_skipped_frames = obsffi.video_output_get_total_frames(video); -- total encoded frames 260 | local skipped_frames = obsffi.video_output_get_skipped_frames(video); -- skipped frames 261 | 262 | if total_skipped_frames ~= nil then 263 | data.stats.total_skipped_frames = total_skipped_frames; 264 | end 265 | 266 | if skipped_frames ~= nil then 267 | data.stats.skipped_frames = skipped_frames; 268 | end 269 | 270 | if data.stats.total_skipped_frames == 0 then 271 | data.stats.skipped_percents = 0; 272 | else 273 | data.stats.skipped_percents = data.stats.skipped_frames / data.stats.total_skipped_frames; 274 | end 275 | end 276 | 277 | function data.update_dropped_frames(streaming_output) 278 | if streaming_output == nil then 279 | return; 280 | end 281 | 282 | local dropped_frames = obslua.obs_output_get_frames_dropped(streaming_output); -- dropped frames 283 | local total_frames = obslua.obs_output_get_total_frames(streaming_output); -- total dropped frames 284 | 285 | if total_frames ~= nil then 286 | data.stats.total_dropped_frames = total_frames; 287 | end 288 | 289 | if dropped_frames ~= nil then 290 | data.stats.dropped_frames = dropped_frames; 291 | end 292 | 293 | if data.stats.total_dropped_frames == 0 then 294 | data.stats.dropped_percents = 0; 295 | else 296 | data.stats.dropped_percents = data.stats.dropped_frames / data.stats.total_dropped_frames; 297 | end 298 | end 299 | 300 | function data.update_congestion(streaming_output) 301 | if streaming_output == nil then 302 | return; 303 | end 304 | 305 | local congestion = obslua.obs_output_get_congestion(streaming_output); 306 | 307 | -- Check that congestion is not NaN 308 | if(congestion ~= nil and not utils.is_NaN(congestion)) then 309 | cumulative_congestion = cumulative_congestion + congestion; 310 | 311 | data.stats.congestion = congestion; 312 | data.stats.average_congestion = cumulative_congestion / data.ticks; 313 | end 314 | end 315 | 316 | function data.update_reconnection_status(streaming_output) 317 | if streaming_output == nil then 318 | return; 319 | end 320 | 321 | if data.stats.streaming_status == data.streaming_statuses.live 322 | or data.stats.streaming_status == data.streaming_statuses.reconnecting then 323 | 324 | local is_reconnecting = obslua.obs_output_reconnecting(streaming_output); 325 | if is_reconnecting then 326 | data.stats.streaming_status = data.streaming_statuses.reconnecting; 327 | else 328 | data.stats.streaming_status = data.streaming_statuses.live; 329 | end 330 | end 331 | end 332 | 333 | function data.update_streaming_status(is_live) 334 | if is_live then 335 | data.stats.streaming_status = data.streaming_statuses.live; 336 | else 337 | data.stats.streaming_status = data.streaming_statuses.offline; 338 | data.stats.bitrate = 0; 339 | end 340 | end 341 | 342 | function data.update_recording_status(is_recording, is_paused) 343 | if is_recording then 344 | if is_paused then 345 | data.stats.recording_status = data.recording_statuses.paused; 346 | else 347 | data.stats.recording_status = data.recording_statuses.on; 348 | end 349 | 350 | else 351 | data.stats.recording_status = data.recording_statuses.off; 352 | data.stats.recording_bitrate = 0; 353 | end 354 | end 355 | 356 | function data.update_streaming_bitrate(streaming_output, time_passed) 357 | if streaming_output == nil then 358 | return; 359 | end 360 | 361 | if time_passed < bitrate_update_delay then 362 | return; 363 | end 364 | 365 | local bytes_sent = obslua.obs_output_get_total_bytes(streaming_output); 366 | 367 | if bytes_sent == nil then 368 | return; 369 | end 370 | 371 | -- the fck is this? 372 | if bytes_sent < last_bytes_sent then 373 | bytes_sent = 0; 374 | end 375 | 376 | local bits_between = (bytes_sent - last_bytes_sent) * 8; 377 | local bitrate = bits_between / time_passed; 378 | 379 | last_bytes_sent = bytes_sent; 380 | 381 | if bitrate ~= nil then 382 | data.stats.bitrate = bitrate; 383 | end 384 | end 385 | 386 | function data.update_recording_bitrate(recording_output, time_passed) 387 | if recording_output == nil then 388 | return; 389 | end 390 | 391 | if time_passed < bitrate_update_delay then 392 | return; 393 | end 394 | 395 | local bytes_recorded = obslua.obs_output_get_total_bytes(recording_output); 396 | 397 | if bytes_recorded == nil then 398 | return; 399 | end 400 | 401 | -- what the fck is this? 402 | if bytes_recorded < last_bytes_recorded then 403 | bytes_recorded = 0; 404 | end 405 | 406 | local recording_bits_between = (bytes_recorded - last_bytes_recorded) * 8; 407 | local recording_bitrate = recording_bits_between / time_passed / ms_to_s; 408 | 409 | last_bytes_recorded = bytes_recorded; 410 | 411 | if recording_bitrate ~= nil then 412 | data.stats.recording_bitrate = recording_bitrate; 413 | end 414 | end 415 | 416 | function data.update_streaming_duration() 417 | -- Needs better approach? 418 | -- Duration is incorrect if fps is not stable 419 | 420 | if data.stats.streaming_status == data.streaming_statuses.offline then 421 | data.stats.streaming_total_minutes = 0; 422 | data.stats.streaming_total_seconds = 0; 423 | data.stats.streaming_hours = 0; 424 | data.stats.streaming_minutes = 0; 425 | data.stats.streaming_seconds = 0; 426 | data.stats.streaming_duration = "00:00:00"; 427 | 428 | return; 429 | end 430 | 431 | local fps = data.stats.fps; 432 | 433 | local streaming_total_seconds = data.stats.total_dropped_frames; 434 | local streaming_total_minutes = 0; 435 | local streaming_hours = 0; 436 | local streaming_minutes = 0; 437 | local streaming_seconds = 0; 438 | 439 | if fps ~= 0 then 440 | streaming_total_seconds = streaming_total_seconds / fps; 441 | end 442 | 443 | streaming_hours = math.floor(streaming_total_seconds / 3600); 444 | streaming_minutes = math.floor((streaming_total_seconds % 3600) / 60); 445 | streaming_seconds = math.floor(0.5 + streaming_total_seconds % 60); 446 | 447 | data.stats.streaming_total_minutes = math.floor(streaming_total_seconds / 60); 448 | data.stats.streaming_total_seconds = streaming_total_seconds; 449 | 450 | data.stats.streaming_hours = streaming_hours; 451 | data.stats.streaming_minutes = streaming_minutes; 452 | data.stats.streaming_seconds = streaming_seconds; 453 | data.stats.streaming_duration = string.format("%.2d:%.2d:%.2d", streaming_hours, streaming_minutes, streaming_seconds); 454 | end 455 | 456 | function data.update_recording_duration(recording_output) 457 | -- Needs better approach? 458 | -- Duration is incorrect if fps is not stable 459 | 460 | if recording_output == nil then 461 | return; 462 | end 463 | 464 | if data.stats.recording_status == data.recording_statuses.off then 465 | data.stats.recording_total_minutes = 0; 466 | data.stats.recording_total_seconds = 0; 467 | data.stats.recording_hours = 0; 468 | data.stats.recording_minutes = 0; 469 | data.stats.recording_seconds = 0; 470 | data.stats.recording_duration = "00:00:00"; 471 | 472 | return; 473 | end 474 | 475 | local recording_total_frames = obslua.obs_output_get_total_frames(recording_output); 476 | 477 | if recording_total_frames == nil then 478 | return; 479 | end 480 | 481 | local fps = data.stats.fps; 482 | 483 | local recording_total_seconds = recording_total_frames; 484 | local recording_total_minutes = 0; 485 | local recording_hours = 0; 486 | local recording_minutes = 0; 487 | local recording_seconds = 0; 488 | 489 | if fps ~= 0 then 490 | recording_total_seconds = recording_total_seconds / fps; 491 | end 492 | 493 | recording_hours = math.floor(recording_total_seconds / 3600); 494 | recording_minutes = math.floor((recording_total_seconds % 3600) / 60); 495 | recording_seconds = math.floor(0.5 + recording_total_seconds % 60); 496 | 497 | data.stats.recording_total_minutes = math.floor(recording_total_seconds / 60); 498 | data.stats.recording_total_seconds = recording_total_seconds; 499 | 500 | data.stats.recording_hours = recording_hours; 501 | data.stats.recording_minutes = recording_minutes; 502 | data.stats.recording_seconds = recording_seconds; 503 | data.stats.recording_duration = string.format("%.2d:%.2d:%.2d", recording_hours, recording_minutes, recording_seconds); 504 | end 505 | 506 | function data.update_output_mode(output_mode) 507 | if output_mode == nil then 508 | data.stats.output_mode = defaults.output_mode; 509 | else 510 | data.stats.output_mode = output_mode; 511 | end 512 | end 513 | 514 | function data.update_encoder(encoder) 515 | if encoder ~= nil then 516 | data.stats.encoder = encoder; 517 | end 518 | end 519 | 520 | function data.update_canvas_resolution(width, height) 521 | if width ~= nil then 522 | data.stats.canvas_width = width; 523 | end 524 | 525 | if height ~= nil then 526 | data.stats.canvas_height = height; 527 | end 528 | 529 | if width ~= nil or height ~= nil then 530 | data.stats.canvas_resolution = string.format("%dx%d", data.stats.canvas_width, data.stats.canvas_height); 531 | end 532 | end 533 | 534 | function data.update_output_resolution(width, height) 535 | if width == nil then 536 | data.stats.output_width = defaults.output_width; 537 | else 538 | data.stats.output_width = width; 539 | end 540 | 541 | if height == nil then 542 | data.stats.output_height = defaults.output_height; 543 | else 544 | data.stats.output_height = height; 545 | end 546 | 547 | data.stats.output_resolution = string.format("%dx%d", data.stats.output_width, data.stats.output_height); 548 | end 549 | 550 | function data.update_target_fps(target_fps) 551 | if target_fps == nil then 552 | data.stats.target_fps = defaults.target_fps; 553 | else 554 | data.stats.target_fps = target_fps; 555 | end 556 | end 557 | 558 | function data.update_audio_bitrate(audio_bitrate) 559 | if audio_bitrate == nil then 560 | data.stats.audio_bitrate = defaults.audio_bitrate; 561 | else 562 | data.stats.audio_bitrate = audio_bitrate; 563 | end 564 | end 565 | 566 | function data.update_cores_on_script_settings_changed() 567 | local physical_cores = obslua.os_get_physical_cores(); 568 | local logical_cores = obslua.os_get_logical_cores(); 569 | 570 | if physical_cores ~= nil then 571 | data.stats.cpu_physical_cores = physical_cores; 572 | end 573 | 574 | if logical_cores ~= nil then 575 | data.stats.cpu_logical_cores = logical_cores; 576 | end 577 | 578 | data.stats.cpu_cores = string.format("%dC/%dT", data.stats.cpu_physical_cores, data.stats.cpu_logical_cores); 579 | end 580 | 581 | function data.update_streaming_status_on_script_settings_changed() 582 | local is_streaming_active = obslua.obs_frontend_streaming_active(); 583 | 584 | if is_streaming_active == nil then 585 | return; 586 | end 587 | 588 | if is_streaming_active then 589 | data.stats.streaming_status = data.streaming_statuses.live; 590 | else 591 | data.stats.streaming_status = data.streaming_statuses.offline; 592 | data.stats.bitrate = 0; 593 | end 594 | end 595 | 596 | function data.update_recording_status_on_script_settings_changed() 597 | local is_recording_active = obslua.obs_frontend_recording_active(); 598 | 599 | if is_recording_active == nil then 600 | return; 601 | end 602 | 603 | if is_recording_active then 604 | local is_recording_paused = obslua.obs_frontend_recording_paused(); 605 | 606 | if is_recording_paused == nil then 607 | return; 608 | end 609 | 610 | if is_recording_paused then 611 | data.stats.recording_status = data.recording_statuses.paused; 612 | else 613 | data.stats.recording_status = data.recording_statuses.on; 614 | end 615 | else 616 | data.stats.recording_status = data.recording_statuses.off; 617 | data.stats.recording_bitrate = 0; 618 | end 619 | end 620 | 621 | function data.init_module() 622 | ffi = require("ffi"); 623 | text_source_handler = require("modules.text_source_handler"); 624 | utils = require("modules.utils"); 625 | 626 | ffi.cdef[[ 627 | struct video_output; 628 | typedef struct video_output video_t; 629 | 630 | struct os_cpu_usage_info; 631 | typedef struct os_cpu_usage_info os_cpu_usage_info_t; 632 | 633 | uint32_t video_output_get_skipped_frames(const video_t *video); 634 | uint32_t video_output_get_total_frames(const video_t *video); 635 | double video_output_get_frame_rate(const video_t *video); 636 | 637 | os_cpu_usage_info_t *os_cpu_usage_info_start(void); 638 | double os_cpu_usage_info_query(os_cpu_usage_info_t *info); 639 | void os_cpu_usage_info_destroy(os_cpu_usage_info_t *info); 640 | 641 | video_t *obs_get_video(void); 642 | ]] 643 | 644 | if ffi.os == "OSX" then 645 | obsffi = ffi.load("obs-opengl.dylib"); -- OS X 646 | else 647 | obsffi = ffi.load("obs"); -- Windows 648 | -- Linux? 649 | end 650 | end 651 | 652 | return data; -------------------------------------------------------------------------------- /modules/json.lua: -------------------------------------------------------------------------------- 1 | -- Module options: 2 | local always_use_lpeg = false 3 | local register_global_module_table = false 4 | local global_module_name = 'json' 5 | 6 | --[==[ 7 | 8 | David Kolf's JSON module for Lua 5.1 - 5.4 9 | 10 | Version 2.6 11 | 12 | 13 | For the documentation see the corresponding readme.txt or visit 14 | . 15 | 16 | You can contact the author by sending an e-mail to 'david' at the 17 | domain 'dkolf.de'. 18 | 19 | 20 | Copyright (C) 2010-2021 David Heiko Kolf 21 | 22 | Permission is hereby granted, free of charge, to any person obtaining 23 | a copy of this software and associated documentation files (the 24 | "Software"), to deal in the Software without restriction, including 25 | without limitation the rights to use, copy, modify, merge, publish, 26 | distribute, sublicense, and/or sell copies of the Software, and to 27 | permit persons to whom the Software is furnished to do so, subject to 28 | the following conditions: 29 | 30 | The above copyright notice and this permission notice shall be 31 | included in all copies or substantial portions of the Software. 32 | 33 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 34 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 35 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 36 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 37 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 38 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 39 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 40 | SOFTWARE. 41 | 42 | --]==] 43 | 44 | -- global dependencies: 45 | local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset = 46 | pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset 47 | local error, require, pcall, select = error, require, pcall, select 48 | local floor, huge = math.floor, math.huge 49 | local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = 50 | string.rep, string.gsub, string.sub, string.byte, string.char, 51 | string.find, string.len, string.format 52 | local strmatch = string.match 53 | local concat = table.concat 54 | 55 | local json = { version = "dkjson 2.6" } 56 | 57 | local jsonlpeg = {} 58 | 59 | if register_global_module_table then 60 | if always_use_lpeg then 61 | _G[global_module_name] = jsonlpeg 62 | else 63 | _G[global_module_name] = json 64 | end 65 | end 66 | 67 | local _ENV = nil -- blocking globals in Lua 5.2 and later 68 | 69 | pcall (function() 70 | -- Enable access to blocked metatables. 71 | -- Don't worry, this module doesn't change anything in them. 72 | local debmeta = require "debug".getmetatable 73 | if debmeta then getmetatable = debmeta end 74 | end) 75 | 76 | json.null = setmetatable ({}, { 77 | __tojson = function () return "null" end 78 | }) 79 | 80 | local function isarray (tbl) 81 | local max, n, arraylen = 0, 0, 0 82 | for k,v in pairs (tbl) do 83 | if k == 'n' and type(v) == 'number' then 84 | arraylen = v 85 | if v > max then 86 | max = v 87 | end 88 | else 89 | if type(k) ~= 'number' or k < 1 or floor(k) ~= k then 90 | return false 91 | end 92 | if k > max then 93 | max = k 94 | end 95 | n = n + 1 96 | end 97 | end 98 | if max > 10 and max > arraylen and max > n * 2 then 99 | return false -- don't create an array with too many holes 100 | end 101 | return true, max 102 | end 103 | 104 | local escapecodes = { 105 | ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", 106 | ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" 107 | } 108 | 109 | local function escapeutf8 (uchar) 110 | local value = escapecodes[uchar] 111 | if value then 112 | return value 113 | end 114 | local a, b, c, d = strbyte (uchar, 1, 4) 115 | a, b, c, d = a or 0, b or 0, c or 0, d or 0 116 | if a <= 0x7f then 117 | value = a 118 | elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then 119 | value = (a - 0xc0) * 0x40 + b - 0x80 120 | elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then 121 | value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 122 | elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then 123 | value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 124 | else 125 | return "" 126 | end 127 | if value <= 0xffff then 128 | return strformat ("\\u%.4x", value) 129 | elseif value <= 0x10ffff then 130 | -- encode as UTF-16 surrogate pair 131 | value = value - 0x10000 132 | local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400) 133 | return strformat ("\\u%.4x\\u%.4x", highsur, lowsur) 134 | else 135 | return "" 136 | end 137 | end 138 | 139 | local function fsub (str, pattern, repl) 140 | -- gsub always builds a new string in a buffer, even when no match 141 | -- exists. First using find should be more efficient when most strings 142 | -- don't contain the pattern. 143 | if strfind (str, pattern) then 144 | return gsub (str, pattern, repl) 145 | else 146 | return str 147 | end 148 | end 149 | 150 | local function quotestring (value) 151 | -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js 152 | value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8) 153 | if strfind (value, "[\194\216\220\225\226\239]") then 154 | value = fsub (value, "\194[\128-\159\173]", escapeutf8) 155 | value = fsub (value, "\216[\128-\132]", escapeutf8) 156 | value = fsub (value, "\220\143", escapeutf8) 157 | value = fsub (value, "\225\158[\180\181]", escapeutf8) 158 | value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8) 159 | value = fsub (value, "\226\129[\160-\175]", escapeutf8) 160 | value = fsub (value, "\239\187\191", escapeutf8) 161 | value = fsub (value, "\239\191[\176-\191]", escapeutf8) 162 | end 163 | return "\"" .. value .. "\"" 164 | end 165 | json.quotestring = quotestring 166 | 167 | local function replace(str, o, n) 168 | local i, j = strfind (str, o, 1, true) 169 | if i then 170 | return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1) 171 | else 172 | return str 173 | end 174 | end 175 | 176 | -- locale independent num2str and str2num functions 177 | local decpoint, numfilter 178 | 179 | local function updatedecpoint () 180 | decpoint = strmatch(tostring(0.5), "([^05+])") 181 | -- build a filter that can be used to remove group separators 182 | numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" 183 | end 184 | 185 | updatedecpoint() 186 | 187 | local function num2str (num) 188 | return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") 189 | end 190 | 191 | local function str2num (str) 192 | local num = tonumber(replace(str, ".", decpoint)) 193 | if not num then 194 | updatedecpoint() 195 | num = tonumber(replace(str, ".", decpoint)) 196 | end 197 | return num 198 | end 199 | 200 | local function addnewline2 (level, buffer, buflen) 201 | buffer[buflen+1] = "\n" 202 | buffer[buflen+2] = strrep (" ", level) 203 | buflen = buflen + 2 204 | return buflen 205 | end 206 | 207 | function json.addnewline (state) 208 | if state.indent then 209 | state.bufferlen = addnewline2 (state.level or 0, 210 | state.buffer, state.bufferlen or #(state.buffer)) 211 | end 212 | end 213 | 214 | local encode2 -- forward declaration 215 | 216 | local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) 217 | local kt = type (key) 218 | if kt ~= 'string' and kt ~= 'number' then 219 | return nil, "type '" .. kt .. "' is not supported as a key by JSON." 220 | end 221 | if prev then 222 | buflen = buflen + 1 223 | buffer[buflen] = "," 224 | end 225 | if indent then 226 | buflen = addnewline2 (level, buffer, buflen) 227 | end 228 | buffer[buflen+1] = quotestring (key) 229 | buffer[buflen+2] = ":" 230 | return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state) 231 | end 232 | 233 | local function appendcustom(res, buffer, state) 234 | local buflen = state.bufferlen 235 | if type (res) == 'string' then 236 | buflen = buflen + 1 237 | buffer[buflen] = res 238 | end 239 | return buflen 240 | end 241 | 242 | local function exception(reason, value, state, buffer, buflen, defaultmessage) 243 | defaultmessage = defaultmessage or reason 244 | local handler = state.exception 245 | if not handler then 246 | return nil, defaultmessage 247 | else 248 | state.bufferlen = buflen 249 | local ret, msg = handler (reason, value, state, defaultmessage) 250 | if not ret then return nil, msg or defaultmessage end 251 | return appendcustom(ret, buffer, state) 252 | end 253 | end 254 | 255 | function json.encodeexception(reason, value, state, defaultmessage) 256 | return quotestring("<" .. defaultmessage .. ">") 257 | end 258 | 259 | encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state) 260 | local valtype = type (value) 261 | local valmeta = getmetatable (value) 262 | valmeta = type (valmeta) == 'table' and valmeta -- only tables 263 | local valtojson = valmeta and valmeta.__tojson 264 | if valtojson then 265 | if tables[value] then 266 | return exception('reference cycle', value, state, buffer, buflen) 267 | end 268 | tables[value] = true 269 | state.bufferlen = buflen 270 | local ret, msg = valtojson (value, state) 271 | if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end 272 | tables[value] = nil 273 | buflen = appendcustom(ret, buffer, state) 274 | elseif value == nil then 275 | buflen = buflen + 1 276 | buffer[buflen] = "null" 277 | elseif valtype == 'number' then 278 | local s 279 | if value ~= value or value >= huge or -value >= huge then 280 | -- This is the behaviour of the original JSON implementation. 281 | s = "null" 282 | else 283 | s = num2str (value) 284 | end 285 | buflen = buflen + 1 286 | buffer[buflen] = s 287 | elseif valtype == 'boolean' then 288 | buflen = buflen + 1 289 | buffer[buflen] = value and "true" or "false" 290 | elseif valtype == 'string' then 291 | buflen = buflen + 1 292 | buffer[buflen] = quotestring (value) 293 | elseif valtype == 'table' then 294 | if tables[value] then 295 | return exception('reference cycle', value, state, buffer, buflen) 296 | end 297 | tables[value] = true 298 | level = level + 1 299 | local isa, n = isarray (value) 300 | if n == 0 and valmeta and valmeta.__jsontype == 'object' then 301 | isa = false 302 | end 303 | local msg 304 | if isa then -- JSON array 305 | buflen = buflen + 1 306 | buffer[buflen] = "[" 307 | for i = 1, n do 308 | buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state) 309 | if not buflen then return nil, msg end 310 | if i < n then 311 | buflen = buflen + 1 312 | buffer[buflen] = "," 313 | end 314 | end 315 | buflen = buflen + 1 316 | buffer[buflen] = "]" 317 | else -- JSON object 318 | local prev = false 319 | buflen = buflen + 1 320 | buffer[buflen] = "{" 321 | local order = valmeta and valmeta.__jsonorder or globalorder 322 | if order then 323 | local used = {} 324 | n = #order 325 | for i = 1, n do 326 | local k = order[i] 327 | local v = value[k] 328 | if v ~= nil then 329 | used[k] = true 330 | buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) 331 | prev = true -- add a seperator before the next element 332 | end 333 | end 334 | for k,v in pairs (value) do 335 | if not used[k] then 336 | buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) 337 | if not buflen then return nil, msg end 338 | prev = true -- add a seperator before the next element 339 | end 340 | end 341 | else -- unordered 342 | for k,v in pairs (value) do 343 | buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) 344 | if not buflen then return nil, msg end 345 | prev = true -- add a seperator before the next element 346 | end 347 | end 348 | if indent then 349 | buflen = addnewline2 (level - 1, buffer, buflen) 350 | end 351 | buflen = buflen + 1 352 | buffer[buflen] = "}" 353 | end 354 | tables[value] = nil 355 | else 356 | return exception ('unsupported type', value, state, buffer, buflen, 357 | "type '" .. valtype .. "' is not supported by JSON.") 358 | end 359 | return buflen 360 | end 361 | 362 | function json.encode (value, state) 363 | state = state or {} 364 | local oldbuffer = state.buffer 365 | local buffer = oldbuffer or {} 366 | state.buffer = buffer 367 | updatedecpoint() 368 | local ret, msg = encode2 (value, state.indent, state.level or 0, 369 | buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state) 370 | if not ret then 371 | error (msg, 2) 372 | elseif oldbuffer == buffer then 373 | state.bufferlen = ret 374 | return true 375 | else 376 | state.bufferlen = nil 377 | state.buffer = nil 378 | return concat (buffer) 379 | end 380 | end 381 | 382 | local function loc (str, where) 383 | local line, pos, linepos = 1, 1, 0 384 | while true do 385 | pos = strfind (str, "\n", pos, true) 386 | if pos and pos < where then 387 | line = line + 1 388 | linepos = pos 389 | pos = pos + 1 390 | else 391 | break 392 | end 393 | end 394 | return "line " .. line .. ", column " .. (where - linepos) 395 | end 396 | 397 | local function unterminated (str, what, where) 398 | return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where) 399 | end 400 | 401 | local function scanwhite (str, pos) 402 | while true do 403 | pos = strfind (str, "%S", pos) 404 | if not pos then return nil end 405 | local sub2 = strsub (str, pos, pos + 1) 406 | if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then 407 | -- UTF-8 Byte Order Mark 408 | pos = pos + 3 409 | elseif sub2 == "//" then 410 | pos = strfind (str, "[\n\r]", pos + 2) 411 | if not pos then return nil end 412 | elseif sub2 == "/*" then 413 | pos = strfind (str, "*/", pos + 2) 414 | if not pos then return nil end 415 | pos = pos + 2 416 | else 417 | return pos 418 | end 419 | end 420 | end 421 | 422 | local escapechars = { 423 | ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f", 424 | ["n"] = "\n", ["r"] = "\r", ["t"] = "\t" 425 | } 426 | 427 | local function unichar (value) 428 | if value < 0 then 429 | return nil 430 | elseif value <= 0x007f then 431 | return strchar (value) 432 | elseif value <= 0x07ff then 433 | return strchar (0xc0 + floor(value/0x40), 434 | 0x80 + (floor(value) % 0x40)) 435 | elseif value <= 0xffff then 436 | return strchar (0xe0 + floor(value/0x1000), 437 | 0x80 + (floor(value/0x40) % 0x40), 438 | 0x80 + (floor(value) % 0x40)) 439 | elseif value <= 0x10ffff then 440 | return strchar (0xf0 + floor(value/0x40000), 441 | 0x80 + (floor(value/0x1000) % 0x40), 442 | 0x80 + (floor(value/0x40) % 0x40), 443 | 0x80 + (floor(value) % 0x40)) 444 | else 445 | return nil 446 | end 447 | end 448 | 449 | local function scanstring (str, pos) 450 | local lastpos = pos + 1 451 | local buffer, n = {}, 0 452 | while true do 453 | local nextpos = strfind (str, "[\"\\]", lastpos) 454 | if not nextpos then 455 | return unterminated (str, "string", pos) 456 | end 457 | if nextpos > lastpos then 458 | n = n + 1 459 | buffer[n] = strsub (str, lastpos, nextpos - 1) 460 | end 461 | if strsub (str, nextpos, nextpos) == "\"" then 462 | lastpos = nextpos + 1 463 | break 464 | else 465 | local escchar = strsub (str, nextpos + 1, nextpos + 1) 466 | local value 467 | if escchar == "u" then 468 | value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16) 469 | if value then 470 | local value2 471 | if 0xD800 <= value and value <= 0xDBff then 472 | -- we have the high surrogate of UTF-16. Check if there is a 473 | -- low surrogate escaped nearby to combine them. 474 | if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then 475 | value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16) 476 | if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then 477 | value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 478 | else 479 | value2 = nil -- in case it was out of range for a low surrogate 480 | end 481 | end 482 | end 483 | value = value and unichar (value) 484 | if value then 485 | if value2 then 486 | lastpos = nextpos + 12 487 | else 488 | lastpos = nextpos + 6 489 | end 490 | end 491 | end 492 | end 493 | if not value then 494 | value = escapechars[escchar] or escchar 495 | lastpos = nextpos + 2 496 | end 497 | n = n + 1 498 | buffer[n] = value 499 | end 500 | end 501 | if n == 1 then 502 | return buffer[1], lastpos 503 | elseif n > 1 then 504 | return concat (buffer), lastpos 505 | else 506 | return "", lastpos 507 | end 508 | end 509 | 510 | local scanvalue -- forward declaration 511 | 512 | local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta) 513 | local len = strlen (str) 514 | local tbl, n = {}, 0 515 | local pos = startpos + 1 516 | if what == 'object' then 517 | setmetatable (tbl, objectmeta) 518 | else 519 | setmetatable (tbl, arraymeta) 520 | end 521 | while true do 522 | pos = scanwhite (str, pos) 523 | if not pos then return unterminated (str, what, startpos) end 524 | local char = strsub (str, pos, pos) 525 | if char == closechar then 526 | return tbl, pos + 1 527 | end 528 | local val1, err 529 | val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) 530 | if err then return nil, pos, err end 531 | pos = scanwhite (str, pos) 532 | if not pos then return unterminated (str, what, startpos) end 533 | char = strsub (str, pos, pos) 534 | if char == ":" then 535 | if val1 == nil then 536 | return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")" 537 | end 538 | pos = scanwhite (str, pos + 1) 539 | if not pos then return unterminated (str, what, startpos) end 540 | local val2 541 | val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) 542 | if err then return nil, pos, err end 543 | tbl[val1] = val2 544 | pos = scanwhite (str, pos) 545 | if not pos then return unterminated (str, what, startpos) end 546 | char = strsub (str, pos, pos) 547 | else 548 | n = n + 1 549 | tbl[n] = val1 550 | end 551 | if char == "," then 552 | pos = pos + 1 553 | end 554 | end 555 | end 556 | 557 | scanvalue = function (str, pos, nullval, objectmeta, arraymeta) 558 | pos = pos or 1 559 | pos = scanwhite (str, pos) 560 | if not pos then 561 | return nil, strlen (str) + 1, "no valid JSON value (reached the end)" 562 | end 563 | local char = strsub (str, pos, pos) 564 | if char == "{" then 565 | return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta) 566 | elseif char == "[" then 567 | return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta) 568 | elseif char == "\"" then 569 | return scanstring (str, pos) 570 | else 571 | local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) 572 | if pstart then 573 | local number = str2num (strsub (str, pstart, pend)) 574 | if number then 575 | return number, pend + 1 576 | end 577 | end 578 | pstart, pend = strfind (str, "^%a%w*", pos) 579 | if pstart then 580 | local name = strsub (str, pstart, pend) 581 | if name == "true" then 582 | return true, pend + 1 583 | elseif name == "false" then 584 | return false, pend + 1 585 | elseif name == "null" then 586 | return nullval, pend + 1 587 | end 588 | end 589 | return nil, pos, "no valid JSON value at " .. loc (str, pos) 590 | end 591 | end 592 | 593 | local function optionalmetatables(...) 594 | if select("#", ...) > 0 then 595 | return ... 596 | else 597 | return {__jsontype = 'object'}, {__jsontype = 'array'} 598 | end 599 | end 600 | 601 | function json.decode (str, pos, nullval, ...) 602 | local objectmeta, arraymeta = optionalmetatables(...) 603 | return scanvalue (str, pos, nullval, objectmeta, arraymeta) 604 | end 605 | 606 | function json.use_lpeg () 607 | local g = require ("lpeg") 608 | 609 | if g.version() == "0.11" then 610 | error "due to a bug in LPeg 0.11, it cannot be used for JSON matching" 611 | end 612 | 613 | local pegmatch = g.match 614 | local P, S, R = g.P, g.S, g.R 615 | 616 | local function ErrorCall (str, pos, msg, state) 617 | if not state.msg then 618 | state.msg = msg .. " at " .. loc (str, pos) 619 | state.pos = pos 620 | end 621 | return false 622 | end 623 | 624 | local function Err (msg) 625 | return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall) 626 | end 627 | 628 | local function ErrorUnterminatedCall (str, pos, what, state) 629 | return ErrorCall (str, pos - 1, "unterminated " .. what, state) 630 | end 631 | 632 | local SingleLineComment = P"//" * (1 - S"\n\r")^0 633 | local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/" 634 | local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0 635 | 636 | local function ErrUnterminated (what) 637 | return g.Cmt (g.Cc (what) * g.Carg (2), ErrorUnterminatedCall) 638 | end 639 | 640 | local PlainChar = 1 - S"\"\\\n\r" 641 | local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars 642 | local HexDigit = R("09", "af", "AF") 643 | local function UTF16Surrogate (match, pos, high, low) 644 | high, low = tonumber (high, 16), tonumber (low, 16) 645 | if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then 646 | return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000) 647 | else 648 | return false 649 | end 650 | end 651 | local function UTF16BMP (hex) 652 | return unichar (tonumber (hex, 16)) 653 | end 654 | local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit)) 655 | local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP 656 | local Char = UnicodeEscape + EscapeSequence + PlainChar 657 | local String = P"\"" * (g.Cs (Char ^ 0) * P"\"" + ErrUnterminated "string") 658 | local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0)) 659 | local Fractal = P"." * R"09"^0 660 | local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1 661 | local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num 662 | local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1) 663 | local SimpleValue = Number + String + Constant 664 | local ArrayContent, ObjectContent 665 | 666 | -- The functions parsearray and parseobject parse only a single value/pair 667 | -- at a time and store them directly to avoid hitting the LPeg limits. 668 | local function parsearray (str, pos, nullval, state) 669 | local obj, cont 670 | local start = pos 671 | local npos 672 | local t, nt = {}, 0 673 | repeat 674 | obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state) 675 | if cont == 'end' then 676 | return ErrorUnterminatedCall (str, start, "array", state) 677 | end 678 | pos = npos 679 | if cont == 'cont' or cont == 'last' then 680 | nt = nt + 1 681 | t[nt] = obj 682 | end 683 | until cont ~= 'cont' 684 | return pos, setmetatable (t, state.arraymeta) 685 | end 686 | 687 | local function parseobject (str, pos, nullval, state) 688 | local obj, key, cont 689 | local start = pos 690 | local npos 691 | local t = {} 692 | repeat 693 | key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state) 694 | if cont == 'end' then 695 | return ErrorUnterminatedCall (str, start, "object", state) 696 | end 697 | pos = npos 698 | if cont == 'cont' or cont == 'last' then 699 | t[key] = obj 700 | end 701 | until cont ~= 'cont' 702 | return pos, setmetatable (t, state.objectmeta) 703 | end 704 | 705 | local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) 706 | local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) 707 | local Value = Space * (Array + Object + SimpleValue) 708 | local ExpectedValue = Value + Space * Err "value expected" 709 | local ExpectedKey = String + Err "key expected" 710 | local End = P(-1) * g.Cc'end' 711 | local ErrInvalid = Err "invalid JSON" 712 | ArrayContent = (Value * Space * (P"," * g.Cc'cont' + P"]" * g.Cc'last'+ End + ErrInvalid) + g.Cc(nil) * (P"]" * g.Cc'empty' + End + ErrInvalid)) * g.Cp() 713 | local Pair = g.Cg (Space * ExpectedKey * Space * (P":" + Err "colon expected") * ExpectedValue) 714 | ObjectContent = (g.Cc(nil) * g.Cc(nil) * P"}" * g.Cc'empty' + End + (Pair * Space * (P"," * g.Cc'cont' + P"}" * g.Cc'last' + End + ErrInvalid) + ErrInvalid)) * g.Cp() 715 | local DecodeValue = ExpectedValue * g.Cp () 716 | 717 | jsonlpeg.version = json.version 718 | jsonlpeg.encode = json.encode 719 | jsonlpeg.null = json.null 720 | jsonlpeg.quotestring = json.quotestring 721 | jsonlpeg.addnewline = json.addnewline 722 | jsonlpeg.encodeexception = json.encodeexception 723 | jsonlpeg.using_lpeg = true 724 | 725 | function jsonlpeg.decode (str, pos, nullval, ...) 726 | local state = {} 727 | state.objectmeta, state.arraymeta = optionalmetatables(...) 728 | local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state) 729 | if state.msg then 730 | return nil, state.pos, state.msg 731 | else 732 | return obj, retpos 733 | end 734 | end 735 | 736 | -- cache result of this function: 737 | json.use_lpeg = function () return jsonlpeg end 738 | jsonlpeg.use_lpeg = json.use_lpeg 739 | 740 | return jsonlpeg 741 | end 742 | 743 | if always_use_lpeg then 744 | return json.use_lpeg() 745 | end 746 | 747 | return json -------------------------------------------------------------------------------- /modules/ljsocket.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local socket = {} 3 | local e = {} 4 | local errno = {} 5 | 6 | local assert = assert; 7 | local bit = bit; 8 | local jit = jit; 9 | local string = string; 10 | local tostring = tostring; 11 | local pairs = pairs; 12 | local ipairs = ipairs; 13 | local error = error; 14 | local type = type; 15 | local tonumber = tonumber; 16 | local table = table; 17 | local setmetatable = setmetatable; 18 | local unpack = unpack; 19 | local print = print; 20 | 21 | do 22 | local C 23 | 24 | if ffi.os == "Windows" then 25 | C = assert(ffi.load("ws2_32")) 26 | else 27 | C = ffi.C 28 | end 29 | 30 | local M = {} 31 | 32 | local function generic_function(C_name, cdef, alias, size_error_handling) 33 | ffi.cdef(cdef) 34 | 35 | alias = alias or C_name 36 | local func_name = alias 37 | local func = C[C_name] 38 | 39 | if size_error_handling == false then 40 | socket[func_name] = func 41 | elseif size_error_handling then 42 | socket[func_name] = function(...) 43 | local len = func(...) 44 | if len < 0 then 45 | return nil, socket.lasterror() 46 | end 47 | 48 | return len 49 | end 50 | else 51 | socket[func_name] = function(...) 52 | local ret = func(...) 53 | 54 | if ret == 0 then 55 | return true 56 | end 57 | 58 | return nil, socket.lasterror() 59 | end 60 | end 61 | end 62 | 63 | ffi.cdef[[ 64 | struct in_addr { 65 | uint32_t s_addr; 66 | }; 67 | 68 | struct in6_addr { 69 | union { 70 | uint8_t u6_addr8[16]; 71 | uint16_t u6_addr16[8]; 72 | uint32_t u6_addr32[4]; 73 | } u6_addr; 74 | }; 75 | ]] 76 | 77 | -- https://www.cs.dartmouth.edu/~sergey/cs60/on-sockaddr-structs.txt 78 | 79 | if ffi.os == "OSX" then 80 | ffi.cdef[[ 81 | struct sockaddr { 82 | uint8_t sa_len; 83 | uint8_t sa_family; 84 | char sa_data[14]; 85 | }; 86 | 87 | struct sockaddr_in { 88 | uint8_t sin_len; 89 | uint8_t sin_family; 90 | uint16_t sin_port; 91 | struct in_addr sin_addr; 92 | char sin_zero[8]; 93 | }; 94 | 95 | struct sockaddr_in6 { 96 | uint8_t sin6_len; 97 | uint8_t sin6_family; 98 | uint16_t sin6_port; 99 | uint32_t sin6_flowinfo; 100 | struct in6_addr sin6_addr; 101 | uint32_t sin6_scope_id; 102 | }; 103 | ]] 104 | elseif ffi.os == "Windows" then 105 | ffi.cdef[[ 106 | struct sockaddr { 107 | uint16_t sa_family; 108 | char sa_data[14]; 109 | }; 110 | 111 | struct sockaddr_in { 112 | int16_t sin_family; 113 | uint16_t sin_port; 114 | struct in_addr sin_addr; 115 | uint8_t sin_zero[8]; 116 | }; 117 | 118 | struct sockaddr_in6 { 119 | int16_t sin6_family; 120 | uint16_t sin6_port; 121 | uint32_t sin6_flowinfo; 122 | struct in6_addr sin6_addr; 123 | uint32_t sin6_scope_id; 124 | }; 125 | ]] 126 | else -- posix 127 | ffi.cdef[[ 128 | struct sockaddr { 129 | uint16_t sa_family; 130 | char sa_data[14]; 131 | }; 132 | 133 | struct sockaddr_in { 134 | uint16_t sin_family; 135 | uint16_t sin_port; 136 | struct in_addr sin_addr; 137 | char sin_zero[8]; 138 | }; 139 | 140 | struct sockaddr_in6 { 141 | uint16_t sin6_family; 142 | uint16_t sin6_port; 143 | uint32_t sin6_flowinfo; 144 | struct in6_addr sin6_addr; 145 | uint32_t sin6_scope_id; 146 | }; 147 | ]] 148 | end 149 | 150 | if ffi.os == "Windows" then 151 | ffi.cdef[[ 152 | typedef size_t SOCKET; 153 | 154 | struct addrinfo { 155 | int ai_flags; 156 | int ai_family; 157 | int ai_socktype; 158 | int ai_protocol; 159 | size_t ai_addrlen; 160 | char *ai_canonname; 161 | struct sockaddr *ai_addr; 162 | struct addrinfo *ai_next; 163 | }; 164 | ]] 165 | socket.INVALID_SOCKET = ffi.new("SOCKET", -1) 166 | elseif ffi.os == "OSX" then 167 | ffi.cdef[[ 168 | typedef int32_t SOCKET; 169 | 170 | struct addrinfo { 171 | int ai_flags; 172 | int ai_family; 173 | int ai_socktype; 174 | int ai_protocol; 175 | uint32_t ai_addrlen; 176 | char *ai_canonname; 177 | struct sockaddr *ai_addr; 178 | struct addrinfo *ai_next; 179 | }; 180 | ]] 181 | socket.INVALID_SOCKET = -1 182 | else 183 | ffi.cdef[[ 184 | typedef int32_t SOCKET; 185 | 186 | struct addrinfo { 187 | int ai_flags; 188 | int ai_family; 189 | int ai_socktype; 190 | int ai_protocol; 191 | uint32_t ai_addrlen; 192 | struct sockaddr *ai_addr; 193 | char *ai_canonname; 194 | struct addrinfo *ai_next; 195 | }; 196 | ]] 197 | socket.INVALID_SOCKET = -1 198 | end 199 | 200 | assert(ffi.sizeof("struct sockaddr") == 16) 201 | assert(ffi.sizeof("struct sockaddr_in") == 16) 202 | 203 | if ffi.os == "Windows" then 204 | ffi.cdef[[ 205 | 206 | struct pollfd { 207 | SOCKET fd; 208 | short events; 209 | short revents; 210 | }; 211 | int WSAPoll(struct pollfd *fds, unsigned long int nfds, int timeout); 212 | 213 | uint32_t GetLastError(); 214 | uint32_t FormatMessageA( 215 | uint32_t dwFlags, 216 | const void* lpSource, 217 | uint32_t dwMessageId, 218 | uint32_t dwLanguageId, 219 | char* lpBuffer, 220 | uint32_t nSize, 221 | va_list *Arguments 222 | ); 223 | ]] 224 | 225 | local function WORD(low, high) 226 | return bit.bor(low , bit.lshift(high , 8)) 227 | end 228 | 229 | do 230 | ffi.cdef[[int GetLastError();]] 231 | 232 | local FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 233 | local FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200 234 | local flags = bit.bor(FORMAT_MESSAGE_IGNORE_INSERTS, FORMAT_MESSAGE_FROM_SYSTEM) 235 | 236 | local cache = {} 237 | 238 | function socket.lasterror(num) 239 | num = num or ffi.C.GetLastError() 240 | 241 | if not cache[num] then 242 | local buffer = ffi.new("char[512]") 243 | local len = ffi.C.FormatMessageA(flags, nil, num, 0, buffer, ffi.sizeof(buffer), nil) 244 | cache[num] = ffi.string(buffer, len - 2) 245 | end 246 | 247 | return cache[num], num 248 | end 249 | end 250 | 251 | do 252 | ffi.cdef[[int WSAStartup(uint16_t version, void *wsa_data);]] 253 | 254 | local wsa_data 255 | 256 | if jit.arch == "x64" then 257 | wsa_data = ffi.typeof([[struct { 258 | uint16_t wVersion; 259 | uint16_t wHighVersion; 260 | unsigned short iMax_M; 261 | unsigned short iMaxUdpDg; 262 | char * lpVendorInfo; 263 | char szDescription[257]; 264 | char szSystemStatus[129]; 265 | }]]) 266 | else 267 | wsa_data = ffi.typeof([[struct { 268 | uint16_t wVersion; 269 | uint16_t wHighVersion; 270 | char szDescription[257]; 271 | char szSystemStatus[129]; 272 | unsigned short iMax_M; 273 | unsigned short iMaxUdpDg; 274 | char * lpVendorInfo; 275 | }]]) 276 | end 277 | 278 | function socket.initialize() 279 | local data = wsa_data() 280 | 281 | if C.WSAStartup(WORD(2, 2), data) == 0 then 282 | return data 283 | end 284 | 285 | return nil, socket.lasterror() 286 | end 287 | end 288 | 289 | do 290 | ffi.cdef[[int WSACleanup();]] 291 | 292 | function socket.shutdown() 293 | if C.WSACleanup() == 0 then 294 | return true 295 | end 296 | 297 | return nil, socket.lasterror() 298 | end 299 | end 300 | 301 | if jit.arch ~= "x64" then -- xp or something 302 | ffi.cdef[[int WSAAddressToStringA(struct sockaddr *, unsigned long, void *, char *, unsigned long *);]] 303 | 304 | function socket.inet_ntop(family, pAddr, strptr, strlen) 305 | -- win XP: http://memset.wordpress.com/2010/10/09/inet_ntop-for-win32/ 306 | local srcaddr = ffi.new("struct sockaddr_in") 307 | ffi.copy(srcaddr.sin_addr, pAddr, ffi.sizeof(srcaddr.sin_addr)) 308 | srcaddr.sin_family = family 309 | local len = ffi.new("unsigned long[1]", strlen) 310 | C.WSAAddressToStringA(ffi.cast("struct sockaddr *", srcaddr), ffi.sizeof(srcaddr), nil, strptr, len) 311 | return strptr 312 | end 313 | end 314 | 315 | generic_function("closesocket", "int closesocket(SOCKET s);", "close") 316 | 317 | do 318 | ffi.cdef[[int ioctlsocket(SOCKET s, long cmd, unsigned long* argp);]] 319 | 320 | local IOCPARM_MASK = 0x7 321 | local IOC_IN = 0x80000000 322 | local function _IOW(x,y,t) 323 | return bit.bor(IOC_IN, bit.lshift(bit.band(ffi.sizeof(t),IOCPARM_MASK),16), bit.lshift(x,8), y) 324 | end 325 | 326 | local FIONBIO = _IOW(string.byte'f', 126, "uint32_t") -- -2147195266 -- 2147772030ULL 327 | 328 | function socket.blocking(fd, b) 329 | local ret = C.ioctlsocket(fd, FIONBIO, ffi.new("int[1]", b and 0 or 1)) 330 | if ret == 0 then 331 | return true 332 | end 333 | 334 | return nil, socket.lasterror() 335 | end 336 | end 337 | 338 | function socket.poll(fds, ndfs, timeout) 339 | local ret = C.WSAPoll(fds, ndfs, timeout) 340 | if ret < 0 then 341 | return nil, socket.lasterror() 342 | end 343 | return ret 344 | end 345 | else 346 | ffi.cdef[[ 347 | struct pollfd { 348 | SOCKET fd; 349 | short events; 350 | short revents; 351 | }; 352 | 353 | int poll(struct pollfd *fds, unsigned long nfds, int timeout); 354 | ]] 355 | 356 | do 357 | local cache = {} 358 | 359 | function socket.lasterror(num) 360 | num = num or ffi.errno() 361 | 362 | if not cache[num] then 363 | local err = ffi.string(ffi.C.strerror(num)) 364 | cache[num] = err == "" and tostring(num) or err 365 | end 366 | 367 | return cache[num], num 368 | end 369 | end 370 | 371 | generic_function("close", "int close(SOCKET s);") 372 | 373 | do 374 | ffi.cdef[[int fcntl(int, int, ...);]] 375 | 376 | local F_GETFL = 3 377 | local F_SETFL = 4 378 | local O_NONBLOCK = 04000 379 | 380 | if ffi.os == "OSX" then 381 | O_NONBLOCK = 0x0004 382 | end 383 | function socket.blocking(fd, b) 384 | local flags = ffi.C.fcntl(fd, F_GETFL, 0) 385 | 386 | if flags < 0 then 387 | -- error 388 | return nil, socket.lasterror() 389 | end 390 | 391 | if b then 392 | flags = bit.band(flags, bit.bnot(O_NONBLOCK)) 393 | else 394 | flags = bit.bor(flags, O_NONBLOCK) 395 | end 396 | 397 | local ret = ffi.C.fcntl(fd, F_SETFL, ffi.new("int", flags)) 398 | 399 | if ret < 0 then 400 | return nil, socket.lasterror() 401 | end 402 | 403 | return true 404 | end 405 | end 406 | 407 | function socket.poll(fds, ndfs, timeout) 408 | local ret = C.poll(fds, ndfs, timeout) 409 | if ret < 0 then 410 | return nil, socket.lasterror() 411 | end 412 | return ret 413 | end 414 | end 415 | 416 | 417 | ffi.cdef[[ 418 | char *strerror(int errnum); 419 | int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); 420 | int getnameinfo(const struct sockaddr* sa, uint32_t salen, char* host, size_t hostlen, char* serv, size_t servlen, int flags); 421 | void freeaddrinfo(struct addrinfo *ai); 422 | const char *gai_strerror(int errcode); 423 | char *inet_ntoa(struct in_addr in); 424 | uint16_t ntohs(uint16_t netshort); 425 | ]] 426 | 427 | function socket.getaddrinfo(node_name, service_name, hints, result) 428 | local ret = C.getaddrinfo(node_name, service_name, hints, result) 429 | if ret == 0 then 430 | return true 431 | end 432 | 433 | return nil, ffi.string(socket.lasterror(ret)) 434 | end 435 | 436 | function socket.getnameinfo(address, length, host, hostlen, serv, servlen, flags) 437 | local ret = C.getnameinfo(address, length, host, hostlen, serv, servlen, flags) 438 | if ret == 0 then 439 | return true 440 | end 441 | 442 | return nil, ffi.string(socket.lasterror(ret)) 443 | end 444 | 445 | do 446 | ffi.cdef[[const char *inet_ntop(int __af, const void *__cp, char *__buf, unsigned int __len);]] 447 | 448 | function socket.inet_ntop(family, addrinfo, strptr, strlen) 449 | if C.inet_ntop(family, addrinfo, strptr, strlen) == nil then 450 | return nil, socket.lasterror() 451 | end 452 | 453 | return strptr 454 | end 455 | end 456 | 457 | do 458 | ffi.cdef[[SOCKET socket(int af, int type, int protocol);]] 459 | 460 | function socket.create(af, type, protocol) 461 | local fd = C.socket(af, type, protocol) 462 | 463 | if fd <= 0 then 464 | return nil, socket.lasterror() 465 | end 466 | 467 | return fd 468 | end 469 | end 470 | 471 | generic_function("shutdown", "int shutdown(SOCKET s, int how);") 472 | 473 | generic_function("setsockopt", "int setsockopt(SOCKET s, int level, int optname, const void* optval, uint32_t optlen);") 474 | generic_function("getsockopt", "int getsockopt(SOCKET s, int level, int optname, void *optval, uint32_t *optlen);") 475 | 476 | generic_function("accept", "SOCKET accept(SOCKET s, struct sockaddr *, int *);", nil, false) 477 | generic_function("bind", "int bind(SOCKET s, const struct sockaddr* name, int namelen);") 478 | generic_function("connect", "int connect(SOCKET s, const struct sockaddr * name, int namelen);") 479 | 480 | generic_function("listen", "int listen(SOCKET s, int backlog);") 481 | generic_function("recv", "int recv(SOCKET s, char* buf, int len, int flags);", nil, true) 482 | generic_function("recvfrom", "int recvfrom(SOCKET s, char* buf, int len, int flags, struct sockaddr *src_addr, unsigned int *addrlen);", nil, true) 483 | 484 | generic_function("send", "int send(SOCKET s, const char* buf, int len, int flags);", nil, true) 485 | generic_function("sendto", "int sendto(SOCKET s, const char* buf, int len, int flags, const struct sockaddr* to, int tolen);", nil, true) 486 | 487 | generic_function("getpeername", "int getpeername(SOCKET s, struct sockaddr *, unsigned int *);") 488 | generic_function("getsockname", "int getsockname(SOCKET s, struct sockaddr *, unsigned int *);") 489 | 490 | socket.inet_ntoa = C.inet_ntoa 491 | socket.ntohs = C.ntohs 492 | 493 | function socket.poll(fd, events, revents) 494 | 495 | end 496 | 497 | e = { 498 | TCP_NODELAY = 1, 499 | TCP_MAXSEG = 2, 500 | TCP_CORK = 3, 501 | TCP_KEEPIDLE = 4, 502 | TCP_KEEPINTVL = 5, 503 | TCP_KEEPCNT = 6, 504 | TCP_SYNCNT = 7, 505 | TCP_LINGER2 = 8, 506 | TCP_DEFER_ACCEPT = 9, 507 | TCP_WINDOW_CLAMP = 10, 508 | TCP_INFO = 11, 509 | TCP_QUICKACK = 12, 510 | TCP_CONGESTION = 13, 511 | TCP_MD5SIG = 14, 512 | TCP_THIN_LINEAR_TIMEOUTS = 16, 513 | TCP_THIN_DUPACK = 17, 514 | TCP_USER_TIMEOUT = 18, 515 | TCP_REPAIR = 19, 516 | TCP_REPAIR_QUEUE = 20, 517 | TCP_QUEUE_SEQ = 21, 518 | TCP_REPAIR_OPTIONS = 22, 519 | TCP_FASTOPEN = 23, 520 | TCP_TIMESTAMP = 24, 521 | TCP_NOTSENT_LOWAT = 25, 522 | TCP_CC_INFO = 26, 523 | TCP_SAVE_SYN = 27, 524 | TCP_SAVED_SYN = 28, 525 | TCP_REPAIR_WINDOW = 29, 526 | TCP_FASTOPEN_CONNECT = 30, 527 | TCP_ULP = 31, 528 | TCP_MD5SIG_EXT = 32, 529 | TCP_FASTOPEN_KEY = 33, 530 | TCP_FASTOPEN_NO_COOKIE = 34, 531 | TCP_ZEROCOPY_RECEIVE = 35, 532 | TCP_INQ = 36, 533 | 534 | AF_INET = 2, 535 | AF_INET6 = 10, 536 | AF_UNSPEC = 0, 537 | 538 | AF_UNIX = 1, 539 | AF_AX25 = 3, 540 | AF_IPX = 4, 541 | AF_APPLETALK = 5, 542 | AF_NETROM = 6, 543 | AF_BRIDGE = 7, 544 | AF_AAL5 = 8, 545 | AF_X25 = 9, 546 | 547 | INET6_ADDRSTRLEN = 46, 548 | INET_ADDRSTRLEN = 16, 549 | 550 | SO_DEBUG = 1, 551 | SO_REUSEADDR = 2, 552 | SO_TYPE = 3, 553 | SO_ERROR = 4, 554 | SO_DONTROUTE = 5, 555 | SO_BROADCAST = 6, 556 | SO_SNDBUF = 7, 557 | SO_RCVBUF = 8, 558 | SO_SNDBUFFORCE = 32, 559 | SO_RCVBUFFORCE = 33, 560 | SO_KEEPALIVE = 9, 561 | SO_OOBINLINE = 10, 562 | SO_NO_CHECK = 11, 563 | SO_PRIORITY = 12, 564 | SO_LINGER = 13, 565 | SO_BSDCOMPAT = 14, 566 | SO_REUSEPORT = 15, 567 | SO_PASSCRED = 16, 568 | SO_PEERCRED = 17, 569 | SO_RCVLOWAT = 18, 570 | SO_SNDLOWAT = 19, 571 | SO_RCVTIMEO = 20, 572 | SO_SNDTIMEO = 21, 573 | SO_SECURITY_AUTHENTICATION = 22, 574 | SO_SECURITY_ENCRYPTION_TRANSPORT = 23, 575 | SO_SECURITY_ENCRYPTION_NETWORK = 24, 576 | SO_BINDTODEVICE = 25, 577 | SO_ATTACH_FILTER = 26, 578 | SO_DETACH_FILTER = 27, 579 | SO_GET_FILTER = 26, 580 | SO_PEERNAME = 28, 581 | SO_TIMESTAMP = 29, 582 | SO_ACCEPTCONN = 30, 583 | SO_PEERSEC = 31, 584 | SO_PASSSEC = 34, 585 | SO_TIMESTAMPNS = 35, 586 | SO_MARK = 36, 587 | SO_TIMESTAMPING = 37, 588 | SO_PROTOCOL = 38, 589 | SO_DOMAIN = 39, 590 | SO_RXQ_OVFL = 40, 591 | SO_WIFI_STATUS = 41, 592 | SO_PEEK_OFF = 42, 593 | SO_NOFCS = 43, 594 | SO_LOCK_FILTER = 44, 595 | SO_SELECT_ERR_QUEUE = 45, 596 | SO_BUSY_POLL = 46, 597 | SO_MAX_PACING_RATE = 47, 598 | SO_BPF_EXTENSIONS = 48, 599 | SO_INCOMING_CPU = 49, 600 | SO_ATTACH_BPF = 50, 601 | SO_DETACH_BPF = 27, 602 | SO_ATTACH_REUSEPORT_CBPF = 51, 603 | SO_ATTACH_REUSEPORT_EBPF = 52, 604 | SO_CNX_ADVICE = 53, 605 | SO_MEMINFO = 55, 606 | SO_INCOMING_NAPI_ID = 56, 607 | SO_COOKIE = 57, 608 | SO_PEERGROUPS = 59, 609 | SO_ZEROCOPY = 60, 610 | SO_TXTIME = 61, 611 | SOL_SOCKET = 1, 612 | SOL_TCP = 6, 613 | 614 | SOMAXCONN = 128, 615 | 616 | IPPROTO_IP = 0, 617 | IPPROTO_HOPOPTS = 0, 618 | IPPROTO_ICMP = 1, 619 | IPPROTO_IGMP = 2, 620 | IPPROTO_IPIP = 4, 621 | IPPROTO_TCP = 6, 622 | IPPROTO_EGP = 8, 623 | IPPROTO_PUP = 12, 624 | IPPROTO_UDP = 17, 625 | IPPROTO_IDP = 22, 626 | IPPROTO_TP = 29, 627 | IPPROTO_DCCP = 33, 628 | IPPROTO_IPV6 = 41, 629 | IPPROTO_ROUTING = 43, 630 | IPPROTO_FRAGMENT = 44, 631 | IPPROTO_RSVP = 46, 632 | IPPROTO_GRE = 47, 633 | IPPROTO_ESP = 50, 634 | IPPROTO_AH = 51, 635 | IPPROTO_ICMPV6 = 58, 636 | IPPROTO_NONE = 59, 637 | IPPROTO_DSTOPTS = 60, 638 | IPPROTO_MTP = 92, 639 | IPPROTO_ENCAP = 98, 640 | IPPROTO_PIM = 103, 641 | IPPROTO_COMP = 108, 642 | IPPROTO_SCTP = 132, 643 | IPPROTO_UDPLITE = 136, 644 | IPPROTO_RAW = 255, 645 | 646 | SOCK_STREAM = 1, 647 | SOCK_DGRAM = 2, 648 | SOCK_RAW = 3, 649 | SOCK_RDM = 4, 650 | SOCK_SEQPACKET = 5, 651 | SOCK_DCCP = 6, 652 | SOCK_PACKET = 10, 653 | SOCK_CLOEXEC = 02000000, 654 | SOCK_NONBLOCK = 04000, 655 | 656 | AI_PASSIVE = 0x00000001, 657 | AI_CANONNAME = 0x00000002, 658 | AI_NUMERICHOST = 0x00000004, 659 | AI_NUMERICSERV = 0x00000008, 660 | AI_ALL = 0x00000100, 661 | AI_ADDRCONFIG = 0x00000400, 662 | AI_V4MAPPED = 0x00000800, 663 | AI_NON_AUTHORITATIVE = 0x00004000, 664 | AI_SECURE = 0x00008000, 665 | AI_RETURN_PREFERRED_NAMES = 0x00010000, 666 | AI_FQDN = 0x00020000, 667 | AI_FILESERVER = 0x00040000, 668 | 669 | POLLIN = 0x0001, 670 | POLLPRI = 0x0002, 671 | POLLOUT = 0x0004, 672 | POLLRDNORM = 0x0040, 673 | POLLWRNORM = 0x0004, 674 | POLLRDBAND = 0x0080, 675 | POLLWRBAND = 0x0100, 676 | POLLEXTEND = 0x0200, 677 | POLLATTRIB = 0x0400, 678 | POLLNLINK = 0x0800, 679 | POLLWRITE = 0x1000, 680 | POLLERR = 0x0008, 681 | POLLHUP = 0x0010, 682 | POLLNVAL = 0x0020, 683 | 684 | MSG_OOB = 0x01, 685 | MSG_PEEK = 0x02, 686 | MSG_DONTROUTE = 0x04, 687 | MSG_CTRUNC = 0x08, 688 | MSG_PROXY = 0x10, 689 | MSG_TRUNC = 0x20, 690 | MSG_DONTWAIT = 0x40, 691 | MSG_EOR = 0x80, 692 | MSG_WAITALL = 0x100, 693 | MSG_FIN = 0x200, 694 | MSG_SYN = 0x400, 695 | MSG_CONFIRM = 0x800, 696 | MSG_RST = 0x1000, 697 | MSG_ERRQUEUE = 0x2000, 698 | MSG_NOSIGNAL = 0x4000, 699 | MSG_MORE = 0x8000, 700 | MSG_WAITFORONE = 0x10000, 701 | MSG_CMSG_CLOEXEC = 0x40000000, 702 | } 703 | 704 | errno = { 705 | EAGAIN = 11, 706 | EWOULDBLOCK = 11, -- is errno.EAGAIN 707 | EINVAL = 22, 708 | ENOTSOCK = 88, 709 | ECONNRESET = 104, 710 | EINPROGRESS = 115, 711 | } 712 | 713 | if ffi.os == "Windows" then 714 | e.SO_SNDLOWAT = 4099 715 | e.SO_REUSEADDR = 4 716 | e.SO_KEEPALIVE = 8 717 | e.SOMAXCONN = 2147483647 718 | e.AF_INET6 = 23 719 | e.SO_RCVTIMEO = 4102 720 | e.SOL_SOCKET = 65535 721 | e.SO_LINGER = 128 722 | e.SO_OOBINLINE = 256 723 | e.POLLWRNORM = 16 724 | e.SO_ERROR = 4103 725 | e.SO_BROADCAST = 32 726 | e.SO_ACCEPTCONN = 2 727 | e.SO_RCVBUF = 4098 728 | e.SO_SNDTIMEO = 4101 729 | e.POLLIN = 768 730 | e.POLLPRI = 1024 731 | e.SO_TYPE = 4104 732 | e.POLLRDBAND = 512 733 | e.POLLWRBAND = 32 734 | e.SO_SNDBUF = 4097 735 | e.POLLNVAL = 4 736 | e.POLLHUP = 2 737 | e.POLLERR = 1 738 | e.POLLRDNORM = 256 739 | e.SO_DONTROUTE = 16 740 | e.SO_RCVLOWAT = 4100 741 | 742 | errno.EINVAL = 10022 743 | errno.EAGAIN = 10035 -- Note: Does not exist on Windows 744 | errno.EWOULDBLOCK = 10035 745 | errno.EINPROGRESS = 10036 746 | errno.ENOTSOCK = 10038 747 | errno.ECONNRESET = 10054 748 | end 749 | 750 | if ffi.os == "OSX" then 751 | e.SOL_SOCKET = 0xffff 752 | e.SO_DEBUG = 0x0001 753 | e.SO_ACCEPTCONN = 0x0002 754 | e.SO_REUSEADDR = 0x0004 755 | e.SO_KEEPALIVE = 0x0008 756 | e.SO_DONTROUTE = 0x0010 757 | e.SO_BROADCAST = 0x0020 758 | 759 | errno.EINVAL = 22 760 | errno.EAGAIN = 35 761 | errno.EWOULDBLOCK = errno.EAGAIN 762 | errno.EINPROGRESS = 36 763 | errno.ENOTSOCK = 38 764 | errno.ECONNRESET = 54 765 | end 766 | 767 | if socket.initialize then 768 | assert(socket.initialize()) 769 | end 770 | end 771 | 772 | local function capture_flags(what) 773 | local flags = {} 774 | local reverse = {} 775 | for k, v in pairs(e) do 776 | if k:sub(0, #what) == what then 777 | k = k:sub(#what + 1):lower() 778 | reverse[v] = k 779 | flags[k] = v 780 | end 781 | end 782 | return { 783 | lookup = flags, 784 | reverse = reverse, 785 | strict_reverse = function(key) 786 | if not key then 787 | error("invalid " .. what:sub(0, -2) .. " flag: nil") 788 | end 789 | if not reverse[key] then 790 | error("invalid "..what:sub(0, -2).." flag: " .. key, 2) 791 | end 792 | return reverse[key] 793 | end, 794 | strict_lookup = function(key) 795 | if not key then 796 | error("invalid " .. what:sub(0, -2) .. " flag: nil") 797 | end 798 | if not flags[key] then 799 | error("invalid "..what:sub(0, -2).." flag: " .. key, 2) 800 | end 801 | return flags[key] 802 | end 803 | } 804 | end 805 | 806 | local SOCK = capture_flags("SOCK_") 807 | local AF = capture_flags("AF_") 808 | local IPPROTO = capture_flags("IPPROTO_") 809 | local AI = capture_flags("AI_") 810 | local SOL = capture_flags("SOL_") 811 | local SO = capture_flags("SO_") 812 | local TCP = capture_flags("TCP_") 813 | local POLL = capture_flags("POLL") 814 | 815 | local function table_to_flags(flags, valid_flags, operation) 816 | if type(flags) == "string" then 817 | flags = {flags} 818 | end 819 | operation = operation or bit.band 820 | 821 | local out = 0 822 | 823 | for k, v in pairs(flags) do 824 | local flag = valid_flags[v] or valid_flags[k] 825 | if not flag then 826 | error("invalid flag " .. tostring(v), 2) 827 | end 828 | 829 | out = operation(out, tonumber(flag)) 830 | end 831 | 832 | return out 833 | end 834 | 835 | local function flags_to_table(flags, valid_flags, operation) 836 | if not flags then return valid_flags.default_valid_flag end 837 | operation = operation or bit.band 838 | 839 | local out = {} 840 | 841 | for k, v in pairs(valid_flags) do 842 | if operation(flags, v) > 0 then 843 | out[k] = true 844 | end 845 | end 846 | 847 | return out 848 | end 849 | 850 | local M = {} 851 | 852 | local timeout_messages = {} 853 | timeout_messages[errno.EINPROGRESS] = true 854 | timeout_messages[errno.EAGAIN] = true 855 | timeout_messages[errno.EWOULDBLOCK] = true 856 | 857 | function M.poll(socket, flags, timeout) 858 | local pfd = ffi.new("struct pollfd[1]", {{ 859 | fd = socket.fd, 860 | events = table_to_flags(flags, POLL.lookup, bit.bor), 861 | revents = 0, 862 | }}) 863 | local ok, err = socket.poll(pfd, 1, timeout or 0) 864 | if not ok then return ok, err end 865 | return flags_to_table(pfd[0].revents, POLL.lookup, bit.bor), ok 866 | end 867 | 868 | local function addrinfo_get_ip(self) 869 | if self.addrinfo.ai_addr == nil then 870 | return nil 871 | end 872 | local str = ffi.new("char[256]") 873 | local addr = assert(socket.inet_ntop(AF.lookup[self.family], ffi.cast("struct sockaddr_in*", self.addrinfo.ai_addr).sin_addr, str, ffi.sizeof(str))) 874 | return ffi.string(addr) 875 | end 876 | 877 | local function addrinfo_get_port(self) 878 | if self.addrinfo.ai_addr == nil then 879 | return nil 880 | end 881 | if self.family == "inet" then 882 | return socket.ntohs(ffi.cast("struct sockaddr_in*", self.addrinfo.ai_addr).sin_port) 883 | elseif self.family == "inet6" then 884 | return socket.ntohs(ffi.cast("struct sockaddr_in6*", self.addrinfo.ai_addr).sin6_port) 885 | end 886 | 887 | return nil, "unknown family " .. tostring(self.family) 888 | end 889 | 890 | local function addrinfo_to_table(res, host, service) 891 | local info = {} 892 | 893 | if res.ai_canonname ~= nil then 894 | info.canonical_name = ffi.string(res.ai_canonname) 895 | end 896 | 897 | info.host = host ~= "*" and host or nil 898 | info.service = service 899 | info.family = AF.reverse[res.ai_family] 900 | info.socket_type = SOCK.reverse[res.ai_socktype] 901 | info.protocol = IPPROTO.reverse[res.ai_protocol] 902 | info.flags = flags_to_table(res.ai_flags, AI.lookup, bit.band) 903 | info.addrinfo = res 904 | info.get_ip = addrinfo_get_ip 905 | info.get_port = addrinfo_get_port 906 | 907 | return info 908 | end 909 | 910 | function M.get_address_info(data) 911 | local hints 912 | 913 | if data.socket_type or data.protocol or data.flags or data.family then 914 | hints = ffi.new("struct addrinfo", { 915 | ai_family = data.family and AF.strict_lookup(data.family) or nil, 916 | ai_socktype = data.socket_type and SOCK.strict_lookup(data.socket_type) or nil, 917 | ai_protocol = data.protocol and IPPROTO.strict_lookup(data.protocol) or nil, 918 | ai_flags = data.flags and table_to_flags(data.flags, AI.lookup, bit.bor) or nil, 919 | }) 920 | end 921 | 922 | local out = ffi.new("struct addrinfo*[1]") 923 | 924 | local ok, err = socket.getaddrinfo( 925 | data.host ~= "*" and data.host or nil, 926 | data.service and tostring(data.service) or nil, 927 | hints, 928 | out 929 | ) 930 | 931 | if not ok then return ok, err end 932 | 933 | local tbl = {} 934 | 935 | local res = out[0] 936 | 937 | while res ~= nil do 938 | table.insert(tbl, addrinfo_to_table(res, data.host, data.service)) 939 | 940 | res = res.ai_next 941 | end 942 | 943 | --ffi.C.freeaddrinfo(out[0]) 944 | 945 | return tbl 946 | end 947 | 948 | function M.find_first_address(host, service, options) 949 | options = options or {} 950 | 951 | local info = {} 952 | info.host = host 953 | info.service = service 954 | 955 | info.family = options.family or "inet" 956 | info.socket_type = options.socket_type or "stream" 957 | info.protocol = options.protocol or "tcp" 958 | info.flags = options.flags 959 | 960 | if host == "*" then 961 | info.flags = info.flags or {} 962 | table.insert(info.flags, "passive") 963 | end 964 | 965 | local addrinfo, err = M.get_address_info(info) 966 | 967 | if not addrinfo then 968 | return nil, err 969 | end 970 | 971 | if not addrinfo[1] then 972 | return nil, "no addresses found (empty address info table)" 973 | end 974 | 975 | for _, v in ipairs(addrinfo) do 976 | if v.family == info.family and v.socket_type == info.socket_type and v.protocol == info.protocol then 977 | return v 978 | end 979 | end 980 | 981 | return addrinfo[1] 982 | end 983 | 984 | 985 | do 986 | local meta = {} 987 | meta.__index = meta 988 | 989 | function meta:__tostring() 990 | return string.format("socket[%s-%s-%s][%s]", self.family, self.socket_type, self.protocol, self.fd) 991 | end 992 | 993 | function M.create(family, socket_type, protocol) 994 | local fd, err, num = socket.create(AF.strict_lookup(family), SOCK.strict_lookup(socket_type), IPPROTO.strict_lookup(protocol)) 995 | 996 | if not fd then return fd, err, num end 997 | 998 | return setmetatable({ 999 | fd = fd, 1000 | family = family, 1001 | socket_type = socket_type, 1002 | protocol = protocol, 1003 | blocking = true, 1004 | }, meta) 1005 | end 1006 | 1007 | function meta:close() 1008 | if self.on_close then 1009 | self:on_close() 1010 | end 1011 | return socket.close(self.fd) 1012 | end 1013 | 1014 | function meta:set_blocking(b) 1015 | local ok, err, num = socket.blocking(self.fd, b) 1016 | if ok then 1017 | self.blocking = b 1018 | end 1019 | return ok, err, num 1020 | end 1021 | 1022 | function meta:set_option(key, val, level) 1023 | level = level or "socket" 1024 | 1025 | if type(val) == "boolean" then 1026 | val = ffi.new("int[1]", val and 1 or 0) 1027 | elseif type(val) == "number" then 1028 | val = ffi.new("int[1]", val) 1029 | elseif type(val) ~= "cdata" then 1030 | error("unknown value type: " .. type(val)) 1031 | end 1032 | 1033 | local env = SO 1034 | if level == "tcp" then 1035 | env = TCP 1036 | end 1037 | 1038 | return socket.setsockopt(self.fd, SOL.strict_lookup(level), env.strict_lookup(key), ffi.cast("void *", val), ffi.sizeof(val)) 1039 | end 1040 | 1041 | function meta:connect(host, service) 1042 | local res 1043 | 1044 | if type(host) == "table" and host.addrinfo then 1045 | res = host 1046 | else 1047 | local res_, err = M.find_first_address(host, service, { 1048 | family = self.family, 1049 | socket_type = self.socket_type, 1050 | protocol = self.protocol 1051 | }) 1052 | 1053 | if not res_ then 1054 | return res_, err 1055 | end 1056 | 1057 | res = res_ 1058 | end 1059 | 1060 | local ok, err, num = socket.connect(self.fd, res.addrinfo.ai_addr, res.addrinfo.ai_addrlen) 1061 | 1062 | if not ok and not self.blocking then 1063 | if timeout_messages[num] then 1064 | self.timeout_connected = {host, service} 1065 | return true 1066 | end 1067 | elseif self.on_connect then 1068 | self:on_connect(host, service) 1069 | end 1070 | 1071 | if not ok then 1072 | return ok, err, num 1073 | end 1074 | 1075 | return true 1076 | end 1077 | 1078 | function meta:poll_connect() 1079 | if self.on_connect and self.timeout_connected and self:is_connected() then 1080 | local ok, err, num = self:on_connect(unpack(self.timeout_connected)) 1081 | self.timeout_connected = nil 1082 | return ok, err, num 1083 | end 1084 | 1085 | return nil, "timeout" 1086 | end 1087 | 1088 | function meta:bind(host, service) 1089 | if host == "*" then 1090 | host = nil 1091 | end 1092 | 1093 | if type(service) == "number" then 1094 | service = tostring(service) 1095 | end 1096 | 1097 | local res 1098 | 1099 | if type(host) == "table" and host.addrinfo then 1100 | res = host 1101 | else 1102 | local res_, err = M.find_first_address(host, service, { 1103 | family = self.family, 1104 | socket_type = self.socket_type, 1105 | protocol = self.protocol 1106 | }) 1107 | 1108 | if not res_ then 1109 | return res_, err 1110 | end 1111 | 1112 | res = res_ 1113 | end 1114 | 1115 | return socket.bind(self.fd, res.addrinfo.ai_addr, res.addrinfo.ai_addrlen) 1116 | end 1117 | 1118 | function meta:listen(max_connections) 1119 | max_connections = max_connections or e.SOMAXCONN 1120 | return socket.listen(self.fd, max_connections) 1121 | end 1122 | 1123 | function meta:accept() 1124 | local address = ffi.new("struct sockaddr_in[1]") 1125 | local fd, err = socket.accept(self.fd, ffi.cast("struct sockaddr *", address), ffi.new("unsigned int[1]", ffi.sizeof(address))) 1126 | 1127 | if fd ~= socket.INVALID_SOCKET then 1128 | local client = setmetatable({ 1129 | fd = fd, 1130 | family = "unknown", 1131 | socket_type = "unknown", 1132 | protocol = "unknown", 1133 | blocking = true, 1134 | }, meta) 1135 | 1136 | if self.debug then 1137 | print(tostring(self), ": accept client: ", tostring(client)) 1138 | end 1139 | 1140 | return client 1141 | end 1142 | 1143 | local err, num = socket.lasterror() 1144 | 1145 | if not self.blocking and timeout_messages[num] then 1146 | return nil, "timeout", num 1147 | end 1148 | 1149 | if self.debug then 1150 | print(tostring(self), ": accept error", num, ":", err) 1151 | end 1152 | 1153 | return nil, err, num 1154 | end 1155 | 1156 | function meta:is_connected() 1157 | local ip, service, num = self:get_peer_name() 1158 | local ip2, service2, num2 = self:get_name() 1159 | 1160 | if not ip and (num == errno.ECONNRESET or num == errno.ENOTSOCK) then 1161 | return false, service, num 1162 | end 1163 | 1164 | if ffi.os == "Windows" then 1165 | return ip ~= "0.0.0.0" and ip2 ~= "0.0.0.0" and service ~= 0 and service2 ~= 0 1166 | else 1167 | return ip and ip2 and service ~= 0 and service2 ~= 0 1168 | end 1169 | end 1170 | 1171 | function meta:get_peer_name() 1172 | local data = ffi.new("struct sockaddr_in") 1173 | local len = ffi.new("unsigned int[1]", ffi.sizeof(data)) 1174 | 1175 | local ok, err, num = socket.getpeername(self.fd, ffi.cast("struct sockaddr *", data), len) 1176 | if not ok then return ok, err, num end 1177 | 1178 | return ffi.string(socket.inet_ntoa(data.sin_addr)), socket.ntohs(data.sin_port) 1179 | end 1180 | 1181 | function meta:get_name() 1182 | local data = ffi.new("struct sockaddr_in") 1183 | local len = ffi.new("unsigned int[1]", ffi.sizeof(data)) 1184 | 1185 | local ok, err, num = socket.getsockname(self.fd, ffi.cast("struct sockaddr *", data), len) 1186 | if not ok then return ok, err, num end 1187 | 1188 | return ffi.string(socket.inet_ntoa(data.sin_addr)), socket.ntohs(data.sin_port) 1189 | end 1190 | 1191 | local default_flags = 0 1192 | 1193 | if ffi.os ~= "Windows" then 1194 | default_flags = e.MSG_NOSIGNAL 1195 | end 1196 | 1197 | function meta:send_to(addr, data, flags) 1198 | return self:send(data, flags, addr) 1199 | end 1200 | 1201 | function meta:send(data, flags, addr) 1202 | flags = flags or default_flags 1203 | 1204 | if self.on_send then 1205 | return self:on_send(data, flags) 1206 | end 1207 | 1208 | local len, err, num 1209 | 1210 | if addr then 1211 | len, err, num = socket.sendto(self.fd, data, #data, flags, addr.addrinfo.ai_addr, addr.addrinfo.ai_addrlen) 1212 | else 1213 | len, err, num = socket.send(self.fd, data, #data, flags) 1214 | end 1215 | 1216 | if not len then 1217 | return len, err, num 1218 | end 1219 | 1220 | if len > 0 then 1221 | return len 1222 | end 1223 | end 1224 | 1225 | function meta:receive_from(address, size, flags) 1226 | local src_addr 1227 | local src_addr_size 1228 | 1229 | if not address then 1230 | src_addr = ffi.new("struct sockaddr_in[1]") 1231 | src_addr_size = ffi.sizeof("struct sockaddr_in") 1232 | else 1233 | src_addr = address.addrinfo.ai_addr 1234 | src_addr_size = address.addrinfo.ai_addrlen 1235 | end 1236 | 1237 | return self:receive(size, flags, src_addr, src_addr_size) 1238 | end 1239 | 1240 | function meta:receive(size, flags, src_address, address_len) 1241 | size = size or 64000 1242 | local buff = ffi.new("char[?]", size) 1243 | 1244 | if self.on_receive then 1245 | return self:on_receive(buff, size, flags) 1246 | end 1247 | 1248 | local len, err, num 1249 | local len_res 1250 | 1251 | if src_address then 1252 | len_res = ffi.new("int[1]", address_len) 1253 | len, err, num = socket.recvfrom(self.fd, buff, ffi.sizeof(buff), flags or 0, ffi.cast("struct sockaddr *", src_address), len_res) 1254 | else 1255 | len, err, num = socket.recv(self.fd, buff, ffi.sizeof(buff), flags or 0) 1256 | end 1257 | 1258 | if num == errno.ECONNRESET then 1259 | self:close() 1260 | if self.debug then 1261 | print(tostring(self), ": closed") 1262 | end 1263 | 1264 | return nil, "closed", num 1265 | end 1266 | 1267 | if not len then 1268 | if not self.blocking and timeout_messages[num] then 1269 | return nil, "timeout", num 1270 | end 1271 | 1272 | if self.debug then 1273 | print(tostring(self), " error", num, ":", err) 1274 | end 1275 | 1276 | return len, err, num 1277 | end 1278 | 1279 | if len > 0 then 1280 | if self.debug then 1281 | print(tostring(self), ": received ", len, " bytes") 1282 | end 1283 | 1284 | if src_address then 1285 | return ffi.string(buff, len), { 1286 | addrinfo = { 1287 | ai_addr = ffi.cast("struct sockaddr *", src_address), 1288 | ai_addrlen = len_res[0], 1289 | }, 1290 | family = self.family, 1291 | get_port = addrinfo_get_port, 1292 | get_ip = addrinfo_get_ip, 1293 | } 1294 | end 1295 | 1296 | return ffi.string(buff, len) 1297 | end 1298 | 1299 | return nil, err, num 1300 | end 1301 | end 1302 | 1303 | function M.bind(host, service) 1304 | local info, err = M.find_first_address(host, service, { 1305 | family = "inet", 1306 | socket_type = "stream", 1307 | protocol = "tcp", 1308 | flags = {"passive"}, 1309 | }) 1310 | 1311 | if not info then 1312 | return info, err 1313 | end 1314 | 1315 | local server, err, num = M.create(info.family, info.socket_type, info.protocol) 1316 | 1317 | if not server then 1318 | return server, err, num 1319 | end 1320 | 1321 | server:set_option("reuseaddr", 1) 1322 | 1323 | local ok, err, num = server:bind(info) 1324 | 1325 | if not ok then 1326 | return ok, err, num 1327 | end 1328 | 1329 | server:set_option("sndbuf", 65536) 1330 | server:set_option("rcvbuf", 65536) 1331 | 1332 | return server 1333 | end 1334 | 1335 | return M --------------------------------------------------------------------------------