├── .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 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
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 |
177 |
178 |
179 |
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
--------------------------------------------------------------------------------